Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5026be12d2 | ||
|
|
c05daba98b | ||
|
|
2e3a65b76b | ||
|
|
62634b5511 | ||
|
|
27223f587d | ||
|
|
1d35801980 | ||
|
|
269ad18ed6 | ||
|
|
9b9b977ae6 | ||
|
|
f4daaa8ca1 | ||
|
|
e221ad14cd | ||
|
|
268428a119 | ||
|
|
080bf30e38 | ||
|
|
c134a072df | ||
|
|
a905877c76 | ||
|
|
3752dfda91 | ||
|
|
71bf8122d1 | ||
|
|
713c4497c6 | ||
|
|
641c739b64 | ||
|
|
99e458f69b | ||
|
|
bc69a2e8b5 | ||
|
|
20785a8414 | ||
|
|
5c44a622a8 | ||
|
|
bf5a42c03e | ||
|
|
7d2977b665 | ||
|
|
0753bbdce6 | ||
|
|
a631887cd1 | ||
|
|
4ce8d266ab | ||
|
|
68474b56dc | ||
|
|
48faa0d4b1 | ||
|
|
9bf7c46ff6 | ||
|
|
195af14707 | ||
|
|
a9a492274c | ||
|
|
3cf82a7999 | ||
|
|
fea752cc4d | ||
|
|
df7ecfe290 | ||
|
|
cca8bdf68f | ||
|
|
6e568dd866 | ||
|
|
de866a74bf | ||
|
|
9c07fbc815 | ||
|
|
3420fc86d2 | ||
|
|
d737f94003 | ||
|
|
dc91cc9128 | ||
|
|
e6f5adbae1 |
No files matched your search
@@ -1,29 +1,32 @@
|
||||
name: Check formatting
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Prettier Format Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run format check
|
||||
run: npm run format
|
||||
name: Check formatting
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Prettier Format Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run format check
|
||||
run: pnpm run format
|
||||
@@ -1,29 +1,32 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: ESLint Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: ESLint Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run lint
|
||||
run: pnpm run lint
|
||||
@@ -1,219 +1,226 @@
|
||||
name: Publish Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Build artifacts only, skip uploading to GitHub Release'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
allow_update_published_release:
|
||||
description: 'Allow uploading assets to an already published release'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
platform: win32
|
||||
arch: x64
|
||||
- os: windows-11-arm
|
||||
platform: win32
|
||||
arch: arm64
|
||||
- os: macos-15-intel
|
||||
platform: darwin
|
||||
arch: x64
|
||||
- os: macos-14
|
||||
platform: darwin
|
||||
arch: arm64
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
arch: x64
|
||||
- os: ubuntu-24.04-arm
|
||||
platform: linux
|
||||
arch: arm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node and NPM
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.platform == 'linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fakeroot rpm libsecret-1-dev pkg-config
|
||||
|
||||
- name: Ensure WiX Toolset
|
||||
if: matrix.platform == 'win32' && matrix.arch == 'x64'
|
||||
shell: powershell
|
||||
run: |
|
||||
$installed = choco list --local-only wixtoolset --exact --limit-output
|
||||
if (-not $installed) {
|
||||
choco install wixtoolset --version=3.14.0 -y
|
||||
} elseif ($installed -match '^wixtoolset\|3\.14(\.|$)') {
|
||||
Write-Host "WiX Toolset already compatible: $installed"
|
||||
} else {
|
||||
Write-Host "Incompatible WiX Toolset detected: $installed"
|
||||
choco upgrade wixtoolset --version=3.14.0 --allow-downgrade -y
|
||||
}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Make Artifacts
|
||||
if: matrix.platform != 'darwin'
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
run: npm run make -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }}
|
||||
|
||||
# DMG creation may be flaky on GitHub macOS runners, so retry the full darwin make.
|
||||
- name: Make Artifacts (darwin with retry)
|
||||
if: matrix.platform == 'darwin'
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
max_attempts: 3
|
||||
timeout_minutes: 20
|
||||
retry_wait_seconds: 20
|
||||
command: npm run make -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }}
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: make-${{ matrix.platform }}-${{ matrix.arch }}
|
||||
path: |
|
||||
out/make/**/*.exe
|
||||
out/make/**/*.dmg
|
||||
out/make/**/*.zip
|
||||
out/make/**/*.AppImage
|
||||
out/make/**/*.deb
|
||||
out/make/**/*.rpm
|
||||
out/make/**/*.msi
|
||||
out/make/**/*.yml
|
||||
out/make/**/*.txt
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Dry-run summary
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true'
|
||||
run: |
|
||||
echo "Dry run enabled: build artifacts generated and uploaded; release publish step will be skipped."
|
||||
|
||||
publish:
|
||||
name: Publish Release (Manual Approval)
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.build.result == 'success' && (github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true')
|
||||
permissions:
|
||||
contents: write
|
||||
# Configure the `release` environment with required reviewers in GitHub settings
|
||||
# to enforce a manual approval gate before assets are uploaded.
|
||||
environment:
|
||||
name: release
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: make-*
|
||||
merge-multiple: true
|
||||
path: release-assets
|
||||
|
||||
- name: Install Node and NPM
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Resolve release tag
|
||||
id: release_tag
|
||||
env:
|
||||
RELEASE_EVENT_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || '' }}
|
||||
run: |
|
||||
if [ -n "$RELEASE_EVENT_TAG" ]; then
|
||||
tag="$RELEASE_EVENT_TAG"
|
||||
else
|
||||
version="$(node -e "const v=require('./package.json').version; if (!v) { throw new Error('package.json version is empty'); } process.stdout.write(v);")"
|
||||
tag="v$version"
|
||||
fi
|
||||
|
||||
if [ -z "$tag" ]; then
|
||||
echo "::error::Resolved release tag is empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
echo "Using release tag: $tag"
|
||||
|
||||
- name: Ensure release is safe to update
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ steps.release_tag.outputs.tag }}
|
||||
ALLOW_UPDATE_PUBLISHED_RELEASE: ${{ github.event_name == 'release' && 'true' || (github.event_name == 'workflow_dispatch' && github.event.inputs.allow_update_published_release) || 'false' }}
|
||||
run: |
|
||||
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
|
||||
is_draft="$(gh release view "$RELEASE_TAG" --json isDraft --jq '.isDraft')"
|
||||
if [ "$is_draft" != "true" ] && [ "$ALLOW_UPDATE_PUBLISHED_RELEASE" != "true" ]; then
|
||||
echo "::error::Release $RELEASE_TAG is already published. Refusing to upload assets."
|
||||
echo "::error::Re-run with workflow_dispatch input allow_update_published_release=true if you explicitly want to update it."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
gh release create "$RELEASE_TAG" --title "$RELEASE_TAG" --draft --notes "Automated release draft."
|
||||
fi
|
||||
|
||||
- name: Upload assets to release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ steps.release_tag.outputs.tag }}
|
||||
run: |
|
||||
mapfile -t files < <(find release-assets -type f \( \
|
||||
-name '*.exe' -o \
|
||||
-name '*.dmg' -o \
|
||||
-name '*.zip' -o \
|
||||
-name '*.AppImage' -o \
|
||||
-name '*.deb' -o \
|
||||
-name '*.rpm' -o \
|
||||
-name '*.msi' -o \
|
||||
-name '*.yml' -o \
|
||||
-name '*.txt' \
|
||||
\) | sort)
|
||||
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "::error::No release assets found under release-assets/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Uploading %s assets to %s\n' "${#files[@]}" "$RELEASE_TAG"
|
||||
gh release upload "$RELEASE_TAG" "${files[@]}" --clobber
|
||||
name: Publish Release
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Release']
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Build artifacts only, skip uploading to GitHub Release'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
allow_update_published_release:
|
||||
description: 'Allow uploading assets to an already published release'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
check-release:
|
||||
name: Check for new release
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
tag: ${{ steps.get_tag.outputs.tag }}
|
||||
has_release: ${{ steps.get_tag.outputs.has_release }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get latest release tag
|
||||
id: get_tag
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
version="$(node -e "process.stdout.write(require('./package.json').version)")"
|
||||
tag="v$version"
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
echo "has_release=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
tag=$(gh release list --limit 1 --json tagName --jq '.[0].tagName // empty')
|
||||
if [ -n "$tag" ]; then
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
echo "has_release=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Found release: $tag"
|
||||
else
|
||||
echo "has_release=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No release found, skipping build."
|
||||
fi
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: check-release
|
||||
if: needs.check-release.outputs.has_release == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
platform: win32
|
||||
arch: x64
|
||||
- os: windows-11-arm
|
||||
platform: win32
|
||||
arch: arm64
|
||||
- os: macos-15-intel
|
||||
platform: darwin
|
||||
arch: x64
|
||||
- os: macos-14
|
||||
platform: darwin
|
||||
arch: arm64
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
arch: x64
|
||||
- os: ubuntu-24.04-arm
|
||||
platform: linux
|
||||
arch: arm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install Node and NPM
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.platform == 'linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fakeroot rpm libsecret-1-dev pkg-config
|
||||
|
||||
- name: Ensure WiX Toolset
|
||||
if: matrix.platform == 'win32' && matrix.arch == 'x64'
|
||||
shell: powershell
|
||||
run: |
|
||||
$installed = choco list --local-only wixtoolset --exact --limit-output
|
||||
if (-not $installed) {
|
||||
choco install wixtoolset --version=3.14.0 -y
|
||||
} elseif ($installed -match '^wixtoolset\|3\.14(\.|$)') {
|
||||
Write-Host "WiX Toolset already compatible: $installed"
|
||||
} else {
|
||||
Write-Host "Incompatible WiX Toolset detected: $installed"
|
||||
choco upgrade wixtoolset --version=3.14.0 --allow-downgrade -y
|
||||
}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Make Artifacts
|
||||
if: matrix.platform != 'darwin'
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
run: pnpm run make -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }}
|
||||
|
||||
# DMG creation may be flaky on GitHub macOS runners, so retry the full darwin make.
|
||||
- name: Make Artifacts (darwin with retry)
|
||||
if: matrix.platform == 'darwin'
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
max_attempts: 3
|
||||
timeout_minutes: 20
|
||||
retry_wait_seconds: 20
|
||||
command: pnpm run make -- --platform=${{ matrix.platform }} --arch=${{ matrix.arch }}
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: make-${{ matrix.platform }}-${{ matrix.arch }}
|
||||
path: |
|
||||
out/make/**/*.exe
|
||||
out/make/**/*.dmg
|
||||
out/make/**/*.zip
|
||||
out/make/**/*.AppImage
|
||||
out/make/**/*.deb
|
||||
out/make/**/*.rpm
|
||||
out/make/**/*.msi
|
||||
out/make/**/*.yml
|
||||
out/make/**/*.txt
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Dry-run summary
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true'
|
||||
run: |
|
||||
echo "Dry run enabled: build artifacts generated and uploaded; release publish step will be skipped."
|
||||
|
||||
publish:
|
||||
name: Publish Release
|
||||
needs: [check-release, build]
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.build.result == 'success' && (github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true')
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Build Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: make-*
|
||||
merge-multiple: true
|
||||
path: release-assets
|
||||
|
||||
- name: Resolve release tag
|
||||
id: release_tag
|
||||
run: |
|
||||
tag="${{ needs.check-release.outputs.tag }}"
|
||||
if [ -z "$tag" ]; then
|
||||
version="$(node -e "process.stdout.write(require('./package.json').version)")"
|
||||
tag="v$version"
|
||||
fi
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
echo "Using release tag: $tag"
|
||||
|
||||
- name: Upload assets to release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ steps.release_tag.outputs.tag }}
|
||||
run: |
|
||||
mapfile -t files < <(find release-assets -type f \( \
|
||||
-name '*.exe' -o \
|
||||
-name '*.dmg' -o \
|
||||
-name '*.zip' -o \
|
||||
-name '*.AppImage' -o \
|
||||
-name '*.deb' -o \
|
||||
-name '*.rpm' -o \
|
||||
-name '*.msi' -o \
|
||||
-name '*.yml' -o \
|
||||
-name '*.txt' \
|
||||
\) | sort)
|
||||
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "::error::No release assets found under release-assets/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Uploading %s assets to %s\n' "${#files[@]}" "$RELEASE_TAG"
|
||||
gh release upload "$RELEASE_TAG" "${files[@]}" --clobber
|
||||
@@ -1,34 +1,39 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Semantic Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Required for semantic-release to analyze commits
|
||||
persist-credentials: false # We use a token in the next steps
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # Use GH_TOKEN to trigger other workflows
|
||||
run: npx semantic-release
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Semantic Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx semantic-release
|
||||
@@ -1,53 +1,59 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests-unit:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm test
|
||||
|
||||
type-check:
|
||||
name: Type Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run TypeScript type check
|
||||
run: npm run type-check
|
||||
|
||||
# NOTE: E2E tests are disabled in CI because Electron apps require a graphical environment.
|
||||
# Run E2E tests locally with: npm run test:e2e
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests-unit:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run unit tests
|
||||
run: pnpm test
|
||||
|
||||
type-check:
|
||||
name: Type Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run TypeScript type check
|
||||
run: pnpm run type-check
|
||||
|
||||
# NOTE: E2E tests are disabled in CI because Electron apps require a graphical environment.
|
||||
# Run E2E tests locally with: pnpm run test:e2e
|
||||
@@ -125,4 +125,32 @@ cli/aliases.json
|
||||
accounts_backup.json
|
||||
|
||||
# Bun lock file
|
||||
bun.lock
|
||||
bun.lock
|
||||
|
||||
# Build tools (installed via CI)
|
||||
tools/
|
||||
*.zip
|
||||
|
||||
# AI agent session data
|
||||
thoughts/
|
||||
.impeccable.md
|
||||
CLAUDE.md
|
||||
|
||||
# One-shot scripts (already applied)
|
||||
fix-ts.js
|
||||
patch-proxy.js
|
||||
patch-settings.js
|
||||
rename-deep.js
|
||||
test-process.ts
|
||||
|
||||
# Legacy CLI launchers
|
||||
agm.bat
|
||||
run_cli.bat
|
||||
|
||||
# Nix (not used)
|
||||
flake.nix
|
||||
flake.lock
|
||||
|
||||
# Planning/spec data
|
||||
.planning/
|
||||
openspec/
|
||||
@@ -1,29 +0,0 @@
|
||||
## Design Context
|
||||
|
||||
### Users
|
||||
|
||||
Developers and power users who manage multiple AI accounts (Google Gemini, Claude) through a desktop Electron app. They use IDEs like Cursor, OpenCode, and Windsurf. They need to monitor token usage across accounts in real-time to avoid quota exhaustion.
|
||||
|
||||
### Brand Personality
|
||||
|
||||
- **Professional**: Clean, precise, no clutter
|
||||
- **Technical**: Data-dense but readable, developer-friendly
|
||||
- **Efficient**: Information at a glance, minimal clicks
|
||||
- 3 words: "precise", "technical", "focused"
|
||||
|
||||
### Aesthetic Direction
|
||||
|
||||
- Dark mode primary (this is a developer tool used for long sessions)
|
||||
- Deep navy/slate backgrounds, not pure black
|
||||
- Sharp accent colors for data visualization (subtle blues, teals, amber for warnings)
|
||||
- High information density without visual noise
|
||||
- Monospace for numbers/data, clean sans-serif for UI chrome
|
||||
- No gradients on text, no glassmorphism, no rounded-everything
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. Data first — charts and numbers are the heroes
|
||||
2. Every pixel earns its place — no decorative elements
|
||||
3. Density over whitespace — developers want to see everything
|
||||
4. Motion only for state changes — no gratuitous animations
|
||||
5. Consistent with shadcn/ui + Tailwind system already in place
|
||||
@@ -4,17 +4,20 @@
|
||||
> Date: 2026-04-22
|
||||
|
||||
## Current State
|
||||
|
||||
The proxy page (`src/routes/proxy.tsx`) is a 724-line monolith that exposes basic proxy controls (start/stop, port, protocol, API key, code examples) but leaves significant value on the table. The backend (`ProxyService`, `ProxyModule`) already implements many advanced features with no UI exposure.
|
||||
|
||||
## Proposed New Features
|
||||
|
||||
### 1. Live Traffic Monitor
|
||||
|
||||
- Real-time SSE/WebSocket feed of proxied requests
|
||||
- Show request method, model, account used, latency, token count
|
||||
- Filter by status (success, rate-limited, error)
|
||||
- Inspired by: Request logging already exists in `TrafficLogger`
|
||||
|
||||
### 2. IDE Quick-Connect Panel
|
||||
|
||||
- One-click copy of ready-to-use configurations for:
|
||||
- Cursor (`settings.json` snippet)
|
||||
- OpenCode (`.env` snippet)
|
||||
@@ -24,6 +27,7 @@ The proxy page (`src/routes/proxy.tsx`) is a 724-line monolith that exposes basi
|
||||
- QR code for mobile/quick access
|
||||
|
||||
### 3. API Playground
|
||||
|
||||
- Built-in Swagger/OpenAPI UI or custom test panel
|
||||
- Pre-loaded with all supported endpoints:
|
||||
- `/v1/chat/completions` (OpenAI-compatible)
|
||||
@@ -33,6 +37,7 @@ The proxy page (`src/routes/proxy.tsx`) is a 724-line monolith that exposes basi
|
||||
- Show exact curl equivalent for every request
|
||||
|
||||
### 4. Health Dashboard
|
||||
|
||||
- Real-time metrics cards:
|
||||
- Requests/minute
|
||||
- Average latency (p50, p95, p99)
|
||||
@@ -43,6 +48,7 @@ The proxy page (`src/routes/proxy.tsx`) is a 724-line monolith that exposes basi
|
||||
- Color-coded status indicators
|
||||
|
||||
### 5. Model Availability Status
|
||||
|
||||
- Live grid showing which models are currently available
|
||||
- Per-model indicators:
|
||||
- Green: available
|
||||
@@ -52,6 +58,7 @@ The proxy page (`src/routes/proxy.tsx`) is a 724-line monolith that exposes basi
|
||||
- Collapse/expand by provider (Gemini, Anthropic)
|
||||
|
||||
### 6. Error Feed
|
||||
|
||||
- Scrollable log of recent errors
|
||||
- Columns: timestamp, endpoint, model, account, error code, message
|
||||
- Click to expand full request/response
|
||||
@@ -59,6 +66,7 @@ The proxy page (`src/routes/proxy.tsx`) is a 724-line monolith that exposes basi
|
||||
- Export to JSON/CSV
|
||||
|
||||
### 7. Connection Visualizer
|
||||
|
||||
- Diagram showing: IDE → Proxy → Account Pool → Upstream API
|
||||
- Animated flow dots representing live requests
|
||||
- Highlight currently active account
|
||||
@@ -66,6 +74,7 @@ The proxy page (`src/routes/proxy.tsx`) is a 724-line monolith that exposes basi
|
||||
- Simple but effective for understanding routing
|
||||
|
||||
### 8. One-Click Diagnostics
|
||||
|
||||
- "Run Diagnostics" button that performs:
|
||||
- Check if proxy port is accessible
|
||||
- Test each configured account
|
||||
@@ -75,6 +84,7 @@ The proxy page (`src/routes/proxy.tsx`) is a 724-line monolith that exposes basi
|
||||
- Output: green checkmarks or detailed error report with fix suggestions
|
||||
|
||||
### 9. Session Stats
|
||||
|
||||
- Summary of current session since proxy started:
|
||||
- Total requests handled
|
||||
- Total tokens proxied
|
||||
@@ -86,6 +96,7 @@ The proxy page (`src/routes/proxy.tsx`) is a 724-line monolith that exposes basi
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Many of these features require new IPC endpoints in `src/ipc/proxy/router.ts`
|
||||
- Health metrics likely already exist in `ProxyService` but need exposure
|
||||
- Traffic logging exists via `TrafficLogger` — just needs UI
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
> Date: 2026-04-22
|
||||
|
||||
## Current Pain Points
|
||||
|
||||
1. **Visual noise**: Gradient backgrounds (`bg-gradient-to-br`) feel dated and clash with the dark, technical aesthetic
|
||||
2. **Information overload**: All content is visible at once — no progressive disclosure
|
||||
3. **Code examples dominate**: Large, always-visible code blocks push important controls below the fold
|
||||
@@ -14,29 +15,35 @@
|
||||
## Proposed Improvements
|
||||
|
||||
### 1. Remove Gradient Backgrounds
|
||||
|
||||
- Replace `bg-gradient-to-br from-slate-800 to-slate-900` with solid `bg-card` or subtle borders
|
||||
- Use elevation (borders, subtle shadows) instead of gradients to create hierarchy
|
||||
- Align with the rest of the app which uses flat, technical styling
|
||||
|
||||
### 2. Tabs-Based Layout
|
||||
|
||||
Organize the page into tabs:
|
||||
|
||||
- **Status** (default): Service state, quick actions, health metrics
|
||||
- **Routing**: Model mapping, account scheduling, protocol settings
|
||||
- **API Docs**: Code examples, endpoint documentation, IDE configs
|
||||
- **Logs**: Live traffic feed, error history
|
||||
|
||||
### 3. Compact Protocol Selector
|
||||
|
||||
- Current: large buttons with icons
|
||||
- Proposed: `Select` dropdown or compact segmented control
|
||||
- Save horizontal space for more important controls
|
||||
|
||||
### 4. Collapsible Code Examples
|
||||
|
||||
- Wrap each code block in a `Collapsible` or `Accordion`
|
||||
- Show only the title + language icon by default
|
||||
- Expand on click
|
||||
- Add nested tabs for different languages (cURL, Python, JavaScript, TypeScript)
|
||||
|
||||
### 5. Security-Focused API Key Display
|
||||
|
||||
- Mask the API key by default (show `sk-••••••••`)
|
||||
- Reveal on explicit "Show" toggle
|
||||
- Add "Regenerate" button with confirmation dialog
|
||||
@@ -44,6 +51,7 @@ Organize the page into tabs:
|
||||
- Add warning if key is the default/weak key
|
||||
|
||||
### 6. Configurable Model Mapping Table
|
||||
|
||||
- Instead of a static list, show an editable table:
|
||||
- Requested model | Resolved model | Provider | Status
|
||||
- Allow users to add custom mappings
|
||||
@@ -51,21 +59,25 @@ Organize the page into tabs:
|
||||
- Validate that resolved models exist in the account pool
|
||||
|
||||
### 7. Account Quota Context
|
||||
|
||||
- Show the currently selected "primary" account with its quota bar
|
||||
- If auto-switching is enabled, show "Pool Mode: Active (N accounts)"
|
||||
- Link to the account pool page for detailed management
|
||||
|
||||
### 8. Toast Copy Feedback
|
||||
|
||||
- When user clicks "Copy", show a `Toast` or `Sonner` notification
|
||||
- Current: no feedback — user doesn't know if copy succeeded
|
||||
- Message: "Copied to clipboard" or "Failed to copy"
|
||||
|
||||
### 9. Health Metrics Stub
|
||||
|
||||
- Even if backend metrics aren't wired yet, create the UI skeleton
|
||||
- Show placeholder charts with "Coming soon" badge
|
||||
- This sets expectations and makes the page feel more complete
|
||||
|
||||
### 10. Empty States
|
||||
|
||||
- When proxy is stopped, show a clear empty state:
|
||||
- Icon: `ServerOff` from lucide
|
||||
- Message: "Proxy is not running"
|
||||
@@ -75,6 +87,7 @@ Organize the page into tabs:
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
- **Density over whitespace**: This is a developer tool — show more information per pixel
|
||||
- **Progressive disclosure**: Hide advanced options behind tabs/accordions
|
||||
- **Immediate feedback**: Every action should have a visible result (toast, spinner, state change)
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
> Date: 2026-04-22
|
||||
|
||||
## Discovery
|
||||
|
||||
The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many sophisticated features that are **completely invisible** in the current UI. The proxy page only shows: port, protocol, API key, and basic start/stop.
|
||||
|
||||
## Hidden Backend Features
|
||||
|
||||
### 1. Advanced Routing & Scheduling Controls
|
||||
|
||||
**Backend location**: `proxy.service.ts`, `proxy-scheduler.service.ts`
|
||||
**What's implemented**:
|
||||
|
||||
- `scheduler.maxRequestsPerAccount`: Limit requests per account before switching
|
||||
- `scheduler.maxWaitTime`: Max time to wait for an available account
|
||||
- `scheduler.retryAttempts`: Number of retries per request
|
||||
@@ -20,8 +23,10 @@ The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many s
|
||||
**UI exposure**: None. These are hardcoded or set via config files only.
|
||||
|
||||
### 2. Live Traffic Inspector
|
||||
|
||||
**Backend location**: `traffic-logger.service.ts`
|
||||
**What's implemented**:
|
||||
|
||||
- `TrafficLogger` captures every request/response with metadata
|
||||
- Fields: timestamp, requestId, accountId, model, promptTokens, completionTokens, latency, status, error
|
||||
- Log rotation and cleanup policies
|
||||
@@ -30,8 +35,10 @@ The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many s
|
||||
**UI exposure**: None. Logs are only written to files.
|
||||
|
||||
### 3. Circuit Breaker Dashboard
|
||||
|
||||
**Backend location**: `proxy-circuit-breaker.service.ts`
|
||||
**What's implemented**:
|
||||
|
||||
- Per-account circuit breaker state: `CLOSED`, `OPEN`, `HALF_OPEN`
|
||||
- Failure threshold and recovery timeout
|
||||
- When open: requests fail fast without hitting the API
|
||||
@@ -40,8 +47,10 @@ The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many s
|
||||
**UI exposure**: None. Circuit breaker state is internal only.
|
||||
|
||||
### 4. Parity Testing Sandbox
|
||||
|
||||
**Backend location**: `proxy-parity.service.ts`
|
||||
**What's implemented**:
|
||||
|
||||
- Send identical requests to multiple models simultaneously
|
||||
- Compare responses for consistency
|
||||
- Detect model drift or API changes
|
||||
@@ -50,8 +59,10 @@ The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many s
|
||||
**UI exposure**: None. Only accessible via manual API calls or scripts.
|
||||
|
||||
### 5. Custom Model Mapping Table
|
||||
|
||||
**Backend location**: `proxy-model-mapper.service.ts`
|
||||
**What's implemented**:
|
||||
|
||||
- Dynamic model resolution: `claude-opus-4` → `gemini-3-flash`
|
||||
- User-defined mappings stored in database
|
||||
- Regex-based model matching
|
||||
@@ -60,8 +71,10 @@ The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many s
|
||||
**UI exposure**: A static, read-only list is shown. No editing capability.
|
||||
|
||||
### 6. Upstream Proxy Configuration
|
||||
|
||||
**Backend location**: `proxy-upstream.service.ts`
|
||||
**What's implemented**:
|
||||
|
||||
- `upstreamProxy.url`: Route all traffic through another proxy
|
||||
- `upstreamProxy.auth`: Basic auth or bearer token for upstream
|
||||
- `upstreamProxy.bypassList`: Domains to bypass
|
||||
@@ -69,8 +82,10 @@ The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many s
|
||||
**UI exposure**: None. Set via environment variables only.
|
||||
|
||||
### 7. IDE Integration Quick-Setup
|
||||
|
||||
**Backend location**: `proxy-config.service.ts` — `generateIdeConfig()`
|
||||
**What's implemented**:
|
||||
|
||||
- Generates ready-to-paste configs for:
|
||||
- Cursor (`settings.json`)
|
||||
- VS Code + Cline/Roo Code
|
||||
@@ -82,8 +97,10 @@ The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many s
|
||||
**UI exposure**: Code examples are generic curl snippets. No IDE-specific configs.
|
||||
|
||||
### 8. Health & Performance Metrics
|
||||
|
||||
**Backend location**: `proxy-metrics.service.ts`
|
||||
**What's implemented**:
|
||||
|
||||
- Request latency histogram (p50, p95, p99)
|
||||
- Requests per minute counter
|
||||
- Error rate by status code
|
||||
@@ -94,8 +111,10 @@ The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many s
|
||||
**UI exposure**: None. Metrics are only exposed via a `/metrics` endpoint for Prometheus.
|
||||
|
||||
### 9. Request Replay & Debug Tool
|
||||
|
||||
**Backend location**: `proxy-replay.service.ts`
|
||||
**What's implemented**:
|
||||
|
||||
- Store recent requests (last 100 by default)
|
||||
- Replay any request with one click
|
||||
- Modify headers/body before replay
|
||||
@@ -105,8 +124,10 @@ The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many s
|
||||
**UI exposure**: None.
|
||||
|
||||
### 10. Global Timeout & Max Wait Controls
|
||||
|
||||
**Backend location**: `proxy-config.service.ts`
|
||||
**What's implemented**:
|
||||
|
||||
- `requestTimeout`: Global timeout for all upstream requests
|
||||
- `connectTimeout`: TCP connection timeout
|
||||
- `maxWaitTime`: Max time to wait for an available account
|
||||
@@ -117,7 +138,9 @@ The backend (`ProxyService`, `ProxyController`, `ProxyModule`) implements many s
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
Create a new "Advanced" tab on the proxy page that exposes these features progressively:
|
||||
|
||||
1. **Phase 1**: Read-only views (circuit breaker status, health metrics, traffic logs)
|
||||
2. **Phase 2**: Interactive controls (model mapping editor, scheduler settings)
|
||||
3. **Phase 3**: Power-user tools (replay, parity testing, upstream proxy config)
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
> Date: 2026-04-22
|
||||
|
||||
## Current State
|
||||
|
||||
`src/routes/proxy.tsx` is a **724-line monolith** containing:
|
||||
|
||||
- Route definition
|
||||
- State management (useState, useEffect, useMemo)
|
||||
- Data fetching (useQuery equivalents for proxy config, local IPs)
|
||||
@@ -14,6 +16,7 @@
|
||||
- Constants and helper functions
|
||||
|
||||
## Target Architecture
|
||||
|
||||
Split into **7+ files** following the project's conventions:
|
||||
|
||||
```
|
||||
@@ -39,7 +42,9 @@ src/
|
||||
## File Breakdown
|
||||
|
||||
### 1. `src/hooks/useProxyConfig.ts`
|
||||
|
||||
**Responsibility**: Fetch and mutate proxy configuration
|
||||
|
||||
```typescript
|
||||
export function useProxyConfig() {
|
||||
// Fetches from ipc.client.proxy.getConfig()
|
||||
@@ -49,7 +54,9 @@ export function useProxyConfig() {
|
||||
```
|
||||
|
||||
### 2. `src/hooks/useLocalIps.ts`
|
||||
|
||||
**Responsibility**: Get local IP addresses for the access banner
|
||||
|
||||
```typescript
|
||||
export function useLocalIps() {
|
||||
// Fetches from ipc.client.system.getLocalIps()
|
||||
@@ -58,7 +65,9 @@ export function useLocalIps() {
|
||||
```
|
||||
|
||||
### 3. `src/hooks/useProxyExamples.ts`
|
||||
|
||||
**Responsibility**: Generate code examples based on current config
|
||||
|
||||
```typescript
|
||||
export function useProxyExamples(config: ProxyConfig, localIps: string[]) {
|
||||
// Returns memoized examples for curl, Python, Node.js
|
||||
@@ -67,7 +76,9 @@ export function useProxyExamples(config: ProxyConfig, localIps: string[]) {
|
||||
```
|
||||
|
||||
### 4. `src/components/proxy/ProxyServiceCard.tsx`
|
||||
|
||||
**Responsibility**: The main service control card
|
||||
|
||||
- Start/stop toggle
|
||||
- Port input
|
||||
- Protocol selector
|
||||
@@ -75,26 +86,34 @@ export function useProxyExamples(config: ProxyConfig, localIps: string[]) {
|
||||
- Status indicator
|
||||
|
||||
### 5. `src/components/proxy/ProxyModelMappingCard.tsx`
|
||||
|
||||
**Responsibility**: Model mapping display/editor
|
||||
|
||||
- Read-only list (current state)
|
||||
- Future: editable table with add/remove
|
||||
- Show provider badges
|
||||
|
||||
### 6. `src/components/proxy/ProxyUsageExamplesCard.tsx`
|
||||
|
||||
**Responsibility**: Code examples section
|
||||
|
||||
- Collapsible/accordion layout
|
||||
- Tabs for different languages
|
||||
- Copy-to-clipboard buttons
|
||||
- Syntax highlighting (if available)
|
||||
|
||||
### 7. `src/components/proxy/ProxyLocalAccessBanner.tsx`
|
||||
|
||||
**Responsibility**: "Access your proxy locally" banner
|
||||
|
||||
- Show local IPs
|
||||
- QR code (future)
|
||||
- Copy URL buttons
|
||||
|
||||
### 8. `src/constants/proxy.ts`
|
||||
|
||||
**Responsibility**: Constants
|
||||
|
||||
```typescript
|
||||
export const DEFAULT_PROXY_PORT = 3000;
|
||||
export const PROTOCOLS = ['http', 'https'] as const;
|
||||
@@ -102,7 +121,9 @@ export const EXAMPLE_MODELS = ['gpt-4o', 'claude-3-opus', 'gemini-pro'];
|
||||
```
|
||||
|
||||
### 9. `src/utils/proxy.ts`
|
||||
|
||||
**Responsibility**: Pure helper functions
|
||||
|
||||
```typescript
|
||||
export function buildProxyUrl(protocol: string, ip: string, port: number): string;
|
||||
export function maskApiKey(key: string): string;
|
||||
@@ -141,6 +162,7 @@ export const Route = createFileRoute('/proxy')({ component: ProxyPage });
|
||||
```
|
||||
|
||||
## Migration Steps
|
||||
|
||||
1. Create new files (hooks, components, constants, utils)
|
||||
2. Move code from `proxy.tsx` to appropriate files
|
||||
3. Update imports in `proxy.tsx`
|
||||
@@ -149,6 +171,7 @@ export const Route = createFileRoute('/proxy')({ component: ProxyPage });
|
||||
6. Verify no regression in functionality
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Testability**: Each hook and component can be unit tested in isolation
|
||||
- **Reusability**: `useLocalIps` can be used in other pages (e.g., settings)
|
||||
- **Maintainability**: 50-line route file vs. 724-line monolith
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
> Date: 2026-04-22
|
||||
|
||||
## Severity Legend
|
||||
|
||||
- 🔴 **Critical**: Bugs or crashes
|
||||
- 🟡 **High**: Performance or reliability issues
|
||||
- 🟢 **Medium**: Maintainability or best practice violations
|
||||
@@ -11,21 +12,26 @@
|
||||
## Issues Found
|
||||
|
||||
### 1. 🔴 State Duplication Between Proxy Config and Local State
|
||||
|
||||
**Location**: Lines ~120-180
|
||||
**Issue**: The component maintains both `proxyConfig` (from IPC) and local `useState` copies for `port`, `protocol`, `apiKey`. These can drift out of sync if the backend config changes externally.
|
||||
**Fix**: Use a single source of truth. Either:
|
||||
|
||||
- Derive local state from `proxyConfig` with `useEffect` sync
|
||||
- Or use `useQuery` with `staleTime: 0` and no local cache
|
||||
|
||||
### 2. 🟡 Race Condition in Start/Stop Toggle
|
||||
|
||||
**Location**: Lines ~200-250
|
||||
**Issue**: If user clicks start/stop rapidly, multiple IPC calls fire in parallel. The UI shows intermediate states that may not reflect the actual backend state.
|
||||
**Fix**: Disable the toggle button while a transition is in progress. Add an `isTransitioning` flag.
|
||||
|
||||
### 3. 🟢 Clipboard Error Handling
|
||||
|
||||
**Location**: Lines ~300-320
|
||||
**Issue**: `navigator.clipboard.writeText()` can fail (permission denied, not in secure context). The code has no try/catch.
|
||||
**Fix**:
|
||||
|
||||
```typescript
|
||||
async function copyToClipboard(text: string) {
|
||||
try {
|
||||
@@ -38,18 +44,22 @@ async function copyToClipboard(text: string) {
|
||||
```
|
||||
|
||||
### 4. 🟢 Unstable Callbacks Causing Re-renders
|
||||
|
||||
**Location**: Throughout
|
||||
**Issue**: Event handlers are defined inline in JSX, causing child components to re-render unnecessarily.
|
||||
**Fix**: Wrap handlers in `useCallback`:
|
||||
|
||||
```typescript
|
||||
const handleStart = useCallback(() => startProxy(), [startProxy]);
|
||||
const handleStop = useCallback(() => stopProxy(), [stopProxy]);
|
||||
```
|
||||
|
||||
### 5. 🟡 Memory Leak in setTimeout
|
||||
|
||||
**Location**: Lines ~400-420
|
||||
**Issue**: A `setTimeout` is used for a "copy feedback" animation but is never cleared on unmount.
|
||||
**Fix**: Use `useEffect` cleanup:
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setCopied(false), 2000);
|
||||
@@ -58,24 +68,29 @@ useEffect(() => {
|
||||
```
|
||||
|
||||
### 6. 🟢 Missing Loading States for Mutations
|
||||
|
||||
**Location**: Lines ~200-250
|
||||
**Issue**: When starting/stopping the proxy, there's no loading spinner or disabled state on the button.
|
||||
**Fix**: Add `isPending` state to the mutation hook and disable the button + show spinner while pending.
|
||||
|
||||
### 7. 🟢 Hardcoded i18n Strings
|
||||
|
||||
**Location**: Throughout
|
||||
**Issue**: Many UI strings are hardcoded in English instead of using `useTranslation()`:
|
||||
|
||||
- "Proxy Service"
|
||||
- "Start Proxy"
|
||||
- "Stop Proxy"
|
||||
- "Copy"
|
||||
- "Copied!"
|
||||
**Fix**: Extract all strings to `src/localization/i18n.ts` and use `t('proxy.startProxy')`, etc.
|
||||
**Fix**: Extract all strings to `src/localization/i18n.ts` and use `t('proxy.startProxy')`, etc.
|
||||
|
||||
### 8. 🟢 Magic Numbers
|
||||
|
||||
**Location**: Lines ~450-500
|
||||
**Issue**: Port validation uses hardcoded `3000`, `65535` without constants.
|
||||
**Fix**:
|
||||
|
||||
```typescript
|
||||
const MIN_PORT = 1024;
|
||||
const MAX_PORT = 65535;
|
||||
@@ -83,9 +98,11 @@ const DEFAULT_PORT = 3000;
|
||||
```
|
||||
|
||||
### 9. 🟢 Unsafe Type Cast
|
||||
|
||||
**Location**: Lines ~550-600
|
||||
**Issue**: `event.target.value` is cast to `Protocol` without validation.
|
||||
**Fix**:
|
||||
|
||||
```typescript
|
||||
const protocol = event.target.value;
|
||||
if (!PROTOCOLS.includes(protocol)) return;
|
||||
@@ -93,22 +110,27 @@ setProtocol(protocol as Protocol);
|
||||
```
|
||||
|
||||
### 10. 🟡 Dangerous Parsing of Local IPs
|
||||
|
||||
**Location**: Lines ~650-700
|
||||
**Issue**: `localIps` is parsed from a shell command output using string splitting that could break on unexpected formats.
|
||||
**Fix**: Use a robust parser or, better, expose `os.networkInterfaces()` via a safe IPC endpoint.
|
||||
|
||||
### 11. 🟢 Unnecessary Re-renders from Inline Objects
|
||||
|
||||
**Location**: Lines ~350-380
|
||||
**Issue**: Inline style objects and arrays are recreated on every render, breaking `React.memo` optimizations in child components.
|
||||
**Fix**: Extract static styles outside the component or memoize:
|
||||
|
||||
```typescript
|
||||
const cardStyle = useMemo(() => ({ padding: '1.5rem' }), []);
|
||||
```
|
||||
|
||||
### 12. 🟢 No Validation on Port Input
|
||||
|
||||
**Location**: Lines ~150-170
|
||||
**Issue**: User can type any value into the port field, including non-numbers, negatives, or out-of-range values.
|
||||
**Fix**: Add input validation:
|
||||
|
||||
```typescript
|
||||
<input
|
||||
type="number"
|
||||
@@ -125,6 +147,7 @@ const cardStyle = useMemo(() => ({ padding: '1.5rem' }), []);
|
||||
---
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
- Add unit tests for `useProxyConfig` hook (mock IPC client)
|
||||
- Test race conditions: rapid start/stop clicks
|
||||
- Test clipboard failure path
|
||||
|
||||
@@ -6,41 +6,45 @@
|
||||
## File Map
|
||||
|
||||
### Frontend
|
||||
| File | Lines | Role |
|
||||
|------|-------|------|
|
||||
| `src/routes/proxy.tsx` | 724 | Main proxy page (monolith) |
|
||||
| `src/components/ModelVisibilitySettings.tsx` | ~200 | Model visibility toggle grid |
|
||||
| `src/components/AccountProxySettings.tsx` | ~180 | Per-account proxy configuration |
|
||||
| `src/hooks/useCloudAccounts.ts` | ~50 | Fetches cloud accounts for proxy routing |
|
||||
|
||||
| File | Lines | Role |
|
||||
| -------------------------------------------- | ----- | ---------------------------------------- |
|
||||
| `src/routes/proxy.tsx` | 724 | Main proxy page (monolith) |
|
||||
| `src/components/ModelVisibilitySettings.tsx` | ~200 | Model visibility toggle grid |
|
||||
| `src/components/AccountProxySettings.tsx` | ~180 | Per-account proxy configuration |
|
||||
| `src/hooks/useCloudAccounts.ts` | ~50 | Fetches cloud accounts for proxy routing |
|
||||
|
||||
### Backend (NestJS)
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `src/server/modules/proxy/proxy.module.ts` | NestJS module definition |
|
||||
| `src/server/modules/proxy/proxy.service.ts` | 1883 lines — core proxy logic, all handlers |
|
||||
| `src/server/modules/proxy/proxy.controller.ts` | HTTP endpoints for proxy control |
|
||||
| `src/server/modules/proxy/proxy-scheduler.service.ts` | Account rotation & scheduling |
|
||||
| `src/server/modules/proxy/proxy-circuit-breaker.service.ts` | Per-account circuit breaker |
|
||||
| `src/server/modules/proxy/traffic-logger.service.ts` | Request/response logging |
|
||||
| `src/server/modules/proxy/proxy-metrics.service.ts` | Prometheus metrics |
|
||||
| `src/server/modules/proxy/proxy-model-mapper.service.ts` | Model name resolution |
|
||||
| `src/server/modules/proxy/proxy-parity.service.ts` | Parity testing |
|
||||
| `src/server/modules/proxy/proxy-replay.service.ts` | Request replay |
|
||||
| `src/server/modules/proxy/proxy-config.service.ts` | Config management |
|
||||
| `src/server/modules/proxy/proxy-upstream.service.ts` | Upstream proxy support |
|
||||
|
||||
| File | Role |
|
||||
| ----------------------------------------------------------- | ------------------------------------------- |
|
||||
| `src/server/modules/proxy/proxy.module.ts` | NestJS module definition |
|
||||
| `src/server/modules/proxy/proxy.service.ts` | 1883 lines — core proxy logic, all handlers |
|
||||
| `src/server/modules/proxy/proxy.controller.ts` | HTTP endpoints for proxy control |
|
||||
| `src/server/modules/proxy/proxy-scheduler.service.ts` | Account rotation & scheduling |
|
||||
| `src/server/modules/proxy/proxy-circuit-breaker.service.ts` | Per-account circuit breaker |
|
||||
| `src/server/modules/proxy/traffic-logger.service.ts` | Request/response logging |
|
||||
| `src/server/modules/proxy/proxy-metrics.service.ts` | Prometheus metrics |
|
||||
| `src/server/modules/proxy/proxy-model-mapper.service.ts` | Model name resolution |
|
||||
| `src/server/modules/proxy/proxy-parity.service.ts` | Parity testing |
|
||||
| `src/server/modules/proxy/proxy-replay.service.ts` | Request replay |
|
||||
| `src/server/modules/proxy/proxy-config.service.ts` | Config management |
|
||||
| `src/server/modules/proxy/proxy-upstream.service.ts` | Upstream proxy support |
|
||||
|
||||
### IPC Layer
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `src/ipc/router.ts` | Main IPC router composition |
|
||||
|
||||
| File | Role |
|
||||
| ------------------------- | ---------------------------- |
|
||||
| `src/ipc/router.ts` | Main IPC router composition |
|
||||
| `src/ipc/proxy/router.ts` | Proxy-specific IPC endpoints |
|
||||
| `src/ipc/manager.ts` | IPC client initialization |
|
||||
| `src/ipc/manager.ts` | IPC client initialization |
|
||||
|
||||
### Types & Schema
|
||||
| File | Role |
|
||||
|------|------|
|
||||
|
||||
| File | Role |
|
||||
| --------------------- | ------------------------------------------------ |
|
||||
| `src/types/config.ts` | `ProxyConfig` Zod schema — reveals hidden fields |
|
||||
| `src/types/proxy.ts` | Request/response types |
|
||||
| `src/types/proxy.ts` | Request/response types |
|
||||
|
||||
## Hidden ProxyConfig Fields
|
||||
|
||||
@@ -124,6 +128,7 @@ interface ProxyConfig {
|
||||
```
|
||||
|
||||
## Key Observations
|
||||
|
||||
1. **Backend is significantly more capable than the UI suggests** — 12 services vs. 1 page
|
||||
2. **IPC layer is thin** — most backend services are not exposed via IPC, only HTTP
|
||||
3. **ProxyConfig schema is the source of truth** — it documents everything the backend can do
|
||||
@@ -131,6 +136,7 @@ interface ProxyConfig {
|
||||
5. **Separation of concerns is good on backend**, terrible on frontend (724-line monolith)
|
||||
|
||||
## Recommended Architecture Changes
|
||||
|
||||
1. Extend `src/ipc/proxy/router.ts` to expose all `ProxyConfig` fields
|
||||
2. Add IPC endpoints for:
|
||||
- `proxy.getTrafficLogs()`
|
||||
|
||||
@@ -12,7 +12,7 @@ This document is a user-level guide for AI assistants working in this repository
|
||||
|
||||
- **Runtime**: Node.js (Electron environment)
|
||||
- **Node**: Recommended Node.js 20+
|
||||
- **Package manager**: `npm` (this project includes `package-lock.json`; use npm only)
|
||||
- **Package manager**: `pnpm` (this project includes `pnpm-lock.yaml`; use pnpm only)
|
||||
- **Build tools**: Electron Forge + Vite
|
||||
- **Terminal**: Windows (PowerShell) / VSCode MCP tools can be used safely
|
||||
|
||||
@@ -75,32 +75,32 @@ This document is a user-level guide for AI assistants working in this repository
|
||||
|
||||
## 📦 Common Scripts
|
||||
|
||||
Use `npm` for all commands:
|
||||
Use `pnpm` for all commands:
|
||||
|
||||
- **Development (Dev)**:
|
||||
- `npm start` - Start Electron dev environment (Electron Forge)
|
||||
- `npm run lint` - Run ESLint checks
|
||||
- `npm run format` - Run Prettier check
|
||||
- `npm run format:write` - Auto-format with Prettier
|
||||
- `npm run type-check` - Run TypeScript type check
|
||||
- `pnpm start` - Start Electron dev environment (Electron Forge)
|
||||
- `pnpm run lint` - Run ESLint checks
|
||||
- `pnpm run format` - Run Prettier check
|
||||
- `pnpm run format:write` - Auto-format with Prettier
|
||||
- `pnpm run type-check` - Run TypeScript type check
|
||||
|
||||
- **Build**:
|
||||
- `npm run package` - Package app (application bundle only)
|
||||
- `npm run make` - Build and generate distributable installers
|
||||
- `npm run publish` - Publish app
|
||||
- `pnpm run package` - Package app (application bundle only)
|
||||
- `pnpm run make` - Build and generate distributable installers
|
||||
- `pnpm run publish` - Publish app
|
||||
|
||||
- **Testing**:
|
||||
- `npm test` - Run Vitest tests
|
||||
- `npm run test:watch` - Run Vitest in watch mode
|
||||
- `npm run test:unit` - Same as above for unit-focused runs
|
||||
- `npm run test:e2e` - Run Playwright E2E tests
|
||||
- `npm run test:all` - Run all tests
|
||||
- `pnpm test` - Run Vitest tests
|
||||
- `pnpm run test:watch` - Run Vitest in watch mode
|
||||
- `pnpm run test:unit` - Same as above for unit-focused runs
|
||||
- `pnpm run test:e2e` - Run Playwright E2E tests
|
||||
- `pnpm run test:all` - Run all tests
|
||||
|
||||
### Running a Single Test
|
||||
|
||||
- Unit test: `npm run test:unit path/to/test.test.ts`
|
||||
- E2E test: `npm run test:e2e path/to/test.spec.ts`
|
||||
- Type check: `npm run type-check`
|
||||
- Unit test: `pnpm run test:unit path/to/test.test.ts`
|
||||
- E2E test: `pnpm run test:e2e path/to/test.spec.ts`
|
||||
- Type check: `pnpm run type-check`
|
||||
|
||||
## 🧪 Development Notes
|
||||
|
||||
@@ -171,7 +171,7 @@ export const Component: React.FC<ComponentProps> = ({ prop1 }) => {
|
||||
};
|
||||
```
|
||||
|
||||
> Before commit, run `npm run lint` and `npm run format`.
|
||||
> Before commit, run `pnpm run lint` and `pnpm run format`.
|
||||
|
||||
## 📝 Terminal Output and References
|
||||
|
||||
@@ -330,7 +330,7 @@ Summary / output:
|
||||
|
||||
Packages and scripts:
|
||||
|
||||
- `npm install` (or `npm i`)
|
||||
- `pnpm install` (or `pnpm i`)
|
||||
|
||||
Shell:
|
||||
|
||||
|
||||
@@ -1,3 +1,61 @@
|
||||
<a name="readme-top"></a>
|
||||
|
||||
# Changelog
|
||||
|
||||
## [0.15.2](https://github.com/evandrodevbr/GeminiNexus/compare/v0.15.1...v0.15.2) (2026-05-02)
|
||||
|
||||
### 👷 Build System
|
||||
|
||||
* allow macos-alias and other native modules to build in pnpm v10 ([2e3a65b](https://github.com/evandrodevbr/GeminiNexus/commit/2e3a65b76b91012638c422f6223b79399775c635))
|
||||
|
||||
## [0.15.1](https://github.com/evandrodevbr/GeminiNexus/compare/v0.15.0...v0.15.1) (2026-05-02)
|
||||
|
||||
### ✅ Tests
|
||||
|
||||
* add universal electron mock to fix headless CI test runs ([1d35801](https://github.com/evandrodevbr/GeminiNexus/commit/1d35801980e3e54cec1d3e52ed75850eb7eec350))
|
||||
* fix test environments failing due to missing IS_DEV_ENVIRONMENT mock export ([269ad18](https://github.com/evandrodevbr/GeminiNexus/commit/269ad18ed6c09585e881d0d916558b1bcc7ad940))
|
||||
* update proxy-advanced-handler tests to reflect optional service registration ([f4daaa8](https://github.com/evandrodevbr/GeminiNexus/commit/f4daaa8ca13bc75c7e5090905176c06b61ecfedc))
|
||||
|
||||
### 👷 Build System
|
||||
|
||||
* replace MakerSquirrel with MakerZIP for Windows ([27223f5](https://github.com/evandrodevbr/GeminiNexus/commit/27223f587d7facf25beef77aa6984cd248f2703d))
|
||||
|
||||
## [0.15.0](https://github.com/evandrodevbr/GeminiNexus/compare/v0.14.0...v0.15.0) (2026-05-02)
|
||||
|
||||
### ✨ Features
|
||||
|
||||
* bump version to 0.14.0 ([a905877](https://github.com/evandrodevbr/GeminiNexus/commit/a905877c76869a9b31b5b079d7afd86e4c905b59))
|
||||
* **core:** isolate dev and production environments ([5c44a62](https://github.com/evandrodevbr/GeminiNexus/commit/5c44a622a8b4fa81246d4ca77073be50b4ddc1c9))
|
||||
* **proxy:** add OpenCode IDE config and fix service availability ([713c449](https://github.com/evandrodevbr/GeminiNexus/commit/713c4497c623b0519847ed2afcc7da04997dae55))
|
||||
* **proxy:** enrich traffic logs with model and token metadata ([641c739](https://github.com/evandrodevbr/GeminiNexus/commit/641c739b64150d1df77fd1a81a028b8cb4a4884d))
|
||||
* **ui:** replace sidebar with compact top header navigation ([7d2977b](https://github.com/evandrodevbr/GeminiNexus/commit/7d2977b665a1a2fb0d5d8844a8e519471cdef8d8))
|
||||
* **usage:** add cost reporting with custom model pricing and OpenRouter ([bc69a2e](https://github.com/evandrodevbr/GeminiNexus/commit/bc69a2e8b52e5019c45faae12771af7e40d3f26b))
|
||||
* **usage:** move Monitor to Usage tab with per-request token tracking ([99e458f](https://github.com/evandrodevbr/GeminiNexus/commit/99e458f69bd5564d27f94c72c719ba49efd72abe))
|
||||
* **usage:** redesign Usage Analytics with accurate token averages ([20785a8](https://github.com/evandrodevbr/GeminiNexus/commit/20785a8414bb0e364085a4a64f6a958c178d1df8))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **ci:** remove explicit pnpm version from workflows ([080bf30](https://github.com/evandrodevbr/GeminiNexus/commit/080bf30e38379db5cb3c8d124f76a39aef897ccc))
|
||||
* **ci:** resolve release pipeline conflicts and clean repo structure ([71bf812](https://github.com/evandrodevbr/GeminiNexus/commit/71bf8122d1a3d5bd74e3450f61d24747b50ed9d7))
|
||||
* **ci:** use Node 22 for semantic-release (requires ^22.14.0) ([268428a](https://github.com/evandrodevbr/GeminiNexus/commit/268428a1193fd0bfd0559472fd50b3293d9c8ccb))
|
||||
* install notice showing in Chinese and not recognizing MSI install path ([0753bbd](https://github.com/evandrodevbr/GeminiNexus/commit/0753bbdce63cffcd40fa13643e1082a67335c9c4))
|
||||
|
||||
### ♻️ Code Refactoring
|
||||
|
||||
* **accounts:** decompose CloudAccountList into focused components ([bf5a42c](https://github.com/evandrodevbr/GeminiNexus/commit/bf5a42c03e706d19033ceff332ee3609afded7ed))
|
||||
|
||||
## [0.14.0](https://github.com/evandrodevbr/GeminiNexus/compare/v0.13.1...v0.14.0) (2026-04-29)
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
* **proxy:** add OpenAI Responses API compatibility for Open CoDesign ([68474b5](https://github.com/evandrodevbr/GeminiNexus/commit/68474b56dca9c9bcd7b06eaae04c521f475e8e2c))
|
||||
|
||||
## [0.13.1](https://github.com/evandrodevbr/GeminiNexus/compare/v0.13.0...v0.13.1) (2026-04-29)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* lazy-initialize api-logger to prevent EACCES in CI environments ([9bf7c46](https://github.com/evandrodevbr/GeminiNexus/commit/9bf7c46ff6320f49cba80923c9e11eac35a7d4f0))
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
# Changelog
|
||||
@@ -308,4 +366,4 @@
|
||||
- Secure credential storage (AES-256-GCM)
|
||||
- IDE synchronization
|
||||
- Dark mode support
|
||||
- System tray integration
|
||||
- System tray integration
|
||||
@@ -1,49 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 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.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers 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, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [INSERT EMAIL ADDRESS]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
---
|
||||
|
||||
**Note**: Please replace `[INSERT EMAIL ADDRESS]` with your actual contact email if you wish to receive reports.
|
||||
# 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, caste, color, 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:**
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive feedback
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
- Helping newcomers feel welcome
|
||||
|
||||
**Examples of unacceptable behavior:**
|
||||
|
||||
- The use of sexualized language or imagery, and unwelcome sexual attention or advances
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers 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.
|
||||
|
||||
Project maintainers 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, or to ban temporarily or permanently any contributor for behaviors that they deem inappropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces — including the repository, issue tracker, discussions, and pull requests — and also applies when an individual is officially representing the project in public spaces.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue on the [GitHub repository](https://github.com/evandrodevbr/GeminiNexus/issues) or by contacting the project maintainers directly.
|
||||
|
||||
All complaints will be reviewed and investigated promptly and fairly. The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Project maintainers will follow these guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Impact**: Use of inappropriate language or other behavior deemed unprofessional.
|
||||
|
||||
**Consequence**: A private, written warning 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
|
||||
|
||||
**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 for a specified period of time. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**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.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**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](https://www.contributor-covenant.org), version 2.1, available at <https://www.contributor-covenant.org/version/2/1/code_of_conduct.html>.
|
||||
|
||||
Enforcement Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
@@ -1,185 +1,268 @@
|
||||
# Contributing to Gemini Nexus
|
||||
|
||||
First off, thank you for considering contributing to Gemini Nexus! 🎉
|
||||
|
||||
It's people like you that make Gemini Nexus such a great tool. We welcome contributions from everyone, whether it's a bug report, feature suggestion, documentation improvement, or code contribution.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Making Changes](#making-changes)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Style Guidelines](#style-guidelines)
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [Suggesting Features](#suggesting-features)
|
||||
|
||||
## 📜 Code of Conduct
|
||||
|
||||
This project and everyone participating in it is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers.
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
1. **Fork the repository** on GitHub
|
||||
2. **Clone your fork** locally:
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/GeminiNexusManager.git
|
||||
cd Gemini NexusManager
|
||||
```
|
||||
3. **Add the upstream remote**:
|
||||
```bash
|
||||
git remote add upstream https://github.com/Draculabo/AntigravityManager.git
|
||||
```
|
||||
|
||||
## 💻 Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js v18 or higher
|
||||
- npm (comes with Node.js)
|
||||
- Git
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development server
|
||||
npm start
|
||||
```
|
||||
|
||||
### Available Scripts
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------- | ----------------------------------- |
|
||||
| `npm start` | Start the app in development mode |
|
||||
| `npm run lint` | Run ESLint to check for code issues |
|
||||
| `npm run format:write` | Format code with Prettier |
|
||||
| `npm run test:unit` | Run unit tests with Vitest |
|
||||
| `npm run test:e2e` | Run E2E tests with Playwright |
|
||||
| `npm run test:all` | Run all tests |
|
||||
| `npm run type-check` | Run TypeScript type checking |
|
||||
| `npm run make` | Build production packages |
|
||||
|
||||
## ✏️ Making Changes
|
||||
|
||||
1. **Create a new branch** from `main`:
|
||||
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
# or
|
||||
git checkout -b fix/your-bug-fix
|
||||
```
|
||||
|
||||
2. **Make your changes** and commit them:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: add amazing new feature"
|
||||
```
|
||||
|
||||
3. **Keep your branch updated**:
|
||||
```bash
|
||||
git fetch upstream
|
||||
git rebase upstream/main
|
||||
```
|
||||
|
||||
### Commit Message Guidelines
|
||||
|
||||
We follow [Conventional Commits](https://www.conventionalcommits.org/). Each commit message should be structured as follows:
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
**Types:**
|
||||
|
||||
- `feat`: A new feature
|
||||
- `fix`: A bug fix
|
||||
- `docs`: Documentation only changes
|
||||
- `style`: Changes that don't affect code meaning (formatting, etc.)
|
||||
- `refactor`: Code change that neither fixes a bug nor adds a feature
|
||||
- `perf`: Performance improvement
|
||||
- `test`: Adding or correcting tests
|
||||
- `chore`: Changes to build process or auxiliary tools
|
||||
|
||||
**Examples:**
|
||||
|
||||
```
|
||||
feat(auth): add Google OAuth support
|
||||
fix(quota): resolve quota refresh timeout issue
|
||||
docs(readme): update installation instructions
|
||||
```
|
||||
|
||||
## 🔄 Pull Request Process
|
||||
|
||||
1. **Update documentation** if you're changing functionality
|
||||
2. **Add tests** for new features
|
||||
3. **Ensure all tests pass**: `npm run test:all`
|
||||
4. **Ensure code is formatted**: `npm run format:write`
|
||||
5. **Ensure no lint errors**: `npm run lint`
|
||||
6. **Push to your fork** and create a Pull Request
|
||||
|
||||
### PR Checklist
|
||||
|
||||
- [ ] My code follows the project's style guidelines
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
|
||||
## 🎨 Style Guidelines
|
||||
|
||||
### TypeScript/JavaScript
|
||||
|
||||
- Use TypeScript for all new code
|
||||
- Follow the existing code style (enforced by ESLint and Prettier)
|
||||
- Use meaningful variable and function names
|
||||
- Add JSDoc comments for public APIs
|
||||
|
||||
### React Components
|
||||
|
||||
- Use functional components with hooks
|
||||
- Keep components small and focused
|
||||
- Use TypeScript interfaces for props
|
||||
- Follow the existing file structure
|
||||
|
||||
### CSS/Styling
|
||||
|
||||
- Use TailwindCSS utility classes
|
||||
- Follow the design system in `components.json`
|
||||
- Ensure responsive design
|
||||
|
||||
## 🐛 Reporting Bugs
|
||||
|
||||
Found a bug? Please [create an issue](https://github.com/Draculabo/AntigravityManager/issues/new?template=bug_report.md) with:
|
||||
|
||||
- **Clear title** describing the issue
|
||||
- **Steps to reproduce** the behavior
|
||||
- **Expected behavior** vs actual behavior
|
||||
- **Screenshots** if applicable
|
||||
- **Environment details** (OS, app version)
|
||||
|
||||
## 💡 Suggesting Features
|
||||
|
||||
Have an idea? Please [create an issue](https://github.com/Draculabo/AntigravityManager/issues/new?template=feature_request.md) with:
|
||||
|
||||
- **Clear description** of the feature
|
||||
- **Use case** - why is this feature needed?
|
||||
- **Possible implementation** ideas (optional)
|
||||
|
||||
## 🙏 Thank You
|
||||
|
||||
Your contributions help make Gemini Nexus better for everyone. Thank you for taking the time to contribute!
|
||||
|
||||
---
|
||||
|
||||
If you have any questions, feel free to open an issue or reach out to the maintainers.
|
||||
# Contributing to Gemini Nexus
|
||||
|
||||
Thank you for considering contributing to Gemini Nexus! 🎉
|
||||
|
||||
Whether it's a bug report, feature suggestion, documentation improvement, or code contribution — all help is welcome.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Code of Conduct](#-code-of-conduct)
|
||||
- [Getting Started](#-getting-started)
|
||||
- [Development Setup](#-development-setup)
|
||||
- [Making Changes](#-making-changes)
|
||||
- [Commit Guidelines](#-commit-guidelines)
|
||||
- [Pull Request Process](#-pull-request-process)
|
||||
- [Code Style](#-code-style)
|
||||
- [Reporting Issues](#-reporting-issues)
|
||||
|
||||
---
|
||||
|
||||
## 📜 Code of Conduct
|
||||
|
||||
This project is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
1. **Fork** the repository on GitHub
|
||||
2. **Clone** your fork locally:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/GeminiNexus.git
|
||||
cd GeminiNexus
|
||||
```
|
||||
|
||||
3. **Add the upstream remote**:
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/evandrodevbr/GeminiNexus.git
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js** v20 or higher
|
||||
- **npm** (this project uses `package-lock.json`)
|
||||
- **Git**
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start the app in development mode
|
||||
npm start
|
||||
```
|
||||
|
||||
### Available Scripts
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------- | ------------------------------------------------------- |
|
||||
| `npm start` | Start the app in development mode (Electron + Vite HMR) |
|
||||
| `npm run lint` | Run ESLint |
|
||||
| `npm run format` | Check formatting with Prettier |
|
||||
| `npm run format:write` | Auto-format code with Prettier |
|
||||
| `npm run type-check` | Run TypeScript type check |
|
||||
| `npm test` | Run unit tests with Vitest |
|
||||
| `npm run test:e2e` | Run E2E tests with Playwright |
|
||||
| `npm run test:all` | Run all tests (unit + E2E) |
|
||||
| `npm run make` | Build production installers |
|
||||
|
||||
### Project Structure
|
||||
|
||||
```plaintext
|
||||
src/
|
||||
├── components/ # React UI components (Radix-based primitives in ui/)
|
||||
├── hooks/ # Custom React hooks
|
||||
├── ipc/ # Electron IPC + database handlers
|
||||
├── layouts/ # Layout components (MainLayout + sidebar)
|
||||
├── localization/ # i18n translation resources
|
||||
├── routes/ # TanStack Router pages (index, usage, proxy, settings)
|
||||
├── server/ # NestJS backend (proxy gateway service)
|
||||
├── services/ # Service layer
|
||||
├── types/ # TypeScript type definitions + Zod schemas
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✏️ Making Changes
|
||||
|
||||
1. **Create a branch** from `main`:
|
||||
|
||||
```bash
|
||||
git checkout -b feat/your-feature-name
|
||||
# or
|
||||
git checkout -b fix/your-bug-fix
|
||||
```
|
||||
|
||||
2. **Make your changes**, then commit:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: add amazing new feature"
|
||||
```
|
||||
|
||||
3. **Keep your branch up to date**:
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
git rebase upstream/main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Commit Guidelines
|
||||
|
||||
We follow [Conventional Commits](https://www.conventionalcommits.org/). Commits are analyzed by `semantic-release` to automatically determine version bumps and generate changelogs.
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
| Type | Description | Release |
|
||||
| ---------- | ----------------------------------- | ---------- |
|
||||
| `feat` | A new feature | **minor** |
|
||||
| `fix` | A bug fix | **patch** |
|
||||
| `perf` | Performance improvement | **patch** |
|
||||
| `refactor` | Code change (no bug fix or feature) | **patch** |
|
||||
| `docs` | Documentation only | no release |
|
||||
| `style` | Formatting, whitespace | no release |
|
||||
| `test` | Adding or fixing tests | no release |
|
||||
| `chore` | Build process, tooling | no release |
|
||||
| `ci` | CI/CD changes | no release |
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
Add `BREAKING CHANGE:` in the footer or `!` after the type to trigger a **major** release:
|
||||
|
||||
```
|
||||
feat!: remove legacy plaintext storage
|
||||
|
||||
BREAKING CHANGE: all accounts must re-authenticate after upgrade.
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
feat(proxy): add Anthropic /v1/messages endpoint
|
||||
fix(usage): handle SQL SUM(null) in empty date ranges
|
||||
docs(readme): add Chinese and Spanish translations
|
||||
ci: normalize Node.js to v20 across all workflows
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Pull Request Process
|
||||
|
||||
### Before Opening a PR
|
||||
|
||||
1. Run the quality checks:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run format:write
|
||||
npm run type-check
|
||||
npm test
|
||||
```
|
||||
|
||||
2. Update documentation if you changed functionality
|
||||
3. Add tests for new features
|
||||
|
||||
### PR Checklist
|
||||
|
||||
- [ ] My code follows the project's style guidelines
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have commented hard-to-understand areas
|
||||
- [ ] I have updated related documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix or feature works
|
||||
- [ ] All existing tests pass locally
|
||||
|
||||
### Review Process
|
||||
|
||||
- PRs require at least one maintainer approval
|
||||
- CI must pass (lint, type-check, unit tests)
|
||||
- Squash-merge is preferred for clean history
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Code Style
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Use TypeScript for all new code — avoid `any`
|
||||
- Use Zod for runtime validation
|
||||
- Prefer `lodash-es` named imports over native utilities
|
||||
- Use `@/` alias for `src/` imports
|
||||
|
||||
### React Components
|
||||
|
||||
- Functional components with hooks only
|
||||
- Use Radix UI primitives for accessible components
|
||||
- TypeScript interfaces for props
|
||||
- Follow existing component structure (`components/ComponentName.tsx`)
|
||||
|
||||
### Styling
|
||||
|
||||
- Tailwind CSS v4 utility classes
|
||||
- Use `clsx` + `tailwind-merge` for conditional classes
|
||||
- Follow the Gemini Nexus design tokens (`rounded-xl`, `border-white/[0.06]`)
|
||||
|
||||
### Import Order
|
||||
|
||||
```typescript
|
||||
// 1. React and core libraries
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
// 2. External dependencies (alphabetical)
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// 3. Internal imports (using @ alias)
|
||||
import { Card } from '@/components/ui/card';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Reporting Issues
|
||||
|
||||
### Bug Reports
|
||||
|
||||
[Create a bug report](https://github.com/evandrodevbr/GeminiNexus/issues/new?labels=bug&template=bug_report.md) with:
|
||||
|
||||
- Clear title describing the issue
|
||||
- Steps to reproduce the behavior
|
||||
- Expected vs actual behavior
|
||||
- Screenshots if applicable
|
||||
- Environment details (OS, app version, Node.js version)
|
||||
|
||||
### Feature Requests
|
||||
|
||||
[Suggest a feature](https://github.com/evandrodevbr/GeminiNexus/issues/new?labels=enhancement&template=feature_request.md) with:
|
||||
|
||||
- Clear description of the feature
|
||||
- Use case — why is this needed?
|
||||
- Possible implementation ideas (optional)
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Thank You
|
||||
|
||||
Your contributions make Gemini Nexus better for everyone. Thank you for your time!
|
||||
|
||||
If you have questions, feel free to [open a discussion](https://github.com/evandrodevbr/GeminiNexus/discussions) or reach out to the maintainers.
|
||||
@@ -0,0 +1,252 @@
|
||||
<p align="center">
|
||||
<img src="docs/assets/logo.png" alt="Gemini Nexus" width="128" height="128" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Gemini Nexus</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>🚀 Gateway de IA multi-cuenta profesional para Google Gemini y Claude</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.pt-BR.md">Português</a> | <a href="README.zh-CN.md">中文</a> | Español
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/v/release/evandrodevbr/GeminiNexus?style=flat-square" alt="Versión" />
|
||||
</a>
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/downloads/evandrodevbr/GeminiNexus/total?style=flat-square&color=blue" alt="Descargas" />
|
||||
</a>
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/evandrodevbr/GeminiNexus?style=flat-square" alt="Licencia" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/plataforma-Windows%20%7C%20macOS%20%7C%20Linux-informational?style=flat-square" alt="Plataforma" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ✨ ¿Por qué Gemini Nexus?
|
||||
|
||||
Al usar IDEs y herramientas de programación con IA, ¿te has encontrado con estos problemas?
|
||||
|
||||
- 😫 La cuota de una sola cuenta se agota rápido, requiriendo cambios manuales frecuentes
|
||||
- 🔄 Gestionar múltiples cuentas de Google/Claude es tedioso
|
||||
- 📊 No sabes cuántos tokens has consumido o cuánta cuota queda
|
||||
- 🔌 Necesitas un proxy local confiable que hable los protocolos OpenAI/Anthropic
|
||||
|
||||
**Gemini Nexus** resuelve todo esto. Es una aplicación de escritorio profesional en Electron que actúa como gateway inteligente entre tus herramientas de desarrollo y Google Gemini / Claude AI:
|
||||
|
||||
- ✅ **Pool de Cuentas Ilimitado** — Agrega cualquier cantidad de cuentas Google Gemini y Claude
|
||||
- ✅ **Cambio Automático Inteligente** — Rota automáticamente a la siguiente cuenta disponible cuando la cuota es baja
|
||||
- ✅ **Analytics de Uso en Tiempo Real** — Dashboard nivel SaaS con gráficos de área, indicadores de tendencia y distribución por modelo
|
||||
- ✅ **Observabilidad Completa del Proxy** — Monitor de tráfico en vivo, replay de solicitudes e inspector de capacidades de modelos
|
||||
- ✅ **Compatible con OpenAI y Anthropic** — Proxy drop-in para Cursor, Windsurf, OpenCode y cualquier herramienta compatible
|
||||
- ✅ **Seguro por Defecto** — Cifrado AES-256-GCM con gestión nativa de credenciales del SO
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Características
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>☁️ Pool de Cuentas en la Nube</h3>
|
||||
<ul>
|
||||
<li>Agrega cuentas ilimitadas de Google Gemini / Claude vía OAuth</li>
|
||||
<li>Muestra avatar, email, estado y último uso</li>
|
||||
<li>Monitoreo de estado en tiempo real (Activa, Rate Limited, Expirada)</li>
|
||||
<li>Configuración de proxy URL por cuenta</li>
|
||||
<li>Gestión de perfil de identidad del dispositivo con historial</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>📊 Dashboard de Analytics de Uso</h3>
|
||||
<ul>
|
||||
<li>Consumo de tokens en tiempo real con auto-refresh cada 15s</li>
|
||||
<li>Gráficos de área para tokens de prompt y completion (diario/horario)</li>
|
||||
<li>Indicadores de tendencia (% de cambio vs período anterior)</li>
|
||||
<li>Sparklines inline en las tarjetas de estadísticas</li>
|
||||
<li>Ranking de distribución por modelo con gráficos de barras horizontales</li>
|
||||
<li>Visualización de proporción Prompt/Completion</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>🔌 Proxy de API Local (Gateway)</h3>
|
||||
<ul>
|
||||
<li>Endpoint compatible con OpenAI <code>/v1/chat/completions</code></li>
|
||||
<li>Endpoint compatible con Anthropic <code>/v1/messages</code></li>
|
||||
<li>Soporte completo de streaming SSE</li>
|
||||
<li>Mapeo de modelos (ej: <code>claude-sonnet-4-6</code> → <code>gemini-3-flash</code>)</li>
|
||||
<li>Puerto, timeout y API key configurables</li>
|
||||
<li>Control de visibilidad de modelos</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🔄 Cambio Automático Inteligente</h3>
|
||||
<ul>
|
||||
<li>Modo pool ilimitado con selección inteligente de respaldo</li>
|
||||
<li>Cambio automático cuando cuota < 5% o con rate limit</li>
|
||||
<li>Monitoreo en segundo plano cada 5 minutos</li>
|
||||
<li>Fallback elegante con seguimiento de razón de estado</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>🔍 Observabilidad del Proxy (Avanzado)</h3>
|
||||
<ul>
|
||||
<li><strong>Monitor de Tráfico</strong> — Log en vivo de solicitudes/respuestas</li>
|
||||
<li><strong>Replay de Solicitudes</strong> — Reproduce cualquier solicitud para debugging</li>
|
||||
<li><strong>Capacidades de Modelos</strong> — Inspecciona visión, thinking, streaming, modo JSON</li>
|
||||
<li><strong>Herramientas para Devs</strong> — Generación de código cURL y Python</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🔐 Seguridad y Cifrado</h3>
|
||||
<ul>
|
||||
<li>Cifrado AES-256-GCM para todos los datos sensibles</li>
|
||||
<li>Integración con gestor de credenciales nativo del SO</li>
|
||||
<li>Migración automática de datos legacy en texto plano</li>
|
||||
<li>Almacenamiento cifrado de tokens y cuotas por cuenta</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>⚙️ Configuración y Personalización</h3>
|
||||
<ul>
|
||||
<li>Tema Oscuro / Claro / Sistema</li>
|
||||
<li>Multi-idioma: English & Português (Brasil)</li>
|
||||
<li>Toggles de visibilidad de modelos</li>
|
||||
<li>Acceso al directorio de logs</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🖥️ Experiencia de Escritorio</h3>
|
||||
<ul>
|
||||
<li>App nativa Electron con integración en bandeja del sistema</li>
|
||||
<li>Sidebar colapsable con estado persistente</li>
|
||||
<li>Layouts responsivos</li>
|
||||
<li>Barra de estado con indicadores en vivo</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## 📸 Capturas de Pantalla
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/main.png" alt="Main" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/2usage.png" alt="Usage" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/3proxy.png" alt="Proxy" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/4routing.png" alt="Routing" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/5docs.png" alt="Docs" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/6connections.png" alt="Connections" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/7config.png" alt="Config" width="80%" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Inicio Rápido
|
||||
|
||||
### Requisitos Previos
|
||||
|
||||
- **Node.js** v20 o superior
|
||||
- **npm**
|
||||
|
||||
### Compilar desde el Código Fuente
|
||||
|
||||
```bash
|
||||
git clone https://github.com/evandrodevbr/GeminiNexus.git
|
||||
cd GeminiNexus
|
||||
npm install
|
||||
npm start
|
||||
|
||||
# Compilar para producción
|
||||
npm run make
|
||||
```
|
||||
|
||||
### Uso con IDEs de IA
|
||||
|
||||
```plaintext
|
||||
API Base URL: http://localhost:10100/v1
|
||||
API Key: (copiar de la página Proxy en la app)
|
||||
Model: gemini-3-flash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Stack Tecnológico
|
||||
|
||||
| Categoría | Tecnologías |
|
||||
| ----------------- | ---------------------------------------------- |
|
||||
| **Core** | Electron, React 19, TypeScript |
|
||||
| **Build** | Vite, Electron Forge |
|
||||
| **Estilos** | Tailwind CSS v4, Radix UI, Lucide Icons |
|
||||
| **Estado** | TanStack Query, TanStack Router |
|
||||
| **Backend** | NestJS (gateway interno), ORPC (RPC type-safe) |
|
||||
| **Base de Datos** | Better-SQLite3, Drizzle ORM |
|
||||
| **Gráficos** | Nivo (Line, Bar, Pie) |
|
||||
| **Testing** | Vitest, Playwright |
|
||||
|
||||
---
|
||||
|
||||
## ❓ Preguntas Frecuentes
|
||||
|
||||
<details>
|
||||
<summary><b>P: ¿La app no inicia?</b></summary>
|
||||
|
||||
1. Asegúrate de que las dependencias están instaladas: `npm install`
|
||||
2. Node.js versión >= 20
|
||||
3. Intenta eliminar `node_modules` y reinstalar
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>P: ¿El IDE no conecta al proxy?</b></summary>
|
||||
|
||||
1. Asegúrate de que el proxy está corriendo (indicador verde en la barra de estado)
|
||||
2. Confirma que el puerto coincide (por defecto `10100`)
|
||||
3. Copia la API key de la página Proxy
|
||||
4. Asegúrate de tener al menos una cuenta activa
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 📄 Licencia
|
||||
|
||||
[CC BY-NC-SA 4.0](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Aviso Legal
|
||||
|
||||
> [!WARNING]
|
||||
> **Solo con Fines Educativos**
|
||||
>
|
||||
> Este proyecto está destinado exclusivamente a fines educativos y de investigación. Se proporciona "tal cual" sin ninguna garantía. **El uso comercial está estrictamente prohibido.**
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Créditos
|
||||
|
||||
**Gemini Nexus** es una versión significativamente mejorada y expandida del proyecto original [AntigravityManager](https://github.com/Draculabo/AntigravityManager) creado por [Draculabo](https://github.com/Draculabo). Toda la arquitectura fundamental y diseño fueron realizados por el autor original.
|
||||
@@ -5,29 +5,25 @@
|
||||
<h1 align="center">Gemini Nexus</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>🚀 Professional multi-account manager for Google Gemini & Claude AI</strong>
|
||||
<strong>🚀 Professional multi-account AI gateway for Google Gemini & Claude</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
English | <a href="README.pt-BR.md">Português (Brasil)</a>
|
||||
English | <a href="README.pt-BR.md">Português</a> | <a href="README.zh-CN.md">中文</a> | <a href="README.es.md">Español</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Draculabo/GeminiNexus/actions/workflows/testing.yaml">
|
||||
<img src="https://github.com/Draculabo/GeminiNexus/actions/workflows/testing.yaml/badge.svg" alt="Tests" />
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/v/release/evandrodevbr/GeminiNexus?style=flat-square" alt="Release" />
|
||||
</a>
|
||||
<a href="https://github.com/Draculabo/GeminiNexus/actions/workflows/lint.yaml">
|
||||
<img src="https://github.com/Draculabo/GeminiNexus/actions/workflows/lint.yaml/badge.svg" alt="Lint" />
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/downloads/evandrodevbr/GeminiNexus/total?style=flat-square&color=blue" alt="Downloads" />
|
||||
</a>
|
||||
<a href="https://github.com/Draculabo/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/v/release/Draculabo/GeminiNexus?style=flat-square" alt="Release" />
|
||||
</a>
|
||||
<a href="https://github.com/Draculabo/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/downloads/Draculabo/GeminiNexus/total?style=flat-square&color=blue" alt="Downloads" />
|
||||
</a>
|
||||
<a href="https://github.com/Draculabo/GeminiNexus/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Draculabo/GeminiNexus?style=flat-square" alt="License" />
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/evandrodevbr/GeminiNexus?style=flat-square" alt="License" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-informational?style=flat-square" alt="Platform" />
|
||||
<img src="https://img.shields.io/badge/electron-latest-47848F?style=flat-square&logo=electron&logoColor=white" alt="Electron" />
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -35,13 +31,11 @@
|
||||
## 📖 Table of Contents
|
||||
|
||||
- [Why Gemini Nexus?](#-why-gemini-nexus)
|
||||
- [Improvements over Base Project](#-improvements-over-base-project)
|
||||
- [Features](#-features)
|
||||
- [Screenshots](#-screenshots)
|
||||
- [Quick Start](#-quick-start)
|
||||
- [Tech Stack](#️-tech-stack)
|
||||
- [Development](#-development)
|
||||
- [Debugging Docs](#-debugging-docs)
|
||||
- [FAQ](#-faq)
|
||||
- [Contributing](#-contributing)
|
||||
- [License](#-license)
|
||||
@@ -50,34 +44,22 @@
|
||||
|
||||
## ✨ Why Gemini Nexus?
|
||||
|
||||
When using your favorite IDEs, have you ever encountered these problems?
|
||||
When using AI-powered IDEs and coding tools, have you ever run into these problems?
|
||||
|
||||
- 😫 Single account quota runs out quickly, requiring frequent manual switching
|
||||
- 🔄 Managing multiple Google/Claude accounts is cumbersome
|
||||
- 📊 Don't know how much quota is left on the current account
|
||||
- ⏰ Worried about missing quota reset times
|
||||
- 🔌 Need a reliable local API proxy for development tools
|
||||
- 📊 Don't know how much quota or tokens you've consumed
|
||||
- 🔌 Need a reliable local API proxy that speaks OpenAI/Anthropic protocols
|
||||
- 🔍 Can't see what's actually being sent/received by the proxy
|
||||
|
||||
**Gemini Nexus** is here to solve these problems! It's a professional Electron desktop app that helps you:
|
||||
**Gemini Nexus** solves all of these. It's a professional Electron desktop application that acts as an intelligent gateway between your development tools and Google Gemini / Claude AI:
|
||||
|
||||
- ✅ **Unlimited Account Pool** - Add any number of Google Gemini / Claude accounts
|
||||
- ✅ **Smart Auto-Switching** - Automatically switch to the next available account when quota is low or rate-limited
|
||||
- ✅ **Real-time Monitoring** - Visualize quota usage for all accounts
|
||||
- ✅ **Local API Proxy** - Built-in OpenAI/Anthropic compatible proxy server
|
||||
- ✅ **Secure Encryption** - AES-256-GCM encryption for sensitive data
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Improvements over Base Project
|
||||
|
||||
This project contains significant development improvements, bug fixes, and configuration enhancements compared to the original base project.
|
||||
|
||||
Key improvements include:
|
||||
- **Enhanced API Management UI:** Implemented dynamic model selectors and "Copy API Name" features to streamline integration with other tools.
|
||||
- **Robust SSE Streaming:** Fixed critical Server-Sent Events (SSE) streaming bugs in the `/v1/chat/completions` proxy endpoint to perfectly support IDEs like Cursor, OpenCode, and Windsurf.
|
||||
- **Rebranding and Localization:** Complete rebranding to Gemini Nexus with improved Portuguese (Brazil) localization and English defaults.
|
||||
- **Agentic Capabilities:** Integration of advanced AI developer agents and skill frameworks (GSD, AutoResearch, Superpowers) directly into the project ecosystem for rapid autonomous development.
|
||||
- **Build & Dev Env:** Better cross-platform build configurations, updated schemas, and automated toolchain processes.
|
||||
- ✅ **Unlimited Account Pool** — Add any number of Google Gemini & Claude accounts
|
||||
- ✅ **Smart Auto-Switching** — Automatically rotates to the next available account when quota is low or rate-limited
|
||||
- ✅ **Real-time Usage Analytics** — SaaS-grade dashboard with area charts, trend indicators, and model distribution
|
||||
- ✅ **Full Proxy Observability** — Live traffic monitor, request replay, and model capabilities inspector
|
||||
- ✅ **OpenAI & Anthropic Compatible** — Drop-in replacement proxy for Cursor, Windsurf, OpenCode, and any OpenAI-compatible tool
|
||||
- ✅ **Secure by Default** — AES-256-GCM encryption with OS-native credential management
|
||||
|
||||
---
|
||||
|
||||
@@ -91,50 +73,83 @@ Key improvements include:
|
||||
<li>Add unlimited Google Gemini / Claude accounts via OAuth</li>
|
||||
<li>Display avatar, email, status, and last used time</li>
|
||||
<li>Real-time status monitoring (Active, Rate Limited, Expired)</li>
|
||||
<li>Per-account proxy URL configuration</li>
|
||||
<li>Device identity profile management with history</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>📊 Real-time Quota Monitoring</h3>
|
||||
<h3>📊 Usage Analytics Dashboard</h3>
|
||||
<ul>
|
||||
<li>Multi-model support: gemini-pro, claude-3-5-sonnet, etc.</li>
|
||||
<li>Visual progress bars with color indicators</li>
|
||||
<li>Auto & manual refresh capabilities</li>
|
||||
<li>Real-time token consumption with 15s auto-refresh</li>
|
||||
<li>Area charts for daily/hourly prompt & completion tokens</li>
|
||||
<li>Trend indicators (% change vs previous period)</li>
|
||||
<li>Inline sparklines on stat cards</li>
|
||||
<li>Model distribution ranking with horizontal bar charts</li>
|
||||
<li>Prompt/Completion ratio visualization</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>🔌 Local API Proxy (Gateway)</h3>
|
||||
<ul>
|
||||
<li>OpenAI <code>/v1/chat/completions</code> compatible endpoint</li>
|
||||
<li>Anthropic <code>/v1/messages</code> compatible endpoint</li>
|
||||
<li>Full SSE streaming support (tested with Cursor, Windsurf, OpenCode)</li>
|
||||
<li>Model mapping (e.g. <code>claude-sonnet-4-6</code> → <code>gemini-3-flash</code>)</li>
|
||||
<li>Configurable port, timeout, and API key</li>
|
||||
<li>Model visibility control (show/hide specific models)</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🔄 Intelligent Auto-Switching</h3>
|
||||
<ul>
|
||||
<li>Unlimited pool mode with smart backup selection</li>
|
||||
<li>Auto-switch when quota < 5% or rate-limited</li>
|
||||
<li>Background monitoring every 5 minutes</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🔐 Security First</h3>
|
||||
<ul>
|
||||
<li>AES-256-GCM encryption for sensitive data</li>
|
||||
<li>OS native credential manager integration</li>
|
||||
<li>Auto migration of legacy plaintext data</li>
|
||||
<li>Graceful fallback with status reason tracking</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>🔌 Local API Proxy</h3>
|
||||
<h3>🔍 Proxy Observability (Advanced)</h3>
|
||||
<ul>
|
||||
<li>OpenAI & Anthropic API compatible</li>
|
||||
<li>Configurable port and request timeout</li>
|
||||
<li>Model mapping (e.g. Claude → Gemini)</li>
|
||||
<li><strong>Traffic Monitor</strong> — Live request/response log with latency, status, and model info</li>
|
||||
<li><strong>Request Replay</strong> — Replay any recorded request for debugging</li>
|
||||
<li><strong>Model Capabilities</strong> — Inspect vision, thinking, streaming, JSON mode support per model</li>
|
||||
<li><strong>Developer Tools</strong> — cURL & Python code generation, one-click copy</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🛠️ Developer Tools</h3>
|
||||
<h3>🔐 Security & Encryption</h3>
|
||||
<ul>
|
||||
<li>Built-in cURL & Python code generation</li>
|
||||
<li>Visual service status monitoring</li>
|
||||
<li>One-click API Key regeneration</li>
|
||||
<li>AES-256-GCM encryption for all sensitive data</li>
|
||||
<li>OS native credential manager integration (Keytar + SafeStorage)</li>
|
||||
<li>Auto migration of legacy plaintext data</li>
|
||||
<li>Encrypted token & quota storage per account</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>⚙️ Settings & Customization</h3>
|
||||
<ul>
|
||||
<li>Dark / Light / System theme support</li>
|
||||
<li>Multi-language: English & Português (Brasil)</li>
|
||||
<li>Per-account proxy URL override</li>
|
||||
<li>Model visibility toggles</li>
|
||||
<li>Log directory access</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🖥️ Desktop Experience</h3>
|
||||
<ul>
|
||||
<li>Native Electron app with system tray integration</li>
|
||||
<li>Collapsible sidebar with persistent state</li>
|
||||
<li>Responsive layouts across all views</li>
|
||||
<li>Status bar with live proxy and connection indicators</li>
|
||||
<li>Error boundaries with user-friendly fallbacks</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -144,26 +159,43 @@ Key improvements include:
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/screenshot-main.png" alt="Main Interface" width="80%" />
|
||||
<img src="docs/assets/main.png" alt="Main" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/2usage.png" alt="Usage" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/3proxy.png" alt="Proxy" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/4routing.png" alt="Routing" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/5docs.png" alt="Docs" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/6connections.png" alt="Connections" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/7config.png" alt="Config" width="80%" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js** v20 or higher
|
||||
- **npm** (this project uses `package-lock.json`)
|
||||
|
||||
### Build from Source
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- Node.js v18 or higher
|
||||
- npm or pnpm
|
||||
|
||||
#### Steps
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/Draculabo/GeminiNexus.git
|
||||
git clone https://github.com/evandrodevbr/GeminiNexus.git
|
||||
cd GeminiNexus
|
||||
|
||||
# Install dependencies
|
||||
@@ -172,21 +204,77 @@ npm install
|
||||
# Start development
|
||||
npm start
|
||||
|
||||
# Build for production
|
||||
# Build for production (Windows installer)
|
||||
npm run make
|
||||
```
|
||||
|
||||
### Using with AI IDEs
|
||||
|
||||
Once the proxy is running, configure your IDE:
|
||||
|
||||
```plaintext
|
||||
# In Cursor / Windsurf / OpenCode settings:
|
||||
API Base URL: http://localhost:10100/v1
|
||||
API Key: (copy from the Proxy page in the app)
|
||||
Model: gemini-3-flash (or any mapped model)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
| Category | Technologies |
|
||||
|----------|-------------|
|
||||
| **Core** | [Electron](https://www.electronjs.org/), [React](https://react.dev/), [TypeScript](https://www.typescriptlang.org/) |
|
||||
| **Build Tool** | [Vite](https://vitejs.dev/) |
|
||||
| **Styling** | [TailwindCSS](https://tailwindcss.com/), [Shadcn UI](https://ui.shadcn.com/) |
|
||||
| **State** | [TanStack Query](https://tanstack.com/query/latest) |
|
||||
| **Database** | [Better-SQLite3](https://github.com/WiseLibs/better-sqlite3) |
|
||||
| **Core** | [Electron](https://www.electronjs.org/), [React 19](https://react.dev/), [TypeScript](https://www.typescriptlang.org/) |
|
||||
| **Build** | [Vite](https://vitejs.dev/), [Electron Forge](https://www.electronforge.io/) |
|
||||
| **Styling** | [Tailwind CSS v4](https://tailwindcss.com/), [Radix UI](https://www.radix-ui.com/), [Lucide Icons](https://lucide.dev/) |
|
||||
| **State** | [TanStack Query](https://tanstack.com/query/latest), [TanStack Router](https://tanstack.com/router/latest) |
|
||||
| **Backend** | [NestJS](https://nestjs.com/) (internal gateway), [ORPC](https://orpc.unnoq.com/) (type-safe RPC) |
|
||||
| **Database** | [Better-SQLite3](https://github.com/WiseLibs/better-sqlite3), [Drizzle ORM](https://orm.drizzle.team/) |
|
||||
| **Charts** | [Nivo](https://nivo.rocks/) (Line, Bar, Pie) |
|
||||
| **i18n** | [react-i18next](https://react.i18next.com/) |
|
||||
| **Testing** | [Vitest](https://vitest.dev/), [Playwright](https://playwright.dev/) |
|
||||
|
||||
---
|
||||
|
||||
## 💻 Development
|
||||
|
||||
```bash
|
||||
# Start dev environment (Electron + Vite HMR)
|
||||
npm start
|
||||
|
||||
# Run linting
|
||||
npm run lint
|
||||
|
||||
# Type check
|
||||
npm run type-check
|
||||
|
||||
# Format code
|
||||
npm run format:write
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```plaintext
|
||||
src/
|
||||
├── actions/ # App actions and flow orchestration
|
||||
├── components/ # React UI components
|
||||
│ ├── proxy/ # Proxy page components (advanced tabs)
|
||||
│ ├── usage/ # Usage analytics components (StatCard, ChartCard)
|
||||
│ └── ui/ # Base UI primitives (Radix-based)
|
||||
├── hooks/ # Custom React hooks
|
||||
├── ipc/ # Electron IPC + database handlers
|
||||
├── layouts/ # Layout components (MainLayout + sidebar)
|
||||
├── localization/ # i18n translation resources
|
||||
├── routes/ # TanStack Router pages (index, usage, proxy, settings)
|
||||
├── server/ # NestJS backend (proxy gateway service)
|
||||
├── services/ # Service layer
|
||||
├── types/ # TypeScript type definitions + Zod schemas
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -195,10 +283,10 @@ npm run make
|
||||
<details>
|
||||
<summary><b>Q: The app won't start?</b></summary>
|
||||
|
||||
Please check:
|
||||
1. Make sure all dependencies are installed: `npm install`
|
||||
2. Check if Node.js version is >= 18
|
||||
2. Check if Node.js version is >= 20
|
||||
3. Try deleting `node_modules` and reinstalling
|
||||
4. On Windows, ensure the WiX Toolset is available for `npm run make`
|
||||
|
||||
</details>
|
||||
|
||||
@@ -211,6 +299,25 @@ Please check:
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Q: IDE can't connect to the proxy?</b></summary>
|
||||
|
||||
1. Make sure the proxy is running (green indicator in the status bar)
|
||||
2. Verify the port matches your IDE config (default: `10100`)
|
||||
3. Copy the API key from the Proxy page and paste it in your IDE settings
|
||||
4. Check that at least one account is active in the Accounts page
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Q: Token counts show zero?</b></summary>
|
||||
|
||||
1. This was a known bug in earlier versions — update to the latest version
|
||||
2. Token tracking now correctly handles streaming metadata and estimated counts
|
||||
3. Usage data refreshes every 15 seconds on the Usage Analytics page
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -5,57 +5,61 @@
|
||||
<h1 align="center">Gemini Nexus</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>🚀 Gerenciador multi-contas profissional para Google Gemini & Claude AI</strong>
|
||||
<strong>🚀 Gateway de IA multi-contas profissional para Google Gemini & Claude</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | Português (Brasil)
|
||||
<a href="README.md">English</a> | Português | <a href="README.zh-CN.md">中文</a> | <a href="README.es.md">Español</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Draculabo/GeminiNexus/actions/workflows/testing.yaml">
|
||||
<img src="https://github.com/Draculabo/GeminiNexus/actions/workflows/testing.yaml/badge.svg" alt="Tests" />
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/v/release/evandrodevbr/GeminiNexus?style=flat-square" alt="Release" />
|
||||
</a>
|
||||
<a href="https://github.com/Draculabo/GeminiNexus/actions/workflows/lint.yaml">
|
||||
<img src="https://github.com/Draculabo/GeminiNexus/actions/workflows/lint.yaml/badge.svg" alt="Lint" />
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/downloads/evandrodevbr/GeminiNexus/total?style=flat-square&color=blue" alt="Downloads" />
|
||||
</a>
|
||||
<a href="https://github.com/Draculabo/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/v/release/Draculabo/GeminiNexus?style=flat-square" alt="Release" />
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/evandrodevbr/GeminiNexus?style=flat-square" alt="Licença" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/plataforma-Windows%20%7C%20macOS%20%7C%20Linux-informational?style=flat-square" alt="Plataforma" />
|
||||
<img src="https://img.shields.io/badge/electron-latest-47848F?style=flat-square&logo=electron&logoColor=white" alt="Electron" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Sumário
|
||||
|
||||
- [Por que o Gemini Nexus?](#-por-que-o-gemini-nexus)
|
||||
- [Funcionalidades](#-funcionalidades)
|
||||
- [Screenshots](#-screenshots)
|
||||
- [Início Rápido](#-início-rápido)
|
||||
- [Stack Tecnológica](#️-stack-tecnológica)
|
||||
- [Desenvolvimento](#-desenvolvimento)
|
||||
- [FAQ](#-faq)
|
||||
- [Contribuindo](#-contribuindo)
|
||||
- [Licença](#-licença)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Por que o Gemini Nexus?
|
||||
|
||||
Quando você usa suas IDEs favoritas, já encontrou estes problemas?
|
||||
Ao usar IDEs e ferramentas de programação com IA, você já enfrentou estes problemas?
|
||||
|
||||
- 😫 A cota da sua única conta acaba rápido, exigindo trocas manuais frequentes
|
||||
- 😫 A cota da conta única acaba rápido, exigindo trocas manuais frequentes
|
||||
- 🔄 Gerenciar múltiplas contas do Google/Claude é cansativo
|
||||
- 📊 Você não sabe quanta cota resta na conta atual
|
||||
- 🔌 Precisa de um proxy local e confiável para suas ferramentas de desenvolvimento
|
||||
- 📊 Não sabe quantos tokens já consumiu ou quanta cota resta
|
||||
- 🔌 Precisa de um proxy local confiável que fale os protocolos OpenAI/Anthropic
|
||||
- 🔍 Não consegue ver o que está sendo enviado/recebido pelo proxy
|
||||
|
||||
**Gemini Nexus** está aqui para resolver tudo isso! Ele é um app desktop profissional construído em Electron que te ajuda a:
|
||||
**Gemini Nexus** resolve tudo isso. É um aplicativo desktop profissional em Electron que funciona como um gateway inteligente entre suas ferramentas de desenvolvimento e o Google Gemini / Claude AI:
|
||||
|
||||
- ✅ **Contas Ilimitadas** - Adicione quantas contas Google Gemini / Claude quiser
|
||||
- ✅ **Troca Automática** - Troca inteligentemente para a próxima conta disponível quando o limite é atingido
|
||||
- ✅ **Monitoramento em Tempo Real** - Visualize o uso de cota de todas as contas
|
||||
- ✅ **Proxy de API Local** - Servidor proxy integrado compatível com OpenAI/Anthropic
|
||||
- ✅ **Criptografia Segura** - Criptografia AES-256-GCM para seus dados sensíveis
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Melhorias sobre o Projeto Base
|
||||
|
||||
Este projeto contém melhorias significativas de desenvolvimento, correção de erros e melhorias de configuração em relação ao projeto original base.
|
||||
|
||||
Principais melhorias incluem:
|
||||
|
||||
- **Interface de Gerenciamento de API Aprimorada:** Seletores dinâmicos de modelos e recursos de "Copiar Nome da API" implementados para facilitar a integração com outras ferramentas.
|
||||
- **Streaming Robusto (SSE):** Correção de bugs críticos no streaming via Server-Sent Events (SSE) no endpoint proxy `/v1/chat/completions` para suportar perfeitamente IDEs como Cursor, OpenCode e Windsurf.
|
||||
- **Rebranding e Localização:** Rebranding completo para Gemini Nexus com traduções melhoradas em Português (Brasil) e padrões em Inglês.
|
||||
- **Capacidades de Agentes:** Integração direta de frameworks avançados de agentes desenvolvedores de IA e skills (GSD, AutoResearch, Superpowers) no ecossistema do projeto para desenvolvimento autônomo rápido.
|
||||
- **Ambiente de Build e Dev:** Melhores configurações de build cross-platform, schemas atualizados e processos de toolchain automatizados.
|
||||
- ✅ **Pool de Contas Ilimitado** — Adicione quantas contas Google Gemini & Claude quiser
|
||||
- ✅ **Troca Automática Inteligente** — Rotaciona automaticamente para a próxima conta disponível quando a cota está baixa ou com rate limit
|
||||
- ✅ **Analytics de Uso em Tempo Real** — Dashboard nível SaaS com gráficos de área, indicadores de tendência e distribuição por modelo
|
||||
- ✅ **Observabilidade Completa do Proxy** — Monitor de tráfego ao vivo, replay de requisições e inspetor de capacidades dos modelos
|
||||
- ✅ **Compatível com OpenAI & Anthropic** — Proxy drop-in para Cursor, Windsurf, OpenCode e qualquer ferramenta compatível com OpenAI
|
||||
- ✅ **Seguro por Padrão** — Criptografia AES-256-GCM com gerenciamento nativo de credenciais do SO
|
||||
|
||||
---
|
||||
|
||||
@@ -66,31 +70,86 @@ Principais melhorias incluem:
|
||||
<td width="50%">
|
||||
<h3>☁️ Pool de Contas na Nuvem</h3>
|
||||
<ul>
|
||||
<li>Adicione contas via OAuth do Google e Anthropic</li>
|
||||
<li>Mostra avatar, e-mail, status e último horário de uso</li>
|
||||
<li>Adicione contas ilimitadas do Google Gemini / Claude via OAuth</li>
|
||||
<li>Exibe avatar, e-mail, status e último uso</li>
|
||||
<li>Monitoramento de status em tempo real (Ativa, Rate Limited, Expirada)</li>
|
||||
<li>Configuração de proxy URL por conta</li>
|
||||
<li>Gerenciamento de perfil de identidade do dispositivo com histórico</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>📊 Monitoramento de Cota</h3>
|
||||
<h3>📊 Dashboard de Analytics de Uso</h3>
|
||||
<ul>
|
||||
<li>Suporte multi-modelo: gemini-pro, claude-3-5-sonnet, etc.</li>
|
||||
<li>Barras de progresso visuais com indicadores de cores</li>
|
||||
<li>Consumo de tokens em tempo real com auto-refresh a cada 15s</li>
|
||||
<li>Gráficos de área para tokens de prompt & completion (diário/horário)</li>
|
||||
<li>Indicadores de tendência (% de variação vs período anterior)</li>
|
||||
<li>Sparklines inline nos cards de estatísticas</li>
|
||||
<li>Ranking de distribuição por modelo com gráficos de barras horizontais</li>
|
||||
<li>Visualização da proporção Prompt/Completion</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>🔄 Troca Automática</h3>
|
||||
<h3>🔌 Proxy de API Local (Gateway)</h3>
|
||||
<ul>
|
||||
<li>Modo de pool ilimitado com seleção inteligente</li>
|
||||
<li>Monitoramento a cada 5 minutos em segundo plano</li>
|
||||
<li>Endpoint compatível com OpenAI <code>/v1/chat/completions</code></li>
|
||||
<li>Endpoint compatível com Anthropic <code>/v1/messages</code></li>
|
||||
<li>Streaming SSE completo (testado com Cursor, Windsurf, OpenCode)</li>
|
||||
<li>Mapeamento de modelos (ex: <code>claude-sonnet-4-6</code> → <code>gemini-3-flash</code>)</li>
|
||||
<li>Porta, timeout e API key configuráveis</li>
|
||||
<li>Controle de visibilidade de modelos (mostrar/ocultar modelos específicos)</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🔌 Proxy Local</h3>
|
||||
<h3>🔄 Troca Automática Inteligente</h3>
|
||||
<ul>
|
||||
<li>Compatível com API da OpenAI & Anthropic</li>
|
||||
<li>Mapeamento de modelos customizável (ex: Claude → Gemini)</li>
|
||||
<li>Modo de pool ilimitado com seleção inteligente de backup</li>
|
||||
<li>Troca automática quando cota < 5% ou com rate limit</li>
|
||||
<li>Monitoramento em segundo plano a cada 5 minutos</li>
|
||||
<li>Fallback gracioso com rastreamento de motivo do status</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>🔍 Observabilidade do Proxy (Avançado)</h3>
|
||||
<ul>
|
||||
<li><strong>Monitor de Tráfego</strong> — Log ao vivo de requisições/respostas com latência, status e informações do modelo</li>
|
||||
<li><strong>Replay de Requisições</strong> — Reproduza qualquer requisição registrada para debugging</li>
|
||||
<li><strong>Capacidades dos Modelos</strong> — Inspecione suporte a visão, thinking, streaming e modo JSON por modelo</li>
|
||||
<li><strong>Ferramentas para Devs</strong> — Geração de código cURL & Python, cópia com um clique</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🔐 Segurança e Criptografia</h3>
|
||||
<ul>
|
||||
<li>Criptografia AES-256-GCM para todos os dados sensíveis</li>
|
||||
<li>Integração com gerenciador de credenciais nativo do SO (Keytar + SafeStorage)</li>
|
||||
<li>Migração automática de dados legados em texto plano</li>
|
||||
<li>Armazenamento criptografado de tokens & cotas por conta</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>⚙️ Configurações e Personalização</h3>
|
||||
<ul>
|
||||
<li>Suporte a temas Escuro / Claro / Sistema</li>
|
||||
<li>Multi-idioma: English & Português (Brasil)</li>
|
||||
<li>Override de proxy URL por conta</li>
|
||||
<li>Toggles de visibilidade de modelos</li>
|
||||
<li>Acesso ao diretório de logs</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🖥️ Experiência Desktop</h3>
|
||||
<ul>
|
||||
<li>App nativo Electron com integração na bandeja do sistema</li>
|
||||
<li>Sidebar retrátil com estado persistente</li>
|
||||
<li>Layouts responsivos em todas as telas</li>
|
||||
<li>Barra de status com indicadores ao vivo de proxy e conexão</li>
|
||||
<li>Error boundaries com fallbacks amigáveis ao usuário</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -98,20 +157,44 @@ Principais melhorias incluem:
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Começo Rápido
|
||||
## 📸 Screenshots
|
||||
|
||||
### Construindo pelo Código Fonte
|
||||
<p align="center">
|
||||
<img src="docs/assets/main.png" alt="Main" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/2usage.png" alt="Usage" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/3proxy.png" alt="Proxy" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/4routing.png" alt="Routing" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/5docs.png" alt="Docs" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/6connections.png" alt="Connections" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/7config.png" alt="Config" width="80%" />
|
||||
</p>
|
||||
|
||||
#### Pré-requisitos
|
||||
---
|
||||
|
||||
- Node.js v18 ou superior
|
||||
- npm
|
||||
## ⚡ Início Rápido
|
||||
|
||||
#### Passos
|
||||
### Pré-requisitos
|
||||
|
||||
- **Node.js** v20 ou superior
|
||||
- **npm** (este projeto usa `package-lock.json`)
|
||||
|
||||
### Compilando pelo Código Fonte
|
||||
|
||||
```bash
|
||||
# Clone o repositório
|
||||
git clone https://github.com/Draculabo/GeminiNexus.git
|
||||
git clone https://github.com/evandrodevbr/GeminiNexus.git
|
||||
cd GeminiNexus
|
||||
|
||||
# Instale as dependências
|
||||
@@ -120,10 +203,126 @@ npm install
|
||||
# Inicie o modo de desenvolvimento
|
||||
npm start
|
||||
|
||||
# Construa para produção
|
||||
# Compile para produção (instalador Windows)
|
||||
npm run make
|
||||
```
|
||||
|
||||
### Usando com IDEs de IA
|
||||
|
||||
Com o proxy em execução, configure sua IDE:
|
||||
|
||||
```plaintext
|
||||
# Nas configurações do Cursor / Windsurf / OpenCode:
|
||||
API Base URL: http://localhost:10100/v1
|
||||
API Key: (copie da página Proxy no app)
|
||||
Model: gemini-3-flash (ou qualquer modelo mapeado)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Stack Tecnológica
|
||||
|
||||
| Categoria | Tecnologias |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Core** | [Electron](https://www.electronjs.org/), [React 19](https://react.dev/), [TypeScript](https://www.typescriptlang.org/) |
|
||||
| **Build** | [Vite](https://vitejs.dev/), [Electron Forge](https://www.electronforge.io/) |
|
||||
| **Estilização** | [Tailwind CSS v4](https://tailwindcss.com/), [Radix UI](https://www.radix-ui.com/), [Lucide Icons](https://lucide.dev/) |
|
||||
| **Estado** | [TanStack Query](https://tanstack.com/query/latest), [TanStack Router](https://tanstack.com/router/latest) |
|
||||
| **Backend** | [NestJS](https://nestjs.com/) (gateway interno), [ORPC](https://orpc.unnoq.com/) (RPC type-safe) |
|
||||
| **Banco de Dados** | [Better-SQLite3](https://github.com/WiseLibs/better-sqlite3), [Drizzle ORM](https://orm.drizzle.team/) |
|
||||
| **Gráficos** | [Nivo](https://nivo.rocks/) (Line, Bar, Pie) |
|
||||
| **i18n** | [react-i18next](https://react.i18next.com/) |
|
||||
| **Testes** | [Vitest](https://vitest.dev/), [Playwright](https://playwright.dev/) |
|
||||
|
||||
---
|
||||
|
||||
## 💻 Desenvolvimento
|
||||
|
||||
```bash
|
||||
# Iniciar ambiente de dev (Electron + Vite HMR)
|
||||
npm start
|
||||
|
||||
# Executar linting
|
||||
npm run lint
|
||||
|
||||
# Verificar tipos
|
||||
npm run type-check
|
||||
|
||||
# Formatar código
|
||||
npm run format:write
|
||||
|
||||
# Rodar testes
|
||||
npm test
|
||||
```
|
||||
|
||||
### Estrutura do Projeto
|
||||
|
||||
```plaintext
|
||||
src/
|
||||
├── actions/ # Ações do app e orquestração de fluxos
|
||||
├── components/ # Componentes React
|
||||
│ ├── proxy/ # Componentes da página Proxy (abas avançadas)
|
||||
│ ├── usage/ # Componentes de analytics de uso (StatCard, ChartCard)
|
||||
│ └── ui/ # Primitivos base de UI (baseados no Radix)
|
||||
├── hooks/ # Custom React hooks
|
||||
├── ipc/ # Electron IPC + handlers de banco de dados
|
||||
├── layouts/ # Componentes de layout (MainLayout + sidebar)
|
||||
├── localization/ # Recursos de tradução i18n
|
||||
├── routes/ # Páginas TanStack Router (index, usage, proxy, settings)
|
||||
├── server/ # Backend NestJS (serviço de gateway proxy)
|
||||
├── services/ # Camada de serviços
|
||||
├── types/ # Definições de tipos TypeScript + schemas Zod
|
||||
└── utils/ # Funções utilitárias
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❓ FAQ
|
||||
|
||||
<details>
|
||||
<summary><b>P: O app não inicia?</b></summary>
|
||||
|
||||
1. Certifique-se de que todas as dependências estão instaladas: `npm install`
|
||||
2. Verifique se a versão do Node.js é >= 20
|
||||
3. Tente deletar `node_modules` e reinstalar
|
||||
4. No Windows, certifique-se de que o WiX Toolset está disponível para `npm run make`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>P: O login da conta falhou?</b></summary>
|
||||
|
||||
1. Verifique se a conexão com a internet está funcionando
|
||||
2. Tente limpar os dados do app e fazer login novamente
|
||||
3. Verifique se a conta está restrita pelo Google/Claude
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>P: A IDE não conecta no proxy?</b></summary>
|
||||
|
||||
1. Certifique-se de que o proxy está rodando (indicador verde na barra de status)
|
||||
2. Verifique se a porta corresponde à configuração da sua IDE (padrão: `10100`)
|
||||
3. Copie a API key da página Proxy e cole nas configurações da sua IDE
|
||||
4. Verifique se pelo menos uma conta está ativa na página de Contas
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>P: A contagem de tokens mostra zero?</b></summary>
|
||||
|
||||
1. Este era um bug conhecido em versões anteriores — atualize para a versão mais recente
|
||||
2. O rastreamento de tokens agora lida corretamente com metadados de streaming e contagens estimadas
|
||||
3. Os dados de uso são atualizados a cada 15 segundos na página de Analytics de Uso
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contribuindo
|
||||
|
||||
Contribuições são bem-vindas! Leia o `CONTRIBUTING.md` para mais detalhes.
|
||||
|
||||
---
|
||||
|
||||
## 📄 Licença
|
||||
@@ -138,9 +337,11 @@ npm run make
|
||||
> **Apenas para Fins Educacionais**
|
||||
>
|
||||
> Este projeto destina-se exclusivamente a fins de pesquisa e educação. Ele é fornecido "como está" sem qualquer garantia. **O uso comercial é estritamente proibido.**
|
||||
>
|
||||
> Ao usar este software, você concorda que não o utilizará para quaisquer fins comerciais, e que é o único responsável por garantir que seu uso esteja em conformidade com todas as leis e regulamentações aplicáveis. Os autores e contribuidores não se responsabilizam por qualquer uso indevido ou danos decorrentes do uso deste software.
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Créditos
|
||||
|
||||
**Gemini Nexus** é uma versão significativamente aprimorada e expandida baseada no projeto original [AntigravityManager](https://github.com/Draculabo/AntigravityManager) criado por [Draculabo](https://github.com/Draculabo). Toda a arquitetura fundamental e design do projeto foram criados pelo autor original.
|
||||
**Gemini Nexus** é uma versão significativamente aprimorada e expandida do projeto original [AntigravityManager](https://github.com/Draculabo/AntigravityManager) criado por [Draculabo](https://github.com/Draculabo). Toda a arquitetura fundamental e design foram criados pelo autor original.
|
||||
@@ -0,0 +1,250 @@
|
||||
<p align="center">
|
||||
<img src="docs/assets/logo.png" alt="Gemini Nexus" width="128" height="128" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">Gemini Nexus</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>🚀 专业的多账户 AI 网关,支持 Google Gemini 和 Claude</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> | <a href="README.pt-BR.md">Português</a> | 中文 | <a href="README.es.md">Español</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/v/release/evandrodevbr/GeminiNexus?style=flat-square" alt="版本" />
|
||||
</a>
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/releases">
|
||||
<img src="https://img.shields.io/github/downloads/evandrodevbr/GeminiNexus/total?style=flat-square&color=blue" alt="下载量" />
|
||||
</a>
|
||||
<a href="https://github.com/evandrodevbr/GeminiNexus/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/evandrodevbr/GeminiNexus?style=flat-square" alt="许可证" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/平台-Windows%20%7C%20macOS%20%7C%20Linux-informational?style=flat-square" alt="平台" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ✨ 为什么选择 Gemini Nexus?
|
||||
|
||||
- 😫 单个账户配额很快用完,需要频繁手动切换
|
||||
- 🔄 管理多个 Google/Claude 账户非常繁琐
|
||||
- 📊 不知道已经消耗了多少 Token 或还剩多少配额
|
||||
- 🔌 需要一个可靠的本地 API 代理,支持 OpenAI/Anthropic 协议
|
||||
|
||||
**Gemini Nexus** 解决所有这些问题。它是一个专业的 Electron 桌面应用,作为你的开发工具和 Google Gemini / Claude AI 之间的智能网关:
|
||||
|
||||
- ✅ **无限账户池** — 添加任意数量的 Google Gemini 和 Claude 账户
|
||||
- ✅ **智能自动切换** — 当配额不足或被限速时自动轮换到下一个可用账户
|
||||
- ✅ **实时使用分析** — SaaS 级仪表板,配备面积图、趋势指标和模型分布
|
||||
- ✅ **完整的代理可观测性** — 实时流量监控、请求重放和模型能力检查器
|
||||
- ✅ **兼容 OpenAI 和 Anthropic** — 即插即用的代理,支持 Cursor、Windsurf、OpenCode
|
||||
- ✅ **默认安全** — AES-256-GCM 加密,配合操作系统原生凭证管理
|
||||
|
||||
---
|
||||
|
||||
## 🎯 功能特性
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>☁️ 云端账户池</h3>
|
||||
<ul>
|
||||
<li>通过 OAuth 添加无限量的 Google Gemini / Claude 账户</li>
|
||||
<li>显示头像、邮箱、状态和最后使用时间</li>
|
||||
<li>实时状态监控(活跃、限速、过期)</li>
|
||||
<li>每个账户可独立配置代理 URL</li>
|
||||
<li>设备身份档案管理及历史记录</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>📊 使用分析仪表板</h3>
|
||||
<ul>
|
||||
<li>实时 Token 消耗,每 15 秒自动刷新</li>
|
||||
<li>面积图展示每日/每小时的 Prompt 和 Completion Token</li>
|
||||
<li>趋势指标(与上一周期相比的百分比变化)</li>
|
||||
<li>统计卡片内嵌迷你折线图(Sparklines)</li>
|
||||
<li>水平柱状图展示模型分布排名</li>
|
||||
<li>Prompt/Completion 比例可视化</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>🔌 本地 API 代理(网关)</h3>
|
||||
<ul>
|
||||
<li>兼容 OpenAI <code>/v1/chat/completions</code></li>
|
||||
<li>兼容 Anthropic <code>/v1/messages</code></li>
|
||||
<li>完整的 SSE 流式传输支持</li>
|
||||
<li>模型映射(如 <code>claude-sonnet-4-6</code> → <code>gemini-3-flash</code>)</li>
|
||||
<li>可配置端口、超时时间和 API 密钥</li>
|
||||
<li>模型可见性控制</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🔄 智能自动切换</h3>
|
||||
<ul>
|
||||
<li>无限池模式,智能备份选择</li>
|
||||
<li>配额低于 5% 或被限速时自动切换</li>
|
||||
<li>每 5 分钟后台监控</li>
|
||||
<li>优雅降级,带状态原因追踪</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>🔍 代理可观测性(高级)</h3>
|
||||
<ul>
|
||||
<li><strong>流量监控</strong> — 实时请求/响应日志</li>
|
||||
<li><strong>请求重放</strong> — 重放任意请求进行调试</li>
|
||||
<li><strong>模型能力</strong> — 检查视觉、思考、流式、JSON 模式</li>
|
||||
<li><strong>开发者工具</strong> — cURL 和 Python 代码生成</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🔐 安全与加密</h3>
|
||||
<ul>
|
||||
<li>所有敏感数据采用 AES-256-GCM 加密</li>
|
||||
<li>集成操作系统原生凭证管理器</li>
|
||||
<li>自动迁移旧版明文数据</li>
|
||||
<li>每个账户的 Token 和配额均加密存储</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<h3>⚙️ 设置与自定义</h3>
|
||||
<ul>
|
||||
<li>深色 / 浅色 / 跟随系统主题</li>
|
||||
<li>多语言:English & Português (Brasil)</li>
|
||||
<li>模型可见性开关</li>
|
||||
<li>日志目录访问</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<h3>🖥️ 桌面体验</h3>
|
||||
<ul>
|
||||
<li>原生 Electron 应用,系统托盘集成</li>
|
||||
<li>可折叠侧边栏,状态持久化</li>
|
||||
<li>响应式布局</li>
|
||||
<li>状态栏实时显示代理和连接指标</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## 📸 截图
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/main.png" alt="Main" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/2usage.png" alt="Usage" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/3proxy.png" alt="Proxy" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/4routing.png" alt="Routing" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/5docs.png" alt="Docs" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/6connections.png" alt="Connections" width="80%" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/assets/7config.png" alt="Config" width="80%" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 快速开始
|
||||
|
||||
### 前置要求
|
||||
|
||||
- **Node.js** v20 或更高版本
|
||||
- **npm**
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
git clone https://github.com/evandrodevbr/GeminiNexus.git
|
||||
cd GeminiNexus
|
||||
npm install
|
||||
npm start
|
||||
|
||||
# 构建生产版本
|
||||
npm run make
|
||||
```
|
||||
|
||||
### 配合 AI IDE 使用
|
||||
|
||||
```plaintext
|
||||
API Base URL: http://localhost:10100/v1
|
||||
API Key: (从应用的 Proxy 页面复制)
|
||||
Model: gemini-3-flash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
| 分类 | 技术 |
|
||||
| ---------- | --------------------------------------- |
|
||||
| **核心** | Electron, React 19, TypeScript |
|
||||
| **构建** | Vite, Electron Forge |
|
||||
| **样式** | Tailwind CSS v4, Radix UI, Lucide Icons |
|
||||
| **状态** | TanStack Query, TanStack Router |
|
||||
| **后端** | NestJS (内部网关), ORPC (类型安全 RPC) |
|
||||
| **数据库** | Better-SQLite3, Drizzle ORM |
|
||||
| **图表** | Nivo (Line, Bar, Pie) |
|
||||
| **测试** | Vitest, Playwright |
|
||||
|
||||
---
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
<details>
|
||||
<summary><b>问:应用无法启动?</b></summary>
|
||||
|
||||
1. 确保依赖已安装:`npm install`
|
||||
2. Node.js 版本 >= 20
|
||||
3. 尝试删除 `node_modules` 并重新安装
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>问:IDE 无法连接代理?</b></summary>
|
||||
|
||||
1. 确保代理正在运行(状态栏绿色指示器)
|
||||
2. 确认端口一致(默认 `10100`)
|
||||
3. 从 Proxy 页面复制 API 密钥
|
||||
4. 确保至少有一个账户处于活跃状态
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
[CC BY-NC-SA 4.0](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 免责声明
|
||||
|
||||
> [!WARNING]
|
||||
> **仅供教育目的**
|
||||
>
|
||||
> 本项目仅供教育和研究目的使用。按"原样"提供,不提供任何担保。**严禁商业用途。**
|
||||
|
||||
---
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
**Gemini Nexus** 基于 [Draculabo](https://github.com/Draculabo) 创建的原始项目 [AntigravityManager](https://github.com/Draculabo/AntigravityManager) 大幅增强和扩展。所有基础架构和设计均由原作者完成。
|
||||
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 131 KiB |
@@ -2,16 +2,16 @@
|
||||
|
||||
## 1. Nome e Descrição
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **Nome** | Gemini Nexus |
|
||||
| **Product Name** | Gemini Nexus |
|
||||
| **Package Name** | gemini-nexus |
|
||||
| **Versão** | 0.11.1 |
|
||||
| **Descrição EN** | Professional multi-account manager for Google Gemini & Claude AI |
|
||||
| Campo | Valor |
|
||||
| ------------------- | -------------------------------------------------------------------- |
|
||||
| **Nome** | Gemini Nexus |
|
||||
| **Product Name** | Gemini Nexus |
|
||||
| **Package Name** | gemini-nexus |
|
||||
| **Versão** | 0.11.1 |
|
||||
| **Descrição EN** | Professional multi-account manager for Google Gemini & Claude AI |
|
||||
| **Descrição PT-BR** | Gerenciador multi-contas profissional para Google Gemini & Claude AI |
|
||||
| **Autor** | evandrodevbr / Draculabo |
|
||||
| **Licença** | CC-BY-NC-SA-4.0 (apenas fins educacionais) |
|
||||
| **Autor** | evandrodevbr / Draculabo |
|
||||
| **Licença** | CC-BY-NC-SA-4.0 (apenas fins educacionais) |
|
||||
|
||||
---
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
Gemini Nexus é um **aplicativo desktop profissional** (Electron) que funciona como um **hub central (nexus)** para desenvolvedores que usam IDEs com IA.
|
||||
|
||||
### Funcionalidades Principais
|
||||
|
||||
- **Pool ilimitado de contas**: Adicionar múltiplas contas Google Gemini / Claude via OAuth
|
||||
- **Auto-switching inteligente**: Troca automática de conta quando a cota acaba ou há rate limit
|
||||
- **Monitoramento em tempo real**: Barras de progresso coloridas para visualizar uso de quota
|
||||
@@ -27,6 +28,7 @@ Gemini Nexus é um **aplicativo desktop profissional** (Electron) que funciona c
|
||||
- **Segurança**: Criptografia AES-256-GCM para dados sensíveis
|
||||
|
||||
### Público-Alvo
|
||||
|
||||
- Desenvolvedores que usam IDEs com integração AI (Cursor, Windsurf, VS Code, Claude Code, etc.)
|
||||
- Power users que precisam alternar entre múltiplas contas de IA
|
||||
- Usuários que precisam de um proxy API local confiável
|
||||
@@ -46,28 +48,32 @@ Gemini Nexus é um **aplicativo desktop profissional** (Electron) que funciona c
|
||||
## 4. Estilo Visual Atual
|
||||
|
||||
### Tema
|
||||
|
||||
- Dual theme (claro/escuro) com detecção automática do sistema
|
||||
- Extremamente minimalista e quase monocromático
|
||||
|
||||
### Paleta de Cores (Tailwind OKLCH)
|
||||
|
||||
| Modo | Background | Foreground | Primary | Destructive | Destaque |
|
||||
|------|-----------|------------|---------|-------------|----------|
|
||||
| **Claro** | oklch(0.99 0 0) — quase branco | oklch(0 0 0) — preto | Preto | Vermelho | Azul |
|
||||
| **Escuro** | oklch(0 0 0) — preto puro | oklch(1 0 0) — branco | Branco | Vermelho escuro | — |
|
||||
| Modo | Background | Foreground | Primary | Destructive | Destaque |
|
||||
| ---------- | ------------------------------ | --------------------- | ------- | --------------- | -------- |
|
||||
| **Claro** | oklch(0.99 0 0) — quase branco | oklch(0 0 0) — preto | Preto | Vermelho | Azul |
|
||||
| **Escuro** | oklch(0 0 0) — preto puro | oklch(1 0 0) — branco | Branco | Vermelho escuro | — |
|
||||
|
||||
### Cores Funcionais
|
||||
|
||||
- **Emerald/Green (`#10b981`)** — status ativo, quota alta, running
|
||||
- **Amber/Yellow** — quota média, alertas
|
||||
- **Rose/Red** — quota baixa, erro, stopped
|
||||
- **Blue** — AI credits, informações premium
|
||||
|
||||
### Tipografia
|
||||
|
||||
- **Geist** (sans-serif) — fonte principal moderna e geométrica (da Vercel)
|
||||
- **Geist Mono** (monospace) — dados técnicos, números de quota
|
||||
- **Tomorrow** (display/futurista) — possíveis títulos estilizados
|
||||
|
||||
### Características de UI
|
||||
|
||||
- Bordas arredondadas (`0.5rem` / 8px)
|
||||
- Sombras sutis
|
||||
- Cards com hover states suaves
|
||||
@@ -77,45 +83,50 @@ Gemini Nexus é um **aplicativo desktop profissional** (Electron) que funciona c
|
||||
|
||||
## 5. Assets Visuais Existentes
|
||||
|
||||
| Arquivo | Descrição |
|
||||
|---------|-----------|
|
||||
| `docs/assets/logo.png` | Logo antiga (Antigravity) — 128x128, usada no README |
|
||||
| `docs/assets/screenshot-main.png` | Screenshot da interface principal |
|
||||
| `docs/assets/screenshot-proxy.png` | Screenshot da aba proxy |
|
||||
| `docs/assets/screenshot-setting.png` | Screenshot de settings |
|
||||
| `src/assets/icon.png` | Ícone do app (também é o logo antigo) |
|
||||
| `src/assets/tray.png` | Ícone da system tray |
|
||||
| `images/icon.ico` / `images/icon.icns` | Ícones de build para Windows/Mac |
|
||||
| `images/favicon-*.png` | Favicons em múltiplos tamanhos |
|
||||
| `images/32x32.png`, `64x64.png`, `128x128.png`, `[email protected]` | Ícones para AppImage Linux |
|
||||
| `src/assets/fonts/geist/` | Fonte Geist (principal) |
|
||||
| `src/assets/fonts/geist-mono/` | Fonte Geist Mono (código/dados) |
|
||||
| `src/assets/fonts/tomorrow/` | Fonte Tomorrow (display, 4 pesos) |
|
||||
| Arquivo | Descrição |
|
||||
| ---------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `docs/assets/logo.png` | Logo antiga (Antigravity) — 128x128, usada no README |
|
||||
| `docs/assets/screenshot-main.png` | Screenshot da interface principal |
|
||||
| `docs/assets/screenshot-proxy.png` | Screenshot da aba proxy |
|
||||
| `docs/assets/screenshot-setting.png` | Screenshot de settings |
|
||||
| `src/assets/icon.png` | Ícone do app (também é o logo antigo) |
|
||||
| `src/assets/tray.png` | Ícone da system tray |
|
||||
| `images/icon.ico` / `images/icon.icns` | Ícones de build para Windows/Mac |
|
||||
| `images/favicon-*.png` | Favicons em múltiplos tamanhos |
|
||||
| `images/32x32.png`, `64x64.png`, `128x128.png`, `[email protected]` | Ícones para AppImage Linux |
|
||||
| `src/assets/fonts/geist/` | Fonte Geist (principal) |
|
||||
| `src/assets/fonts/geist-mono/` | Fonte Geist Mono (código/dados) |
|
||||
| `src/assets/fonts/tomorrow/` | Fonte Tomorrow (display, 4 pesos) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Conceitos Visuais Sugeridos para a Logo
|
||||
|
||||
### A. Nodo / Conexão (Nexus)
|
||||
|
||||
- Ponto central conectado a múltiplos pontos (contas gerenciadas)
|
||||
- Círculo central com 2-3 linhas finas irradiando
|
||||
- Dois círculos interligados (Gemini + Claude)
|
||||
|
||||
### B. Infinito / Loop (Gemini + Proxy)
|
||||
|
||||
- Símbolo de infinito (∞) modificado como circuito/loop de dados
|
||||
- Referência à dualidade "Gemini" (gêmeos)
|
||||
- Curva contínua elegante com seta indicando fluxo
|
||||
|
||||
### C. Cubo / Hexágono (Nexus / Hub)
|
||||
|
||||
- Hexágono ou cubo isométrico minimalista
|
||||
- Linhas finas, perspectiva suave
|
||||
- Face "aberta" ou conectada
|
||||
|
||||
### D. Monograma "GN"
|
||||
|
||||
- Letras "G" e "N" abstraídas como nodo de rede
|
||||
- Estilo geométrico, similar a Tomorrow ou Geist
|
||||
|
||||
### E. Proxy / Pipe (Fluxo de Dados)
|
||||
|
||||
- Duas barras paralelas com seta passando entre elas
|
||||
- "Pipe" minimalista — conduz tráfego entre APIs
|
||||
|
||||
@@ -123,15 +134,15 @@ Gemini Nexus é um **aplicativo desktop profissional** (Electron) que funciona c
|
||||
|
||||
## 7. Diretrizes para a Nova Logo
|
||||
|
||||
| Elemento | Especificação |
|
||||
|----------|---------------|
|
||||
| **Estilo** | Minimalista, geométrica, precisa, técnica |
|
||||
| **Cores principais** | Preto (`#000000`) e branco (`#ffffff`) |
|
||||
| **Cor de acento (opcional)** | Emerald (`#10b981`) para "ativo/online" |
|
||||
| **Versões necessárias** | Dark mode e light mode |
|
||||
| **Tamanhos** | De 16x16 (tray) até 512x512 (macOS) |
|
||||
| **Monocromática** | Deve funcionar em preto e branco puro |
|
||||
| **Tipografia do wordmark** | Geométrica sans-serif, peso medium/semibold, tracking levemente expandido |
|
||||
| Elemento | Especificação |
|
||||
| ---------------------------- | ------------------------------------------------------------------------- |
|
||||
| **Estilo** | Minimalista, geométrica, precisa, técnica |
|
||||
| **Cores principais** | Preto (`#000000`) e branco (`#ffffff`) |
|
||||
| **Cor de acento (opcional)** | Emerald (`#10b981`) para "ativo/online" |
|
||||
| **Versões necessárias** | Dark mode e light mode |
|
||||
| **Tamanhos** | De 16x16 (tray) até 512x512 (macOS) |
|
||||
| **Monocromática** | Deve funcionar em preto e branco puro |
|
||||
| **Tipografia do wordmark** | Geométrica sans-serif, peso medium/semibold, tracking levemente expandido |
|
||||
|
||||
---
|
||||
|
||||
@@ -140,6 +151,7 @@ Gemini Nexus é um **aplicativo desktop profissional** (Electron) que funciona c
|
||||
O app pede uma logo que evoque **conexão/nexo** (Nexus) com **múltiplos pontos** (contas) de forma geométrica e limpa, usando primariamente preto/branco com um toque sutil de verde (emerald) para representar o estado "ativo/online" central ao propósito do app.
|
||||
|
||||
A logo deve transmitir:
|
||||
|
||||
- **Tecnologia** e **precisão**
|
||||
- **Conectividade** e **hub central**
|
||||
- **Profissionalismo** e **confiança**
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const projectRoot = process.cwd();
|
||||
|
||||
// Extensões permitidas
|
||||
const allowedExtensions = /\.(ts|tsx)$/;
|
||||
|
||||
function walkAndFix(dir) {
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
for (const file of files) {
|
||||
if (['node_modules', '.git', 'dist', 'out'].includes(file)) continue;
|
||||
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
walkAndFix(fullPath);
|
||||
} else if (allowedExtensions.test(file)) {
|
||||
let content = fs.readFileSync(fullPath, 'utf-8');
|
||||
const original = content;
|
||||
|
||||
// Fix specific known broken identifiers
|
||||
content = content.replace(/closeGemini Nexus/g, 'closeGeminiNexus');
|
||||
content = content.replace(/startGemini Nexus/g, 'startGeminiNexus');
|
||||
content = content.replace(/Gemini NexusVersion/g, 'GeminiNexusVersion');
|
||||
content = content.replace(/Gemini NexusDb/g, 'GeminiNexusDb');
|
||||
content = content.replace(/Gemini NexusStorage/g, 'GeminiNexusStorage');
|
||||
content = content.replace(/Gemini NexusExecutable/g, 'GeminiNexusExecutable');
|
||||
content = content.replace(/Gemini NexusCoreFeatures/g, 'GeminiNexusCoreFeatures');
|
||||
content = content.replace(/getGemini Nexus/g, 'getGeminiNexus');
|
||||
|
||||
if (content !== original) {
|
||||
fs.writeFileSync(fullPath, content, 'utf-8');
|
||||
console.log(`Fixed TS errors in: ${fullPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkAndFix(projectRoot);
|
||||
console.log('Fix complete.');
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1774106199,
|
||||
"narHash": "sha256-US5Tda2sKmjrg2lNHQL3jRQ6p96cgfWh3J1QBliQ8Ws=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6c9a78c09ff4d6c21d0319114873508a6ec01655",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
{
|
||||
description = "Antigravity Manager";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
}: let
|
||||
supportedSystems = ["x86_64-linux" "aarch64-linux"];
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
|
||||
version = "0.10.0";
|
||||
|
||||
sources = {
|
||||
"x86_64-linux" = {
|
||||
url = "https://github.com/Draculabo/AntigravityManager/releases/download/v${version}/Antigravity.Manager_${version}_amd64.deb";
|
||||
sha256 = "5fc08b4a9308c71fc00e44b95d4fb3178182aea8821708fa51fdf2bf5dbbb856";
|
||||
};
|
||||
"aarch64-linux" = {
|
||||
url = "https://github.com/Draculabo/AntigravityManager/releases/download/v${version}/Antigravity.Manager_${version}_arm64.deb";
|
||||
sha256 = "666c9659efa66bb6be6a8e7fb0c384ceb799a17245eab374bf9b3d7901c5c4cb";
|
||||
};
|
||||
};
|
||||
|
||||
mkPackage = pkgs: let
|
||||
system = pkgs.stdenv.hostPlatform.system;
|
||||
source = sources.${system} or (throw "Unsupported system: ${system}");
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "antigravity-manager";
|
||||
inherit version;
|
||||
|
||||
src = pkgs.fetchurl {
|
||||
inherit (source) url sha256;
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
autoPatchelfHook
|
||||
makeWrapper
|
||||
zstd
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
alsa-lib
|
||||
at-spi2-atk
|
||||
atk
|
||||
cups
|
||||
dbus
|
||||
expat
|
||||
glib
|
||||
gtk3
|
||||
libsecret
|
||||
libdrm
|
||||
libxkbcommon
|
||||
mesa
|
||||
nspr
|
||||
nss
|
||||
pango
|
||||
systemd
|
||||
libx11
|
||||
libxcomposite
|
||||
libxdamage
|
||||
libxext
|
||||
libxfixes
|
||||
libxrandr
|
||||
libxcb
|
||||
];
|
||||
|
||||
unpackPhase = ''
|
||||
ar x $src
|
||||
tar xf data.tar.* --no-same-permissions
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin $out/lib $out/share
|
||||
cp -r usr/lib/antigravity-manager $out/lib/
|
||||
cp -r usr/share/* $out/share/
|
||||
|
||||
ln -s $out/lib/antigravity-manager/antigravity-manager $out/bin/antigravity-manager
|
||||
'';
|
||||
|
||||
postFixup = ''
|
||||
# Wrap the executable so it finds Wayland/X11 and libsecret
|
||||
wrapProgram $out/bin/antigravity-manager \
|
||||
--add-flags "--enable-features=WaylandWindowDecorations" \
|
||||
--add-flags "--ozone-platform-hint=auto" \
|
||||
--prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath [pkgs.libsecret]}
|
||||
|
||||
if [ -e $out/share/applications/antigravity-manager.desktop ]; then
|
||||
substituteInPlace $out/share/applications/antigravity-manager.desktop \
|
||||
--replace "/usr/lib/antigravity-manager/antigravity-manager" "antigravity-manager"
|
||||
fi
|
||||
'';
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Professional multi-account manager for Google Gemini & Claude AI";
|
||||
homepage = "https://github.com/Draculabo/AntigravityManager";
|
||||
license = licenses.cc-by-nc-sa-40;
|
||||
maintainers = [];
|
||||
platforms = ["x86_64-linux" "aarch64-linux"];
|
||||
mainProgram = "antigravity-manager";
|
||||
};
|
||||
};
|
||||
in {
|
||||
overlays.default = final: prev: {
|
||||
antigravity-manager = mkPackage final;
|
||||
};
|
||||
|
||||
packages = forAllSystems (system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
in {
|
||||
default = mkPackage pkgs;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
import { MakerDeb } from '@electron-forge/maker-deb';
|
||||
import { MakerDMG } from '@electron-forge/maker-dmg';
|
||||
import { MakerRpm } from '@electron-forge/maker-rpm';
|
||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
|
||||
import { MakerWix } from '@electron-forge/maker-wix';
|
||||
import { MakerZIP } from '@electron-forge/maker-zip';
|
||||
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
|
||||
@@ -441,10 +440,6 @@ const config: ForgeConfig = {
|
||||
},
|
||||
},
|
||||
makers: [
|
||||
new MakerSquirrel({
|
||||
setupIcon: 'images/icon.ico',
|
||||
iconUrl: 'https://raw.githubusercontent.com/evandrodevbr/GeminiNexus/main/images/icon.ico',
|
||||
}),
|
||||
...(process.platform === 'win32' && process.arch === 'x64' && isWixAvailable()
|
||||
? [
|
||||
new MakerWix({
|
||||
@@ -463,7 +458,7 @@ const config: ForgeConfig = {
|
||||
},
|
||||
['darwin'],
|
||||
),
|
||||
new MakerZIP({}, ['darwin']),
|
||||
new MakerZIP({}, ['darwin', 'win32']),
|
||||
appImageMaker,
|
||||
new MakerRpm({}),
|
||||
new MakerDeb({}),
|
||||
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 1001 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1001 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 736 KiB |
|
Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 436 B |
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "gemini-nexus",
|
||||
"productName": "Gemini Nexus",
|
||||
"version": "0.13.0",
|
||||
"version": "0.15.2",
|
||||
"description": "Gemini Nexus - Electron App",
|
||||
"main": ".vite/build/main.js",
|
||||
"private": true,
|
||||
"packageManager": "[email protected]",
|
||||
"scripts": {
|
||||
"start": "cross-env DEBUG=electron-forge:* electron-forge start",
|
||||
"dev": "cross-env DEBUG=electron-forge:* electron-forge start",
|
||||
@@ -31,7 +32,6 @@
|
||||
"@electron-forge/maker-deb": "^7.11.1",
|
||||
"@electron-forge/maker-dmg": "^7.11.1",
|
||||
"@electron-forge/maker-rpm": "^7.11.1",
|
||||
"@electron-forge/maker-squirrel": "^7.11.1",
|
||||
"@electron-forge/maker-wix": "^7.9.0",
|
||||
"@electron-forge/maker-zip": "^7.11.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
|
||||
@@ -45,6 +45,7 @@
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@pengx17/electron-forge-maker-appimage": "^1.0.2",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@rolldown/plugin-babel": "^0.2.3",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/github": "^12.0.6",
|
||||
@@ -62,7 +63,7 @@
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"code-inspector-plugin": "^1.3.3",
|
||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||
@@ -169,9 +170,28 @@
|
||||
"@nestjs/core",
|
||||
"@sentry/cli",
|
||||
"@swc/core",
|
||||
"protobufjs"
|
||||
"protobufjs",
|
||||
"macos-alias",
|
||||
"better-sqlite3",
|
||||
"keytar",
|
||||
"electron",
|
||||
"electron-winstaller",
|
||||
"esbuild"
|
||||
],
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@bitdisaster/exe-icon-extractor",
|
||||
"@nestjs/core",
|
||||
"@sentry/cli",
|
||||
"@swc/core",
|
||||
"protobufjs",
|
||||
"macos-alias",
|
||||
"better-sqlite3",
|
||||
"keytar",
|
||||
"electron",
|
||||
"electron-winstaller",
|
||||
"esbuild"
|
||||
],
|
||||
"overrides": {
|
||||
"node-abi": "^4.28.0",
|
||||
"fastify": "^5.8.5",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let file = fs.readFileSync('src/routes/proxy.tsx', 'utf8');
|
||||
|
||||
// The Proxy layout also uses heavy cards
|
||||
file = file.replace(/<Card>/g, '<Card className="bg-background border border-white/5 shadow-none">');
|
||||
|
||||
fs.writeFileSync('src/routes/proxy.tsx', file);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
const fs = require('fs');
|
||||
let file = fs.readFileSync('src/routes/settings.tsx', 'utf8');
|
||||
|
||||
// The settings page uses standard cards that look bulky in minimal dark mode
|
||||
file = file.replace(/<div className="flex flex-col gap-6">/g, '<div className="flex flex-col gap-2">');
|
||||
file = file.replace(/<Card>/g, '<Card className="bg-transparent border-0 shadow-none rounded-none border-b border-white/5 last:border-0 pb-6">');
|
||||
file = file.replace(/<CardHeader>/g, '<CardHeader className="px-0 pt-4">');
|
||||
file = file.replace(/<CardContent>/g, '<CardContent className="px-0">');
|
||||
|
||||
fs.writeFileSync('src/routes/settings.tsx', file);
|
||||
|
||||
@@ -227,9 +227,6 @@ importers:
|
||||
'@electron-forge/maker-rpm':
|
||||
specifier: ^7.11.1
|
||||
version: 7.11.1
|
||||
'@electron-forge/maker-squirrel':
|
||||
specifier: ^7.11.1
|
||||
version: 7.11.1
|
||||
'@electron-forge/maker-wix':
|
||||
specifier: ^7.9.0
|
||||
version: 7.11.1
|
||||
@@ -269,6 +266,9 @@ importers:
|
||||
'@playwright/test':
|
||||
specifier: ^1.56.1
|
||||
version: 1.59.1
|
||||
'@rolldown/plugin-babel':
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3(@babel/[email protected])(@babel/[email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
'@semantic-release/changelog':
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3([email protected]([email protected]))
|
||||
@@ -321,8 +321,8 @@ importers:
|
||||
specifier: ^19.2.2
|
||||
version: 19.2.3(@types/[email protected])
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(@rolldown/[email protected](@babel/[email protected])(@babel/[email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
babel-plugin-react-compiler:
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
@@ -394,7 +394,7 @@ importers:
|
||||
version: 25.0.3([email protected])
|
||||
semantic-release-config-gitmoji:
|
||||
specifier: ^1.5.3
|
||||
version: 1.5.3(@octokit/core@7.0.6)([email protected])([email protected]([email protected]))
|
||||
version: 1.5.3(@octokit/core@4.2.4)([email protected])([email protected]([email protected]))
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
@@ -536,18 +536,6 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/[email protected]':
|
||||
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/[email protected]':
|
||||
resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/[email protected]':
|
||||
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -672,10 +660,6 @@ packages:
|
||||
resolution: {integrity: sha512-iEfJPRQQyaTqk2EbUfZgulChNWvxGXeYUH0xBX/r5cj1pL4vcJXt3jLMQBVn3mk/0Ytv9UWRs8R/XuNWX6sf2w==}
|
||||
engines: {node: '>= 16.4.0'}
|
||||
|
||||
'@electron-forge/[email protected]':
|
||||
resolution: {integrity: sha512-oSg7fgad6l+X0DjtRkSpMzB0AjzyDO4mb2gzM4kTodkP1ADeiMi08bxy0ZeCESqLm5+fG72cAPmEr3BAPvI1yw==}
|
||||
engines: {node: '>= 16.4.0'}
|
||||
|
||||
'@electron-forge/[email protected]':
|
||||
resolution: {integrity: sha512-yKsnfPQVBrjyjWndmSB0rqQSXAiIukwz5fuVyoCTnYpfJQojrEEMUN/eXSHA08d+rOPbB/hkMAR+QJcjJ8vO2Q==}
|
||||
engines: {node: '>= 16.4.0'}
|
||||
@@ -1169,183 +1153,155 @@ packages:
|
||||
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
||||
@@ -2719,42 +2675,36 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]':
|
||||
resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/[email protected]':
|
||||
resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]':
|
||||
resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]':
|
||||
resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]':
|
||||
resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/[email protected]':
|
||||
resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==}
|
||||
@@ -2779,12 +2729,29 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/plugin[email protected]':
|
||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||
'@rolldown/plugin[email protected]':
|
||||
resolution: {integrity: sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==}
|
||||
engines: {node: '>=22.12.0 || ^24.0.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.29.0 || ^8.0.0-rc.1
|
||||
'@babel/plugin-transform-runtime': ^7.29.0 || ^8.0.0-rc.1
|
||||
'@babel/runtime': ^7.27.0 || ^8.0.0-rc.1
|
||||
rolldown: ^1.0.0-rc.5
|
||||
vite: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
'@babel/plugin-transform-runtime':
|
||||
optional: true
|
||||
'@babel/runtime':
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]':
|
||||
resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==}
|
||||
|
||||
'@rolldown/[email protected]':
|
||||
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -2828,79 +2795,66 @@ packages:
|
||||
resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==}
|
||||
@@ -3172,42 +3126,36 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/[email protected]':
|
||||
resolution: {integrity: sha512-1SYGs2l0Yyyi0pR/P/NKz/x0kqxkoiw+BXeJjLUdecSk/KasncWlJrc6hOvFSgKHOBrzgM5jwuluKtlT8dnrcA==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/[email protected]':
|
||||
resolution: {integrity: sha512-TXREtiXeRhbfDFbmhnkIsXpKfzbfT73YkV2ZF6w0sfxgjC5zI2ZAbaCOq25qxvegofj2K93DtOpm9RLaBgqR2g==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/[email protected]':
|
||||
resolution: {integrity: sha512-DCR2YYeyd6DQE4OuDhImouuNcjXEiEdnn1Y0DyGteugPEDvVuvYk8Xddi+4o2SgWH6jiW8/I+3emZvbep1NC+g==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/[email protected]':
|
||||
resolution: {integrity: sha512-5Pizw3NgfOJ5BJOBK8TIRa59xFW2avESTOBDPTAYwZYa1JNDs+KMF9lUfjJiJLM5HiMs/wPheA9eiT0q9m2AoA==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/[email protected]':
|
||||
resolution: {integrity: sha512-qyqydP/wyH8alcIP4a2hnGSjHLJjm9H7yDFup+CPy9oTahFgLLwnNcv5UHXqO2Qs3AIND+cls5f/Bb6hqpxdgA==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/[email protected]':
|
||||
resolution: {integrity: sha512-CaQENgDHVGOg1mSF5sQVgvfFHG9kjMor2rkLMLeLOkfZYNj13ppnJ9+lfaBZLZUMMbnlGQnavCJb8PVBUOso7Q==}
|
||||
@@ -3284,28 +3232,24 @@ packages:
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==}
|
||||
@@ -3491,18 +3435,6 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
|
||||
|
||||
@@ -3717,11 +3649,18 @@ packages:
|
||||
resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@vitejs/plugin-react@4.7.0':
|
||||
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
'@vitejs/plugin-react@6.0.1':
|
||||
resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
'@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
|
||||
babel-plugin-react-compiler: ^1.0.0
|
||||
vite: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
'@rolldown/plugin-babel':
|
||||
optional: true
|
||||
babel-plugin-react-compiler:
|
||||
optional: true
|
||||
|
||||
'@vitest/[email protected]':
|
||||
resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==}
|
||||
@@ -4958,10 +4897,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-1sZTssferjgDgaqRTc0ieP+ozzpOy7LQTPTtEW3yQFn4+ORdIAZWV5BthXPyHF7YqLvFJCUPhNhdAJQYlYUgiw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-EYj1cm1nZoVHmIIx3o0aKt784lxdEpJnXbEnyypklUCnglqSb7ni+1xi1Vp/gtrGS/mzIxnWBT+x5fIfuDjhvA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -5430,10 +5365,6 @@ packages:
|
||||
resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
|
||||
engines: {node: '>=6 <7 || >=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
||||
engines: {node: '>=6 <7 || >=8'}
|
||||
@@ -6293,28 +6224,24 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||
@@ -7440,10 +7367,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8108,10 +8031,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==}
|
||||
engines: {node: '>=14.16'}
|
||||
@@ -8875,10 +8794,10 @@ snapshots:
|
||||
|
||||
'@acemir/[email protected]': {}
|
||||
|
||||
'@achingbrain/[email protected](@octokit/core@7.0.6)([email protected]([email protected]))':
|
||||
'@achingbrain/[email protected](@octokit/core@4.2.4)([email protected]([email protected]))':
|
||||
dependencies:
|
||||
'@octokit/plugin-retry': 3.0.9
|
||||
'@octokit/plugin-throttling': 3.7.0(@octokit/core@7.0.6)
|
||||
'@octokit/plugin-throttling': 3.7.0(@octokit/core@4.2.4)
|
||||
'@octokit/rest': 19.0.13
|
||||
'@semantic-release/error': 3.0.0
|
||||
aggregate-error: 3.1.0
|
||||
@@ -9029,16 +8948,6 @@ snapshots:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/[email protected](@babel/[email protected])':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/[email protected](@babel/[email protected])':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/[email protected]': {}
|
||||
|
||||
'@babel/[email protected]':
|
||||
@@ -9280,17 +9189,6 @@ snapshots:
|
||||
- bluebird
|
||||
- supports-color
|
||||
|
||||
'@electron-forge/[email protected]':
|
||||
dependencies:
|
||||
'@electron-forge/maker-base': 7.11.1
|
||||
'@electron-forge/shared-types': 7.11.1
|
||||
fs-extra: 10.1.0
|
||||
optionalDependencies:
|
||||
electron-winstaller: 5.4.0
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
|
||||
'@electron-forge/[email protected]':
|
||||
dependencies:
|
||||
'@electron-forge/core-utils': 7.11.1
|
||||
@@ -10648,9 +10546,9 @@ snapshots:
|
||||
'@octokit/types': 16.0.0
|
||||
bottleneck: 2.19.5
|
||||
|
||||
'@octokit/[email protected](@octokit/core@7.0.6)':
|
||||
'@octokit/[email protected](@octokit/core@4.2.4)':
|
||||
dependencies:
|
||||
'@octokit/core': 7.0.6
|
||||
'@octokit/core': 4.2.4
|
||||
'@octokit/types': 6.41.0
|
||||
bottleneck: 2.19.5
|
||||
|
||||
@@ -11708,10 +11606,19 @@ snapshots:
|
||||
'@rolldown/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@rolldown/plugin[email protected]': {}
|
||||
'@rolldown/plugin[email protected](@babel/[email protected])(@babel/[email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
picomatch: 4.0.4
|
||||
rolldown: 1.0.0-rc.17
|
||||
optionalDependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
vite: 8.0.10(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
|
||||
|
||||
'@rolldown/[email protected]': {}
|
||||
|
||||
'@rolldown/[email protected]': {}
|
||||
|
||||
'@rollup/[email protected]([email protected])':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@@ -12392,27 +12299,6 @@ snapshots:
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.2
|
||||
'@babel/types': 7.29.0
|
||||
'@types/babel__generator': 7.27.0
|
||||
'@types/babel__template': 7.4.4
|
||||
'@types/babel__traverse': 7.28.0
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.2
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@types/node': 24.12.2
|
||||
@@ -12672,17 +12558,13 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.59.1
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
'@vitejs/plugin-react@4.7.0([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))':
|
||||
'@vitejs/plugin-react@6.0.1(@rolldown/[email protected](@babel/[email protected])(@babel/[email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/[email protected])
|
||||
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/[email protected])
|
||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.17.0
|
||||
'@rolldown/pluginutils': 1.0.0-rc.7
|
||||
vite: 8.0.10(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optionalDependencies:
|
||||
'@rolldown/plugin-babel': 0.2.3(@babel/[email protected])(@babel/[email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
babel-plugin-react-compiler: 1.0.0
|
||||
|
||||
'@vitest/[email protected]':
|
||||
dependencies:
|
||||
@@ -14063,19 +13945,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@electron/asar': 3.4.1
|
||||
debug: 4.4.3
|
||||
fs-extra: 7.0.1
|
||||
lodash: 4.18.1
|
||||
temp: 0.9.4
|
||||
optionalDependencies:
|
||||
'@electron/windows-sign': 1.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@electron/windows-sign': 1.2.2
|
||||
@@ -14757,13 +14626,6 @@ snapshots:
|
||||
jsonfile: 6.2.1
|
||||
universalify: 2.0.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 4.0.0
|
||||
universalify: 0.1.2
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -16615,8 +16477,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected](@types/[email protected])([email protected]):
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
@@ -16973,12 +16833,12 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected](@octokit/core@7.0.6)([email protected])([email protected]([email protected])):
|
||||
[email protected](@octokit/core@4.2.4)([email protected])([email protected]([email protected])):
|
||||
dependencies:
|
||||
'@gitmoji/commit-types': 1.1.5
|
||||
'@semantic-release/changelog': 6.0.3([email protected]([email protected]))
|
||||
'@semantic-release/git': 10.0.1([email protected]([email protected]))
|
||||
'@semantic-release/github': '@achingbrain/[email protected](@octokit/core@7.0.6)([email protected]([email protected]))'
|
||||
'@semantic-release/github': '@achingbrain/[email protected](@octokit/core@4.2.4)([email protected]([email protected]))'
|
||||
'@semrel-extra/npm': 1.2.2
|
||||
'@types/semantic-release': 17.2.11
|
||||
conventional-changelog-gitmoji-config: 1.5.2([email protected])
|
||||
@@ -17501,12 +17361,6 @@ snapshots:
|
||||
async-exit-hook: 2.0.1
|
||||
fs-extra: 10.1.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
mkdirp: 0.5.6
|
||||
rimraf: 2.6.3
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-stream: 3.0.0
|
||||
|
||||
@@ -8,5 +8,6 @@ allowBuilds:
|
||||
electron-winstaller: true
|
||||
esbuild: true
|
||||
keytar: true
|
||||
macos-alias: true
|
||||
protobufjs: true
|
||||
sharp: true
|
||||
@@ -6,7 +6,7 @@
|
||||
* instead of Gitmoji (since the project history doesn't use emojis).
|
||||
*/
|
||||
|
||||
const REPO_URL = 'https://github.com/Draculabo/AntigravityManager';
|
||||
const REPO_URL = 'https://github.com/evandrodevbr/GeminiNexus';
|
||||
const GITHUB_BASE_URL = 'https://github.com';
|
||||
const FULL_CHANGELOG_URL = `${REPO_URL}/blob/main/CHANGELOG.md`;
|
||||
|
||||
@@ -244,7 +244,7 @@ const releaseConfig = {
|
||||
[
|
||||
'@semantic-release/git',
|
||||
{
|
||||
assets: ['CHANGELOG.md', 'package.json', 'package-lock.json', 'npm-shrinkwrap.json'],
|
||||
assets: ['CHANGELOG.md', 'package.json', 'pnpm-lock.yaml'],
|
||||
message: 'chore(release): ${nextRelease.version}\n\n${nextRelease.notes}',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const projectRoot = process.cwd();
|
||||
|
||||
// Extensões permitidas
|
||||
const allowedExtensions = /\.(ts|tsx|js|jsx|json|md|html|jsonc|yml|yaml|css)$/;
|
||||
|
||||
// Arquivos/Pastas a ignorar (já curados ou irrelevantes)
|
||||
const ignoreDirs = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
'.gemini',
|
||||
'dist',
|
||||
'out',
|
||||
'build',
|
||||
'images',
|
||||
'docs',
|
||||
'.planning',
|
||||
];
|
||||
const ignoreFiles = [
|
||||
'README.md',
|
||||
'README.pt-BR.md',
|
||||
'CHANGELOG.md',
|
||||
'forge.config.ts',
|
||||
'package.json',
|
||||
'rename.js',
|
||||
'rename-deep.js',
|
||||
'task.md',
|
||||
'walkthrough.md',
|
||||
];
|
||||
|
||||
function walkAndReplace(dir) {
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
for (const file of files) {
|
||||
if (ignoreDirs.includes(file)) continue;
|
||||
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
walkAndReplace(fullPath);
|
||||
|
||||
// Rename directory if it contains 'antigravity'
|
||||
if (file.toLowerCase().includes('antigravity')) {
|
||||
const newName = file.replace(/antigravity/gi, (match) => {
|
||||
if (match === 'antigravity') return 'geminiNexus';
|
||||
if (match === 'Antigravity') return 'GeminiNexus';
|
||||
return 'geminiNexus';
|
||||
});
|
||||
const newPath = path.join(dir, newName);
|
||||
fs.renameSync(fullPath, newPath);
|
||||
console.log(`Renamed directory: ${fullPath} -> ${newPath}`);
|
||||
}
|
||||
} else {
|
||||
if (ignoreFiles.includes(file)) continue;
|
||||
|
||||
if (allowedExtensions.test(file)) {
|
||||
let content = fs.readFileSync(fullPath, 'utf-8');
|
||||
const originalContent = content;
|
||||
|
||||
// Save original GitHub links
|
||||
const repoUrlRegex = /github\.com\/Draculabo\/AntigravityManager/gi;
|
||||
const tempUrlMarker = '___TEMP_GITHUB_URL___';
|
||||
content = content.replace(repoUrlRegex, tempUrlMarker);
|
||||
|
||||
// 1. Strings exact match "Antigravity" -> "Gemini Nexus"
|
||||
content = content.replace(/['"]Antigravity['"]/g, (match) => {
|
||||
return match[0] + 'Gemini Nexus' + match[0];
|
||||
});
|
||||
|
||||
// 2. String "Antigravity " -> "Gemini Nexus "
|
||||
content = content.replace(/Antigravity /g, 'Gemini Nexus ');
|
||||
|
||||
// 3. String " Antigravity" -> " Gemini Nexus"
|
||||
content = content.replace(/ Antigravity/g, ' Gemini Nexus');
|
||||
|
||||
// 4. Identifiers Antigravity -> GeminiNexus
|
||||
content = content.replace(/Antigravity/g, 'GeminiNexus');
|
||||
|
||||
// 5. Identifiers antigravity -> geminiNexus
|
||||
content = content.replace(/antigravity/g, 'geminiNexus');
|
||||
|
||||
// Restore GitHub links
|
||||
content = content.replace(
|
||||
new RegExp(tempUrlMarker, 'g'),
|
||||
'github.com/Draculabo/AntigravityManager',
|
||||
);
|
||||
|
||||
// Cleanup any double replacements
|
||||
content = content.replace(/GeminiNexus Nexus/g, 'Gemini Nexus');
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(fullPath, content, 'utf-8');
|
||||
console.log(`Updated file content: ${fullPath}`);
|
||||
}
|
||||
|
||||
// Rename file if it contains 'antigravity'
|
||||
if (file.toLowerCase().includes('antigravity')) {
|
||||
const newName = file.replace(/antigravity/gi, (match) => {
|
||||
if (match === 'antigravity') return 'geminiNexus'; // For files like antigravityVersion.ts -> geminiNexusVersion.ts
|
||||
if (match === 'Antigravity') return 'GeminiNexus';
|
||||
return 'geminiNexus';
|
||||
});
|
||||
const newPath = path.join(dir, newName);
|
||||
fs.renameSync(fullPath, newPath);
|
||||
console.log(`Renamed file: ${fullPath} -> ${newPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Starting global deep rename of Antigravity to GeminiNexus...');
|
||||
walkAndReplace(projectRoot);
|
||||
console.log('Rename complete.');
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
set PYTHONPATH=%~dp0
|
||||
python -m cli.main %*
|
||||
@@ -41,7 +41,9 @@ function color(code, text) {
|
||||
}
|
||||
|
||||
function header(text) {
|
||||
console.log(`\n${color(BOLD + MAGENTA, '══')} ${color(BOLD, text)} ${color(BOLD + MAGENTA, '══')}`);
|
||||
console.log(
|
||||
`\n${color(BOLD + MAGENTA, '══')} ${color(BOLD, text)} ${color(BOLD + MAGENTA, '══')}`,
|
||||
);
|
||||
}
|
||||
|
||||
function success(text) {
|
||||
@@ -145,10 +147,13 @@ function checkWindows() {
|
||||
|
||||
for (const vswhere of vswherePaths) {
|
||||
try {
|
||||
const output = execSync(`"${vswhere}" -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`, {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
const output = execSync(
|
||||
`"${vswhere}" -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`,
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
encoding: 'utf8',
|
||||
},
|
||||
).trim();
|
||||
|
||||
if (output) {
|
||||
vsFound = true;
|
||||
@@ -163,10 +168,9 @@ function checkWindows() {
|
||||
// Check registry fallback
|
||||
if (!vsFound) {
|
||||
try {
|
||||
execSync(
|
||||
'reg query "HKLM\\SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VS7" /v 17.0',
|
||||
{ stdio: 'ignore' },
|
||||
);
|
||||
execSync('reg query "HKLM\\SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VS7" /v 17.0', {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
vsFound = true;
|
||||
success('Visual Studio 2022 found (registry)');
|
||||
} catch {
|
||||
@@ -198,7 +202,10 @@ function checkMacOS() {
|
||||
const results = { ok: true, missing: [] };
|
||||
|
||||
try {
|
||||
const xcodePath = execSync('xcode-select -p', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
|
||||
const xcodePath = execSync('xcode-select -p', {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
if (xcodePath) {
|
||||
success(`Xcode CLT found at: ${xcodePath}`);
|
||||
}
|
||||
@@ -323,14 +330,26 @@ async function installWindows() {
|
||||
// Check if winget is available
|
||||
if (!commandExists('winget')) {
|
||||
console.log('');
|
||||
console.log(color(RED, ' winget not found. Please install Visual Studio Build Tools manually:'));
|
||||
console.log(color(CYAN, ' https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022'));
|
||||
console.log(
|
||||
color(RED, ' winget not found. Please install Visual Studio Build Tools manually:'),
|
||||
);
|
||||
console.log(
|
||||
color(
|
||||
CYAN,
|
||||
' https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022',
|
||||
),
|
||||
);
|
||||
console.log('');
|
||||
console.log(' During installation, select:');
|
||||
console.log(color(BOLD, ' "Desktop development with C++" workload'));
|
||||
console.log('');
|
||||
console.log(' Or install via PowerShell (as Administrator):');
|
||||
console.log(color(DIM, ' winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"'));
|
||||
console.log(
|
||||
color(
|
||||
DIM,
|
||||
' winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"',
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -398,7 +417,9 @@ async function installLinux() {
|
||||
}
|
||||
|
||||
const status = await runCommand('sudo', [
|
||||
'apt-get', 'install', '-y',
|
||||
'apt-get',
|
||||
'install',
|
||||
'-y',
|
||||
'build-essential',
|
||||
'python3',
|
||||
'libsecret-1-dev',
|
||||
@@ -421,7 +442,10 @@ async function installLinux() {
|
||||
}
|
||||
|
||||
const status = await runCommand('sudo', [
|
||||
'pacman', '-S', '--needed', '--noconfirm',
|
||||
'pacman',
|
||||
'-S',
|
||||
'--needed',
|
||||
'--noconfirm',
|
||||
'base-devel',
|
||||
'python',
|
||||
'libsecret',
|
||||
@@ -443,7 +467,9 @@ async function installLinux() {
|
||||
}
|
||||
|
||||
const status = await runCommand('sudo', [
|
||||
'dnf', 'install', '-y',
|
||||
'dnf',
|
||||
'install',
|
||||
'-y',
|
||||
'gcc-c++',
|
||||
'make',
|
||||
'python3',
|
||||
@@ -471,7 +497,9 @@ async function installLinux() {
|
||||
console.log(' - libsecret development headers (for keytar)');
|
||||
console.log(' - pkg-config');
|
||||
console.log('');
|
||||
console.log(' Install them using your distribution package manager, then run npm install again.');
|
||||
console.log(
|
||||
' Install them using your distribution package manager, then run npm install again.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -520,7 +548,9 @@ async function main() {
|
||||
|
||||
// ── Missing tools ──
|
||||
console.log('');
|
||||
error('Some build tools are missing. Native modules like better-sqlite3 and keytar require compilation.');
|
||||
error(
|
||||
'Some build tools are missing. Native modules like better-sqlite3 and keytar require compilation.',
|
||||
);
|
||||
|
||||
if (checkOnly) {
|
||||
console.log('');
|
||||
@@ -528,7 +558,9 @@ async function main() {
|
||||
warn('Run "npm run setup" to install them automatically, or install manually:');
|
||||
printManualInstructions(platform, results.missing);
|
||||
console.log('');
|
||||
dim('npm install succeeded, but npm start / electron-rebuild will fail until build tools are installed.');
|
||||
dim(
|
||||
'npm install succeeded, but npm start / electron-rebuild will fail until build tools are installed.',
|
||||
);
|
||||
// Don't exit with error - allow npm install to succeed (prebuilt binaries may work)
|
||||
return;
|
||||
}
|
||||
@@ -572,13 +604,23 @@ function printManualInstructions(platform, missing) {
|
||||
switch (platform) {
|
||||
case 'win32':
|
||||
console.log(' 1. Download Visual Studio 2022 Build Tools:');
|
||||
console.log(color(CYAN, ' https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022'));
|
||||
console.log(
|
||||
color(
|
||||
CYAN,
|
||||
' https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022',
|
||||
),
|
||||
);
|
||||
console.log('');
|
||||
console.log(' 2. Run the installer and select:');
|
||||
console.log(color(BOLD, ' "Desktop development with C++"'));
|
||||
console.log('');
|
||||
console.log(' 3. Or install via PowerShell (as Administrator):');
|
||||
console.log(color(DIM, ' winget install Microsoft.VisualStudio.2022.BuildTools --override "--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"'));
|
||||
console.log(
|
||||
color(
|
||||
DIM,
|
||||
' winget install Microsoft.VisualStudio.2022.BuildTools --override "--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"',
|
||||
),
|
||||
);
|
||||
console.log('');
|
||||
console.log(' 4. Restart your terminal after installation.');
|
||||
break;
|
||||
@@ -594,13 +636,22 @@ function printManualInstructions(platform, missing) {
|
||||
const distro = detectLinuxDistro();
|
||||
if (distro === 'debian') {
|
||||
console.log(' Run:');
|
||||
console.log(color(DIM, ' sudo apt-get install -y build-essential python3 libsecret-1-dev pkg-config'));
|
||||
console.log(
|
||||
color(
|
||||
DIM,
|
||||
' sudo apt-get install -y build-essential python3 libsecret-1-dev pkg-config',
|
||||
),
|
||||
);
|
||||
} else if (distro === 'arch') {
|
||||
console.log(' Run:');
|
||||
console.log(color(DIM, ' sudo pacman -S --needed base-devel python libsecret pkg-config'));
|
||||
console.log(
|
||||
color(DIM, ' sudo pacman -S --needed base-devel python libsecret pkg-config'),
|
||||
);
|
||||
} else if (distro === 'fedora') {
|
||||
console.log(' Run:');
|
||||
console.log(color(DIM, ' sudo dnf install -y gcc-c++ make python3 libsecret-devel pkg-config'));
|
||||
console.log(
|
||||
color(DIM, ' sudo dnf install -y gcc-c++ make python3 libsecret-devel pkg-config'),
|
||||
);
|
||||
} else {
|
||||
console.log(' Install: C/C++ compiler, make, python3, libsecret-dev, pkg-config');
|
||||
}
|
||||
|
||||
@@ -114,8 +114,10 @@ export const AccountCard: React.FC<AccountCardProps> = ({
|
||||
<Card
|
||||
className={cn(
|
||||
'relative overflow-hidden transition-all duration-200',
|
||||
|
||||
isCurrent ? 'border-primary/50 bg-primary/10' : 'hover:bg-accent/10 cursor-pointer border-white/5',
|
||||
|
||||
isCurrent
|
||||
? 'border-primary/50 bg-primary/10'
|
||||
: 'hover:bg-accent/10 cursor-pointer border-white/5',
|
||||
(isSwitching || isDeleting) && 'pointer-events-none opacity-60',
|
||||
)}
|
||||
onClick={() => !isCurrent && !isSwitching && !isDeleting && onSwitch(account.id)}
|
||||
|
||||
@@ -235,54 +235,51 @@ export function CloudAccountCard({
|
||||
const renderQuotaModelGroup = (title: string, models: ModelQuotaEntry[]) => {
|
||||
if (models.length === 0) return null;
|
||||
return (
|
||||
<div key={title} className="space-y-1">
|
||||
<div className="flex items-center gap-1.5 px-2 py-1">
|
||||
<span className="text-muted-foreground/70 text-[10px] font-bold tracking-wider uppercase">
|
||||
{title}
|
||||
</span>
|
||||
<div className="h-[1px] flex-1 bg-white/[0.04]" />
|
||||
</div>
|
||||
{models.map(([modelName, info]) => (
|
||||
<div
|
||||
key={modelName}
|
||||
className="group/item grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-lg border border-transparent px-2 py-1.5 text-[13px] transition-all hover:bg-white/[0.03]"
|
||||
>
|
||||
<span
|
||||
className="text-muted-foreground group-hover/item:text-foreground min-w-0 truncate font-semibold"
|
||||
title={modelName}
|
||||
<div key={title} className="mt-1 space-y-1">
|
||||
<h4 className="text-muted-foreground/60 mb-1.5 px-1 text-[10px] font-bold tracking-widest uppercase">
|
||||
{title}
|
||||
</h4>
|
||||
<div className="flex flex-col gap-1">
|
||||
{models.map(([modelName, info]) => (
|
||||
<div
|
||||
key={modelName}
|
||||
className="group/item grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-md px-1.5 py-1 text-[13px] transition-colors hover:bg-white/[0.03]"
|
||||
>
|
||||
{formatModelDisplayName(modelName)}
|
||||
</span>
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
<span
|
||||
className="text-muted-foreground text-[9px] leading-none opacity-80"
|
||||
title={formatResetTimeTitleText(info.resetTime)}
|
||||
className="text-muted-foreground group-hover/item:text-foreground truncate font-medium transition-colors"
|
||||
title={modelName}
|
||||
>
|
||||
{formatResetTimeLabelText(info.resetTime)}
|
||||
{formatModelDisplayName(modelName)}
|
||||
</span>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`font-mono text-xs leading-none font-bold ${getQuotaTextColorClass(info.percentage)}`}
|
||||
className="text-muted-foreground/70 min-w-[50px] text-right text-[9px]"
|
||||
title={formatResetTimeTitleText(info.resetTime)}
|
||||
>
|
||||
{formatResetTimeLabelText(info.resetTime)}
|
||||
</span>
|
||||
<span
|
||||
className={`w-9 text-right font-mono text-xs font-bold ${getQuotaTextColorClass(info.percentage)}`}
|
||||
>
|
||||
{info.percentage}%
|
||||
</span>
|
||||
<div className="bg-muted h-1 w-16 overflow-hidden rounded-full">
|
||||
<div className="h-1.5 w-12 overflow-hidden rounded-full bg-white/[0.04]">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColorClass(info.percentage)}`}
|
||||
className={`h-full rounded-full transition-all duration-500 ease-out ${getQuotaBarColorClass(info.percentage)}`}
|
||||
style={{ width: `${clampQuotaPercentage(info.percentage)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const emptyQuotaState = (
|
||||
<div className="text-muted-foreground flex flex-col items-center justify-center py-4">
|
||||
<Box className="mb-2 h-8 w-8 opacity-20" />
|
||||
<div className="text-muted-foreground flex flex-col items-center justify-center py-6">
|
||||
<Box className="mb-2 h-6 w-6 opacity-20" />
|
||||
<span className="text-xs">{t('cloud.card.noQuota')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -290,18 +287,20 @@ export function CloudAccountCard({
|
||||
const providerStats = providerGroupingsEnabled ? getAccountStats(account) : null;
|
||||
const providerGroupedQuotaSection =
|
||||
providerStats && providerStats.visibleModels > 0 ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between rounded-lg bg-white/[0.03] px-3 py-1.5 text-xs">
|
||||
<span className="font-medium">{t('settings.providerGroupings.overall')}</span>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between rounded-lg border border-white/[0.04] bg-white/[0.02] px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs font-medium">
|
||||
{t('settings.providerGroupings.overall')}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`font-mono font-bold ${getQuotaTextColorClass(providerStats.overallPercentage)}`}
|
||||
className={`font-mono text-xs font-bold ${getQuotaTextColorClass(providerStats.overallPercentage)}`}
|
||||
>
|
||||
{formatQuotaLabel(providerStats.overallPercentage)}
|
||||
</span>
|
||||
<div className="bg-muted h-1.5 w-16 overflow-hidden rounded-full">
|
||||
<div className="h-1.5 w-16 overflow-hidden rounded-full bg-white/[0.04]">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColorClass(providerStats.overallPercentage)}`}
|
||||
className={`h-full rounded-full transition-all duration-500 ease-out ${getQuotaBarColorClass(providerStats.overallPercentage)}`}
|
||||
style={{
|
||||
width: `${clampQuotaPercentage(providerStats.overallPercentage)}%`,
|
||||
}}
|
||||
@@ -310,7 +309,7 @@ export function CloudAccountCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="space-y-3">
|
||||
{providerStats.providers.map((group) => (
|
||||
<ProviderGroup
|
||||
key={group.providerKey}
|
||||
@@ -326,7 +325,7 @@ export function CloudAccountCard({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
emptyQuotaState
|
||||
);
|
||||
@@ -343,17 +342,17 @@ export function CloudAccountCard({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`group flex h-full flex-col overflow-hidden rounded-xl border border-white/[0.06] bg-card transition-all duration-200 hover:border-white/[0.12] ${isSelected ? 'ring-accent/30 border-accent/40 ring-2' : ''}`}
|
||||
className={`group bg-card relative flex h-full flex-col overflow-hidden rounded-xl border transition-all duration-300 hover:shadow-md ${isSelected ? 'border-primary/50 ring-primary/50 ring-1' : 'border-white/[0.06] hover:border-white/[0.15]'}`}
|
||||
>
|
||||
<CardHeader className="relative flex flex-row items-center gap-4 space-y-0 pb-2">
|
||||
<CardHeader className="flex flex-row items-start gap-4 space-y-0 p-5 pb-4">
|
||||
{onToggleSelection && (
|
||||
<div
|
||||
className={`absolute top-2 left-2 z-10 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} bg-background/90 rounded-full p-2 transition-opacity`}
|
||||
className={`absolute top-3 left-3 z-10 transition-opacity duration-200 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) => onToggleSelection(account.id, checked as boolean)}
|
||||
className="h-5 w-5 border-2"
|
||||
className="bg-background/80 data-[state=checked]:bg-primary h-4 w-4 backdrop-blur-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -362,188 +361,193 @@ export function CloudAccountCard({
|
||||
<img
|
||||
src={account.avatar_url}
|
||||
alt={account.name || ''}
|
||||
className="bg-muted h-10 w-10 rounded-full border"
|
||||
className="h-11 w-11 shrink-0 rounded-full border border-white/[0.08] bg-white/[0.02] object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/[0.06] text-muted-foreground">
|
||||
<div className="text-muted-foreground flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.02] text-lg font-medium">
|
||||
{account.name?.[0]?.toUpperCase() || 'A'}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<CardTitle className="truncate text-base font-semibold">
|
||||
|
||||
<div className="flex flex-1 flex-col gap-0.5 overflow-hidden pt-0.5">
|
||||
<CardTitle className="truncate text-base leading-tight font-semibold">
|
||||
{account.name || t('cloud.card.unknown')}
|
||||
</CardTitle>
|
||||
<CardDescription className="truncate text-xs">{account.email}</CardDescription>
|
||||
<CardDescription className="text-muted-foreground/80 truncate text-xs font-medium">
|
||||
{account.email}
|
||||
</CardDescription>
|
||||
|
||||
{shouldShowAiCredits && aiCredits && (
|
||||
<div className="mt-1 flex items-center gap-1 text-[10px] font-medium text-blue-500">
|
||||
<span>
|
||||
{t('cloud.card.aiCreditsValue', {
|
||||
amount: formatAiCreditsAmount(aiCredits.credits),
|
||||
})}
|
||||
</span>
|
||||
{aiCredits.expiryDate && (
|
||||
<span className="text-muted-foreground opacity-70">
|
||||
·{' '}
|
||||
{t('cloud.card.creditsExpiry', {
|
||||
date: formatCreditsExpiry(aiCredits.expiryDate),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{allModelEntries.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer rounded-full">
|
||||
{(() => {
|
||||
const hiddenCount = allModelEntries.filter(
|
||||
([modelName]) => config?.model_visibility?.[modelName] === false,
|
||||
).length;
|
||||
return hiddenCount > 0 ? (
|
||||
<EyeOff className="text-muted-foreground h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
);
|
||||
})()}
|
||||
<span className="sr-only">{t('cloud.card.modelVisibility')}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64" align="end">
|
||||
<DropdownMenuLabel>{t('cloud.card.modelVisibility')}</DropdownMenuLabel>
|
||||
<div className="max-h-64 overflow-auto px-2 py-1">
|
||||
{allModelEntries.map(([modelName]) => {
|
||||
const isVisible = config?.model_visibility?.[modelName] !== false;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={modelName}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
checked={isVisible}
|
||||
onCheckedChange={(checked) => {
|
||||
if (config) {
|
||||
const newVisibility = { ...config.model_visibility };
|
||||
newVisibility[modelName] = checked as boolean;
|
||||
saveConfig({ ...config, model_visibility: newVisibility });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="truncate text-xs" title={modelName}>
|
||||
{formatModelDisplayName(modelName)}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer rounded-full">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
<span className="sr-only">Menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t('cloud.card.actions')}</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => onSwitch(account.id)} disabled={isSwitching}>
|
||||
<Power className="mr-2 h-4 w-4" />
|
||||
{t('cloud.card.useAccount')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onRefresh(account.id)} disabled={isRefreshing}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t('cloud.card.refresh')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onManageIdentity(account.id)}>
|
||||
<Fingerprint className="mr-2 h-4 w-4" />
|
||||
{t('cloud.card.identityProfile')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete(account.id)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
{t('cloud.card.delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 pb-2">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
||||
<Badge
|
||||
variant={
|
||||
account.status === 'rate_limited' || account.status === 'expired'
|
||||
? 'destructive'
|
||||
: 'outline'
|
||||
}
|
||||
className="text-xs"
|
||||
className="h-4 border-white/[0.1] bg-white/[0.02] px-1.5 py-0 text-[9px] uppercase"
|
||||
>
|
||||
{account.provider.toUpperCase()}
|
||||
{account.provider}
|
||||
</Badge>
|
||||
{account.is_active && (
|
||||
<Badge variant="default" className="bg-emerald-500/15 text-emerald-400 border-emerald-500/20 text-xs hover:bg-emerald-500/20">
|
||||
<Badge
|
||||
variant="default"
|
||||
className="h-4 border border-emerald-500/20 bg-emerald-500/10 px-1.5 py-0 text-[9px] text-emerald-400 uppercase hover:bg-emerald-500/20"
|
||||
>
|
||||
{t('cloud.card.active')}
|
||||
</Badge>
|
||||
)}
|
||||
{validationBlockedStatusLabel && (
|
||||
<span className="text-destructive text-xs font-medium">
|
||||
{validationBlockedStatusLabel}
|
||||
</span>
|
||||
{hasHighTier && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-4 animate-pulse border-blue-500/20 bg-blue-500/10 px-1.5 py-0 text-[9px] text-blue-400 uppercase"
|
||||
>
|
||||
{t('cloud.card.gemini3Ready')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasHighTier && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="animate-pulse border-blue-500/20 bg-blue-500/10 text-[10px] text-blue-500"
|
||||
>
|
||||
{t('cloud.card.gemini3Ready')}
|
||||
</Badge>
|
||||
{validationBlockedStatusLabel && (
|
||||
<span className="text-destructive mt-1 text-xs font-medium">
|
||||
{validationBlockedStatusLabel}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{account.is_active ? (
|
||||
<Button variant="ghost" size="sm" disabled className="text-emerald-400 opacity-100">
|
||||
<Power className="mr-1 h-3 w-3" />
|
||||
{t('cloud.card.active')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => onSwitch(account.id)}
|
||||
disabled={isSwitching}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{isSwitching ? (
|
||||
<RefreshCw className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Power className="mr-1 h-3 w-3" />
|
||||
{shouldShowAiCredits && aiCredits && (
|
||||
<div className="mt-2 flex w-fit items-center gap-1.5 rounded-md border border-blue-500/20 bg-blue-500/10 px-2 py-1">
|
||||
<span className="text-[10px] font-semibold text-blue-400">
|
||||
{t('cloud.card.aiCreditsValue', {
|
||||
amount: formatAiCreditsAmount(aiCredits.credits),
|
||||
})}
|
||||
</span>
|
||||
{aiCredits.expiryDate && (
|
||||
<>
|
||||
<span className="h-2.5 w-[1px] bg-blue-500/30"></span>
|
||||
<span className="text-[9px] font-medium text-blue-400/80">
|
||||
{t('cloud.card.creditsExpiry', {
|
||||
date: formatCreditsExpiry(aiCredits.expiryDate),
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{t('cloud.card.use')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{allModelEntries.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground/70 hover:text-foreground h-7 w-7"
|
||||
>
|
||||
{(() => {
|
||||
const hiddenCount = allModelEntries.filter(
|
||||
([modelName]) => config?.model_visibility?.[modelName] === false,
|
||||
).length;
|
||||
return hiddenCount > 0 ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
);
|
||||
})()}
|
||||
<span className="sr-only">{t('cloud.card.modelVisibility')}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64" align="end">
|
||||
<DropdownMenuLabel className="text-xs">
|
||||
{t('cloud.card.modelVisibility')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="max-h-[300px] overflow-y-auto p-1">
|
||||
{allModelEntries.map(([modelName]) => {
|
||||
const isVisible = config?.model_visibility?.[modelName] !== false;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={modelName}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="flex cursor-pointer items-center gap-2.5 rounded-sm px-2 py-1.5"
|
||||
>
|
||||
<Checkbox
|
||||
checked={isVisible}
|
||||
onCheckedChange={(checked) => {
|
||||
if (config) {
|
||||
const newVisibility = { ...config.model_visibility };
|
||||
newVisibility[modelName] = checked as boolean;
|
||||
saveConfig({ ...config, model_visibility: newVisibility });
|
||||
}
|
||||
}}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
<span className="truncate text-xs font-medium" title={modelName}>
|
||||
{formatModelDisplayName(modelName)}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground/70 hover:text-foreground h-7 w-7"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
<span className="sr-only">Menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onSwitch(account.id)}
|
||||
disabled={isSwitching}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Power className="mr-2 h-4 w-4" />
|
||||
<span className="font-medium">{t('cloud.card.useAccount')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onRefresh(account.id)}
|
||||
disabled={isRefreshing}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
<span>{t('cloud.card.refresh')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onManageIdentity(account.id)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Fingerprint className="mr-2 h-4 w-4" />
|
||||
<span>{t('cloud.card.identityProfile')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete(account.id)}
|
||||
className="text-destructive focus:bg-destructive/10 focus:text-destructive cursor-pointer"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
<span>{t('cloud.card.delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 px-5 pb-5">
|
||||
<div className="space-y-4">
|
||||
{providerGroupingsEnabled ? (
|
||||
providerGroupedQuotaSection
|
||||
) : hasVisibleQuotaModels ? (
|
||||
<div className="space-y-3">
|
||||
{renderQuotaModelGroup(t('cloud.card.groupGoogleGemini'), geminiModels)}
|
||||
<div className="pt-1" />
|
||||
{renderQuotaModelGroup(t('cloud.card.groupAnthropicClaude'), claudeModels)}
|
||||
{renderQuotaModelGroup('Google Gemini', geminiModels)}
|
||||
{claudeModels.length > 0 && renderQuotaModelGroup('Anthropic Claude', claudeModels)}
|
||||
</div>
|
||||
) : (
|
||||
emptyQuotaState
|
||||
@@ -551,11 +555,36 @@ export function CloudAccountCard({
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="justify-center border-t border-white/[0.04] p-2 px-4 text-xs text-muted-foreground">
|
||||
<span>
|
||||
<CardFooter className="flex items-center justify-between border-t border-white/[0.04] bg-white/[0.01] px-5 py-3">
|
||||
<span className="text-muted-foreground/70 text-[11px] font-medium">
|
||||
{t('cloud.card.used')}{' '}
|
||||
{formatDistanceToNow(account.last_used * 1000, { addSuffix: true })}
|
||||
</span>
|
||||
|
||||
{account.is_active ? (
|
||||
<span className="flex items-center gap-1.5 text-xs font-semibold text-emerald-400">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500"></span>
|
||||
</span>
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onSwitch(account.id)}
|
||||
disabled={isSwitching}
|
||||
className="hover:bg-primary/10 hover:text-primary h-7 cursor-pointer px-3 text-xs font-medium transition-colors"
|
||||
>
|
||||
{isSwitching ? (
|
||||
<RefreshCw className="mr-1.5 h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Power className="mr-1.5 h-3 w-3" />
|
||||
)}
|
||||
{t('cloud.card.use')}
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
@@ -613,22 +642,22 @@ export function CompactCloudAccountCard({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="group flex items-center gap-3 rounded-xl border border-white/[0.06] bg-card px-3 py-2 transition-all duration-200 hover:border-white/[0.12]">
|
||||
<div className="group bg-card flex items-center gap-3 rounded-xl border border-white/[0.04] px-4 py-3 transition-all duration-200 hover:border-white/[0.1] hover:shadow-sm">
|
||||
{account.avatar_url ? (
|
||||
<img
|
||||
src={account.avatar_url}
|
||||
alt={account.name || ''}
|
||||
className="bg-muted h-7 w-7 rounded-full border"
|
||||
className="h-8 w-8 shrink-0 rounded-full border border-white/[0.08] bg-white/[0.02] object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-white/[0.06] text-xs font-semibold text-muted-foreground">
|
||||
<div className="text-muted-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.02] text-xs font-semibold">
|
||||
{account.name?.[0]?.toUpperCase() || 'A'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-[13px] font-semibold">
|
||||
<div className="mb-0.5 flex items-center gap-2">
|
||||
<span className="text-foreground truncate text-[13px] leading-none font-semibold">
|
||||
{account.name || t('cloud.card.unknown')}
|
||||
</span>
|
||||
<Badge
|
||||
@@ -637,63 +666,62 @@ export function CompactCloudAccountCard({
|
||||
? 'destructive'
|
||||
: 'outline'
|
||||
}
|
||||
className="shrink-0 text-[10px]"
|
||||
className="h-4 shrink-0 border-white/[0.1] bg-white/[0.02] px-1.5 py-0 text-[9px] uppercase"
|
||||
>
|
||||
{account.provider.toUpperCase()}
|
||||
{account.provider}
|
||||
</Badge>
|
||||
{account.is_active && (
|
||||
<Badge
|
||||
variant="default"
|
||||
className="shrink-0 bg-emerald-500/15 text-emerald-400 border-emerald-500/20 text-[10px] hover:bg-emerald-500/20"
|
||||
className="h-4 shrink-0 border border-emerald-500/20 bg-emerald-500/10 px-1.5 py-0 text-[9px] text-emerald-400 uppercase"
|
||||
>
|
||||
{t('cloud.card.active')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground flex items-center gap-3 text-xs">
|
||||
<div className="text-muted-foreground/80 flex items-center gap-3 text-[11px] font-medium">
|
||||
<span className="truncate">{account.email}</span>
|
||||
{validationBlockedStatusLabel && (
|
||||
<span className="text-destructive shrink-0 font-medium">
|
||||
{validationBlockedStatusLabel}
|
||||
</span>
|
||||
<span className="text-destructive shrink-0">{validationBlockedStatusLabel}</span>
|
||||
)}
|
||||
|
||||
{shouldShowAiCredits && aiCredits && (
|
||||
<span className="shrink-0 text-blue-500">
|
||||
{t('cloud.card.aiCreditsValue', {
|
||||
amount: formatAiCreditsAmount(aiCredits.credits),
|
||||
})}
|
||||
<span className="flex shrink-0 items-center gap-1.5 text-blue-400">
|
||||
<span>
|
||||
{t('cloud.card.aiCreditsValue', {
|
||||
amount: formatAiCreditsAmount(aiCredits.credits),
|
||||
})}
|
||||
</span>
|
||||
{aiCredits.expiryDate && (
|
||||
<span className="text-muted-foreground">
|
||||
{' '}
|
||||
·{' '}
|
||||
{t('cloud.card.creditsExpiry', {
|
||||
date: formatCreditsExpiry(aiCredits.expiryDate),
|
||||
})}
|
||||
</span>
|
||||
<>
|
||||
<span className="h-2.5 w-[1px] bg-blue-500/30"></span>
|
||||
<span className="text-blue-400/70">
|
||||
{t('cloud.card.creditsExpiry', {
|
||||
date: formatCreditsExpiry(aiCredits.expiryDate),
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{compactModels.length > 0 && (
|
||||
<div className="mt-1 flex items-center gap-1">
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
{compactModels.map(([modelName, info]) => (
|
||||
<TooltipProvider key={modelName}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="bg-muted h-1.5 w-12 overflow-hidden rounded-full">
|
||||
<div className="h-1.5 w-10 overflow-hidden rounded-full bg-white/[0.04]">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColorClass(info.percentage)}`}
|
||||
className={`h-full rounded-full transition-all duration-300 ease-out ${getQuotaBarColorClass(info.percentage)}`}
|
||||
style={{ width: `${clampQuotaPercentage(info.percentage)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">
|
||||
{formatModelDisplayName(modelName)}: {info.percentage}%
|
||||
</p>
|
||||
<TooltipContent side="bottom" className="text-xs font-medium">
|
||||
{formatModelDisplayName(modelName)}: {info.percentage}%
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -706,29 +734,36 @@ export function CompactCloudAccountCard({
|
||||
{allModelEntries.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 cursor-pointer rounded-full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground/70 hover:text-foreground h-8 w-8"
|
||||
>
|
||||
{(() => {
|
||||
const hiddenCount = allModelEntries.filter(
|
||||
([modelName]) => config?.model_visibility?.[modelName] === false,
|
||||
).length;
|
||||
return hiddenCount > 0 ? (
|
||||
<EyeOff className="text-muted-foreground h-3.5 w-3.5" />
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
<Eye className="h-4 w-4" />
|
||||
);
|
||||
})()}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64" align="end">
|
||||
<DropdownMenuLabel>{t('cloud.card.modelVisibility')}</DropdownMenuLabel>
|
||||
<div className="max-h-64 overflow-auto px-2 py-1">
|
||||
<DropdownMenuContent className="w-56" align="end">
|
||||
<DropdownMenuLabel className="text-xs">
|
||||
{t('cloud.card.modelVisibility')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="max-h-[250px] overflow-y-auto p-1">
|
||||
{allModelEntries.map(([modelName]) => {
|
||||
const isVisible = config?.model_visibility?.[modelName] !== false;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={modelName}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
className="flex cursor-pointer items-center gap-2.5 rounded-sm px-2 py-1.5"
|
||||
>
|
||||
<Checkbox
|
||||
checked={isVisible}
|
||||
@@ -739,8 +774,9 @@ export function CompactCloudAccountCard({
|
||||
saveConfig({ ...config, model_visibility: newVisibility });
|
||||
}
|
||||
}}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
<span className="truncate text-xs" title={modelName}>
|
||||
<span className="truncate text-xs font-medium" title={modelName}>
|
||||
{formatModelDisplayName(modelName)}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
@@ -752,27 +788,27 @@ export function CompactCloudAccountCard({
|
||||
)}
|
||||
|
||||
{account.is_active ? (
|
||||
<div className="flex h-8 items-center px-2">
|
||||
<span className="flex items-center gap-1.5 text-[11px] font-bold text-emerald-400">
|
||||
<span className="relative flex h-1.5 w-1.5">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500"></span>
|
||||
</span>
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled
|
||||
className="h-7 text-xs text-emerald-400 opacity-100"
|
||||
>
|
||||
<Power className="mr-1 h-3 w-3" />
|
||||
{t('cloud.card.active')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => onSwitch(account.id)}
|
||||
disabled={isSwitching}
|
||||
className="h-7 cursor-pointer text-xs"
|
||||
className="hover:bg-primary/10 hover:text-primary h-8 cursor-pointer px-3 text-xs font-medium transition-colors"
|
||||
>
|
||||
{isSwitching ? (
|
||||
<RefreshCw className="h-3 w-3 animate-spin" />
|
||||
<RefreshCw className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Power className="mr-1 h-3 w-3" />
|
||||
<Power className="mr-1.5 h-3.5 w-3.5" />
|
||||
)}
|
||||
{t('cloud.card.use')}
|
||||
</Button>
|
||||
@@ -780,35 +816,47 @@ export function CompactCloudAccountCard({
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 cursor-pointer rounded-full">
|
||||
<MoreVertical className="h-3.5 w-3.5" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground/70 hover:text-foreground h-8 w-8"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
<span className="sr-only">Menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t('cloud.card.actions')}</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => onSwitch(account.id)} disabled={isSwitching}>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onSwitch(account.id)}
|
||||
disabled={isSwitching}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Power className="mr-2 h-4 w-4" />
|
||||
{t('cloud.card.useAccount')}
|
||||
<span className="font-medium">{t('cloud.card.useAccount')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onRefresh(account.id)} disabled={isRefreshing}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onRefresh(account.id)}
|
||||
disabled={isRefreshing}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t('cloud.card.refresh')}
|
||||
<span>{t('cloud.card.refresh')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onManageIdentity(account.id)}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onManageIdentity(account.id)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Fingerprint className="mr-2 h-4 w-4" />
|
||||
{t('cloud.card.identityProfile')}
|
||||
<span>{t('cloud.card.identityProfile')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete(account.id)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
className="text-destructive focus:bg-destructive/10 focus:text-destructive cursor-pointer"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
{t('cloud.card.delete')}
|
||||
<span>{t('cloud.card.delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -134,7 +134,10 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => { void refreshProfiles(); }, 0); }, [open, refreshProfiles]);
|
||||
setTimeout(() => {
|
||||
void refreshProfiles();
|
||||
}, 0);
|
||||
}, [open, refreshProfiles]);
|
||||
|
||||
const runAction = async (key: string, action: () => Promise<void>) => {
|
||||
if (actionLockRef.current) {
|
||||
@@ -343,7 +346,10 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
|
||||
|
||||
{!showLoadingPlaceholder
|
||||
? history.map((version) => (
|
||||
<div key={version.id} className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-3">
|
||||
<div
|
||||
key={version.id}
|
||||
className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-3"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium">{version.label}</div>
|
||||
|
||||
@@ -48,8 +48,12 @@ export function ModelVisibilitySettings() {
|
||||
|
||||
// Initialize model visibility and provider groupings from config
|
||||
useEffect(() => {
|
||||
if (config?.model_visibility) { setTimeout(() => setModelVisibilityMap(config.model_visibility), 0); }
|
||||
if (config?.provider_groupings_enabled !== undefined) { setTimeout(() => setProviderGroupingEnabled(config.provider_groupings_enabled), 0); }
|
||||
if (config?.model_visibility) {
|
||||
setTimeout(() => setModelVisibilityMap(config.model_visibility), 0);
|
||||
}
|
||||
if (config?.provider_groupings_enabled !== undefined) {
|
||||
setTimeout(() => setProviderGroupingEnabled(config.provider_groupings_enabled), 0);
|
||||
}
|
||||
}, [config?.model_visibility, config?.provider_groupings_enabled]);
|
||||
|
||||
// Get all unique models from all accounts
|
||||
@@ -184,7 +188,7 @@ export function ModelVisibilitySettings() {
|
||||
return (
|
||||
<div
|
||||
key={modelId}
|
||||
className="flex items-center space-x-3 rounded-lg p-2 hover:bg-white/[0.03] transition-colors"
|
||||
className="flex items-center space-x-3 rounded-lg p-2 transition-colors hover:bg-white/[0.03]"
|
||||
>
|
||||
<Checkbox
|
||||
id={`model-${modelId}`}
|
||||
|
||||
@@ -3,15 +3,9 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { isProcessRunning, startGeminiNexus, closeGeminiNexus } from '@/actions/process';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Play, Square, Loader2, Power } from 'lucide-react';
|
||||
import { Play, Square, Loader2 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
|
||||
interface StatusBarProps {
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
export const StatusBar: React.FC<StatusBarProps> = ({ isCollapsed = false }) => {
|
||||
export const StatusBar: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -44,112 +38,48 @@ export const StatusBar: React.FC<StatusBarProps> = ({ isCollapsed = false }) =>
|
||||
};
|
||||
|
||||
const isPending = startMutation.isPending || stopMutation.isPending;
|
||||
|
||||
// Collapsed View
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<div className="flex justify-center py-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
disabled={isLoading || isPending}
|
||||
className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-200',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
isRunning
|
||||
? 'bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20'
|
||||
: 'bg-white/[0.04] text-muted-foreground hover:bg-white/[0.08] hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Power className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="flex items-center gap-2 text-xs">
|
||||
<div
|
||||
className={cn(
|
||||
'h-1.5 w-1.5 rounded-full',
|
||||
isRunning ? 'bg-emerald-400' : 'bg-muted-foreground',
|
||||
)}
|
||||
/>
|
||||
<span>
|
||||
{isLoading
|
||||
? t('status.checking')
|
||||
: isRunning
|
||||
? t('status.running')
|
||||
: t('status.stopped')}
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Expanded View
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between rounded-lg px-3 py-2 transition-all duration-200',
|
||||
isRunning
|
||||
? 'bg-emerald-500/[0.08]'
|
||||
: 'bg-white/[0.02]',
|
||||
<div className="flex items-center gap-2 rounded-md border border-white/[0.06] bg-white/[0.02] px-3 py-2">
|
||||
{isLoading ? (
|
||||
<Loader2 className="text-muted-foreground h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'h-2 w-2 rounded-full',
|
||||
isRunning
|
||||
? 'bg-emerald-400 shadow-[0_0_6px_rgba(16,185,129,0.4)]'
|
||||
: 'bg-muted-foreground/50',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-3 w-3 shrink-0 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'h-2 w-2 shrink-0 rounded-full transition-colors duration-300',
|
||||
isRunning
|
||||
? 'bg-emerald-400 shadow-[0_0_6px_rgba(16,185,129,0.4)]'
|
||||
: 'bg-muted-foreground/50',
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs font-medium',
|
||||
isRunning ? 'text-emerald-400' : 'text-muted-foreground',
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Status
|
||||
</span>
|
||||
<span className={cn(
|
||||
'text-xs font-medium leading-tight',
|
||||
isRunning ? 'text-emerald-400' : 'text-muted-foreground',
|
||||
)}>
|
||||
{isLoading
|
||||
? t('status.checking')
|
||||
: isRunning
|
||||
? t('status.running')
|
||||
: t('status.stopped')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
{isLoading ? '...' : isRunning ? 'Running' : 'Stopped'}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleToggle}
|
||||
disabled={isLoading || isPending}
|
||||
className={cn(
|
||||
'h-7 gap-1.5 rounded-md px-2.5 text-xs',
|
||||
'h-7 w-7 rounded-md p-0',
|
||||
isRunning
|
||||
? 'text-emerald-400 hover:bg-emerald-500/10'
|
||||
: 'text-muted-foreground hover:bg-white/[0.06]',
|
||||
)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : isRunning ? (
|
||||
<Square className="h-3 w-3 fill-current" />
|
||||
<Square className="h-3.5 w-3.5 fill-current" />
|
||||
) : (
|
||||
<Play className="h-3 w-3 fill-current" />
|
||||
<Play className="h-3.5 w-3.5 fill-current" />
|
||||
)}
|
||||
<span>{isRunning ? t('action.stop') : t('action.start')}</span>
|
||||
<span className="sr-only">{isRunning ? t('action.stop') : t('action.start')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Cloud } from 'lucide-react';
|
||||
|
||||
import { CloudAccountCard, CompactCloudAccountCard } from '@/components/CloudAccountCard';
|
||||
import { type GridLayout } from '@/components/CloudAccountList';
|
||||
import type { CloudAccount } from '@/types/cloudAccount';
|
||||
|
||||
export const GRID_LAYOUT_CLASSES: Record<GridLayout, string> = {
|
||||
auto: 'grid gap-3 md:grid-cols-2 xl:grid-cols-3',
|
||||
'2-col': 'grid gap-3 grid-cols-2',
|
||||
'3-col': 'grid gap-3 grid-cols-3',
|
||||
list: 'grid gap-3 grid-cols-1',
|
||||
compact: 'flex flex-col gap-2',
|
||||
};
|
||||
|
||||
interface AccountsGridProps {
|
||||
accounts: CloudAccount[];
|
||||
gridLayout: GridLayout;
|
||||
selectedIds: Set<string>;
|
||||
onToggleSelection: (id: string, selected: boolean) => void;
|
||||
onRefresh: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onSwitch: (id: string) => void;
|
||||
onManageIdentity: (id: string) => void;
|
||||
refreshingAccountId?: string;
|
||||
deletingAccountId?: string;
|
||||
switchingAccountId?: string;
|
||||
isRefreshPending: boolean;
|
||||
isDeletePending: boolean;
|
||||
isSwitchPending: boolean;
|
||||
}
|
||||
|
||||
export function AccountsGrid(props: AccountsGridProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
accounts,
|
||||
gridLayout,
|
||||
selectedIds,
|
||||
onToggleSelection,
|
||||
onRefresh,
|
||||
onDelete,
|
||||
onSwitch,
|
||||
onManageIdentity,
|
||||
refreshingAccountId,
|
||||
deletingAccountId,
|
||||
switchingAccountId,
|
||||
isRefreshPending,
|
||||
isDeletePending,
|
||||
isSwitchPending,
|
||||
} = props;
|
||||
return (
|
||||
<div className={GRID_LAYOUT_CLASSES[gridLayout]}>
|
||||
{accounts.map((account) =>
|
||||
gridLayout === 'compact' ? (
|
||||
<CompactCloudAccountCard
|
||||
key={account.id}
|
||||
account={account}
|
||||
onRefresh={onRefresh}
|
||||
onDelete={onDelete}
|
||||
onSwitch={onSwitch}
|
||||
onManageIdentity={onManageIdentity}
|
||||
isRefreshing={isRefreshPending && refreshingAccountId === account.id}
|
||||
isDeleting={isDeletePending && deletingAccountId === account.id}
|
||||
isSwitching={isSwitchPending && switchingAccountId === account.id}
|
||||
/>
|
||||
) : (
|
||||
<CloudAccountCard
|
||||
key={account.id}
|
||||
account={account}
|
||||
onRefresh={onRefresh}
|
||||
onDelete={onDelete}
|
||||
onSwitch={onSwitch}
|
||||
onManageIdentity={onManageIdentity}
|
||||
isSelected={selectedIds.has(account.id)}
|
||||
onToggleSelection={onToggleSelection}
|
||||
isRefreshing={isRefreshPending && refreshingAccountId === account.id}
|
||||
isDeleting={isDeletePending && deletingAccountId === account.id}
|
||||
isSwitching={isSwitchPending && switchingAccountId === account.id}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{accounts.length === 0 && (
|
||||
<div className="col-span-full flex flex-col items-center justify-center rounded-xl border border-dashed border-white/[0.08] bg-white/[0.01] py-20 text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-white/[0.04]">
|
||||
<Cloud className="text-muted-foreground/40 h-8 w-8" />
|
||||
</div>
|
||||
<div className="mt-4 text-sm font-medium">{t('cloud.list.noAccounts')}</div>
|
||||
<p className="text-muted-foreground mt-1 max-w-xs text-xs leading-relaxed">
|
||||
{t('cloud.description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { clampQuotaPercentage, getQuotaStatus, type QuotaStatus } from '@/utils/quota-display';
|
||||
|
||||
interface AccountsHeaderProps {
|
||||
totalAccounts: number;
|
||||
activeAccounts: number;
|
||||
rateLimitedAccounts: number;
|
||||
overallQuotaPercentage: number | null;
|
||||
overallQuotaStatus: QuotaStatus | null;
|
||||
}
|
||||
|
||||
const GLOBAL_QUOTA_BAR_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = {
|
||||
high: 'bg-emerald-500',
|
||||
medium: 'bg-amber-500',
|
||||
low: 'bg-rose-500',
|
||||
};
|
||||
|
||||
const GLOBAL_QUOTA_TEXT_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = {
|
||||
high: 'text-emerald-400',
|
||||
medium: 'text-amber-400',
|
||||
low: 'text-rose-400',
|
||||
};
|
||||
|
||||
export function AccountsHeader({
|
||||
totalAccounts,
|
||||
activeAccounts,
|
||||
rateLimitedAccounts,
|
||||
overallQuotaPercentage,
|
||||
overallQuotaStatus,
|
||||
}: AccountsHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
const quotaStatus =
|
||||
overallQuotaStatus ??
|
||||
(overallQuotaPercentage === null ? null : getQuotaStatus(overallQuotaPercentage));
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex flex-col gap-6 md:flex-row md:items-end md:justify-between">
|
||||
{/* Title Area */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h2 className="text-foreground text-2xl font-semibold tracking-tight">
|
||||
{t('cloud.title')}
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-xl text-sm leading-relaxed">
|
||||
{t('cloud.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Row */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* Total & Active combined stat */}
|
||||
<div className="flex min-w-[120px] flex-col justify-center rounded-xl border border-white/[0.04] bg-white/[0.01] px-4 py-3">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-foreground text-2xl font-bold tracking-tight">
|
||||
{totalAccounts}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-emerald-400">{activeAccounts} active</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground/70 mt-0.5 text-xs font-medium tracking-wider uppercase">
|
||||
{t('cloud.card.actions')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Rate Limited stat (only if > 0) */}
|
||||
{rateLimitedAccounts > 0 && (
|
||||
<div className="flex min-w-[100px] flex-col justify-center rounded-xl border border-rose-500/10 bg-rose-500/[0.02] px-4 py-3">
|
||||
<span className="text-2xl font-bold tracking-tight text-rose-400">
|
||||
{rateLimitedAccounts}
|
||||
</span>
|
||||
<span className="mt-0.5 text-xs font-medium tracking-wider text-rose-400/60 uppercase">
|
||||
{t('cloud.card.rateLimited')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Quota */}
|
||||
{overallQuotaPercentage !== null && quotaStatus && (
|
||||
<div className="flex min-w-[140px] flex-col justify-center rounded-xl border border-white/[0.04] bg-white/[0.01] px-4 py-3">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span
|
||||
className={`text-2xl font-bold tracking-tight ${GLOBAL_QUOTA_TEXT_COLOR_CLASS_BY_STATUS[quotaStatus]}`}
|
||||
>
|
||||
{overallQuotaPercentage}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-1.5 h-1 w-full overflow-hidden rounded-full bg-white/[0.04]">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-700 ease-in-out ${GLOBAL_QUOTA_BAR_COLOR_CLASS_BY_STATUS[quotaStatus]}`}
|
||||
style={{ width: `${clampQuotaPercentage(overallQuotaPercentage)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-muted-foreground/70 mt-1.5 text-xs font-medium tracking-wider uppercase">
|
||||
{t('cloud.globalQuota')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Check,
|
||||
CheckSquare,
|
||||
Columns2,
|
||||
Columns3,
|
||||
Download,
|
||||
FileDown,
|
||||
LayoutGrid,
|
||||
LayoutList,
|
||||
List,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
RefreshCcw,
|
||||
SortAsc,
|
||||
Upload,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
interface AccountsToolbarProps {
|
||||
autoSwitchEnabled: boolean | undefined;
|
||||
isSettingsLoading: boolean;
|
||||
onToggleAutoSwitch: (checked: boolean) => void;
|
||||
isAutoSwitchPending: boolean;
|
||||
selectedCount: number;
|
||||
totalCount: number;
|
||||
onToggleSelectAll: () => void;
|
||||
onForcePoll: () => void;
|
||||
isForcePollPending: boolean;
|
||||
onSyncLocal: () => void;
|
||||
isSyncPending: boolean;
|
||||
onOpenExportDialog: () => void;
|
||||
onOpenImportDialog: () => void;
|
||||
onOpenAddDialog: () => void;
|
||||
currentSort: string;
|
||||
sortOptions: readonly string[];
|
||||
sortI18nKeys: Record<string, string>;
|
||||
onSortChange: (sort: string) => void;
|
||||
gridLayout: string;
|
||||
onLayoutChange: (layout: string) => void;
|
||||
}
|
||||
|
||||
export function AccountsToolbar(props: AccountsToolbarProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
autoSwitchEnabled,
|
||||
isSettingsLoading,
|
||||
onToggleAutoSwitch,
|
||||
isAutoSwitchPending,
|
||||
selectedCount,
|
||||
totalCount,
|
||||
onToggleSelectAll,
|
||||
onForcePoll,
|
||||
isForcePollPending,
|
||||
onSyncLocal,
|
||||
isSyncPending,
|
||||
onOpenExportDialog,
|
||||
onOpenImportDialog,
|
||||
onOpenAddDialog,
|
||||
currentSort,
|
||||
sortOptions,
|
||||
sortI18nKeys,
|
||||
onSortChange,
|
||||
gridLayout,
|
||||
onLayoutChange,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
{/* Primary Actions Group */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={onOpenAddDialog} className="cursor-pointer shadow-sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('cloud.addAccount')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onToggleSelectAll}
|
||||
title={t('cloud.batch.selectAll')}
|
||||
className={`cursor-pointer transition-colors ${selectedCount > 0 && selectedCount === totalCount ? 'border-primary/50 bg-primary/5 text-primary' : ''}`}
|
||||
>
|
||||
<CheckSquare
|
||||
className={`mr-2 h-4 w-4 ${selectedCount > 0 && selectedCount === totalCount ? 'text-primary' : 'opacity-70'}`}
|
||||
/>
|
||||
{selectedCount > 0 ? `${selectedCount} Selected` : t('cloud.batch.selectAll')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Secondary Actions & View Options */}
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
{/* Layout Toggles */}
|
||||
<div className="hidden items-center gap-1 rounded-lg border border-white/[0.08] bg-white/[0.02] p-1 sm:flex">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={gridLayout === 'auto' ? 'secondary' : 'ghost'}
|
||||
size="icon"
|
||||
className="h-7 w-7 cursor-pointer rounded-md"
|
||||
onClick={() => onLayoutChange('auto')}
|
||||
>
|
||||
<LayoutGrid className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('cloud.layout.auto')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={gridLayout === '2-col' ? 'secondary' : 'ghost'}
|
||||
size="icon"
|
||||
className="h-7 w-7 cursor-pointer rounded-md"
|
||||
onClick={() => onLayoutChange('2-col')}
|
||||
>
|
||||
<Columns2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('cloud.layout.twoCol')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={gridLayout === '3-col' ? 'secondary' : 'ghost'}
|
||||
size="icon"
|
||||
className="h-7 w-7 cursor-pointer rounded-md"
|
||||
onClick={() => onLayoutChange('3-col')}
|
||||
>
|
||||
<Columns3 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('cloud.layout.threeCol')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={gridLayout === 'list' ? 'secondary' : 'ghost'}
|
||||
size="icon"
|
||||
className="h-7 w-7 cursor-pointer rounded-md"
|
||||
onClick={() => onLayoutChange('list')}
|
||||
>
|
||||
<List className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('cloud.layout.list')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={gridLayout === 'compact' ? 'secondary' : 'ghost'}
|
||||
size="icon"
|
||||
className="h-7 w-7 cursor-pointer rounded-md"
|
||||
onClick={() => onLayoutChange('compact')}
|
||||
>
|
||||
<LayoutList className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('cloud.layout.compact')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
{/* Sort Dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="cursor-pointer gap-2 bg-white/[0.02]">
|
||||
<SortAsc className="h-4 w-4 opacity-70" />
|
||||
<span className="hidden text-xs sm:inline-block">{t(sortI18nKeys[currentSort])}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs tracking-wider uppercase">
|
||||
Sort By
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{sortOptions.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option}
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSortChange(option)}
|
||||
>
|
||||
{currentSort === option ? (
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
<div className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t(sortI18nKeys[option])}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* More Options Dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer bg-white/[0.02]">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
{/* Auto-Switch Toggle embedded in menu */}
|
||||
<div className="flex items-center justify-between px-3 py-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap
|
||||
className={`h-4 w-4 ${autoSwitchEnabled ? 'fill-yellow-500 text-yellow-500' : 'text-muted-foreground'}`}
|
||||
/>
|
||||
<Label htmlFor="auto-switch" className="cursor-pointer text-sm font-medium">
|
||||
{t('cloud.autoSwitch')}
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="auto-switch"
|
||||
checked={!!autoSwitchEnabled}
|
||||
onCheckedChange={onToggleAutoSwitch}
|
||||
disabled={isSettingsLoading || isAutoSwitchPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs tracking-wider uppercase">
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={onForcePoll}
|
||||
disabled={isForcePollPending}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<RefreshCcw className={`mr-2 h-4 w-4 ${isForcePollPending ? 'animate-spin' : ''}`} />
|
||||
{t('cloud.checkQuota')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={onSyncLocal}
|
||||
disabled={isSyncPending}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Download className={`mr-2 h-4 w-4 ${isSyncPending ? 'animate-bounce' : ''}`} />
|
||||
{t('cloud.syncFromIDE')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs tracking-wider uppercase">
|
||||
Data
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={onOpenExportDialog} className="cursor-pointer">
|
||||
<FileDown className="mr-2 h-4 w-4" />
|
||||
{t('cloud.exportImport.export')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onOpenImportDialog} className="cursor-pointer">
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{t('cloud.exportImport.import')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Cloud, Loader2 } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
interface AddAccountDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
authCode: string;
|
||||
onAuthCodeChange: (code: string) => void;
|
||||
selectedOAuthClientKey: string;
|
||||
onOAuthClientChange: (key: string) => void;
|
||||
oauthClients: Array<{ key: string; label: string; is_active: boolean }>;
|
||||
isOAuthClientsLoading: boolean;
|
||||
isSetActiveOAuthClientPending: boolean;
|
||||
onOpenGoogleAuth: () => void;
|
||||
onSubmitAuthCode: () => void;
|
||||
isAddPending: boolean;
|
||||
}
|
||||
|
||||
export function AddAccountDialog(props: AddAccountDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
open,
|
||||
onOpenChange,
|
||||
authCode,
|
||||
onAuthCodeChange,
|
||||
selectedOAuthClientKey,
|
||||
onOAuthClientChange,
|
||||
oauthClients,
|
||||
isOAuthClientsLoading,
|
||||
isSetActiveOAuthClientPending,
|
||||
onOpenGoogleAuth,
|
||||
onSubmitAuthCode,
|
||||
isAddPending,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<span />
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('cloud.authDialog.title')}</DialogTitle>
|
||||
<DialogDescription>{t('cloud.authDialog.description')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="oauth-client-select">{t('cloud.authDialog.oauthClient')}</Label>
|
||||
<Select
|
||||
value={selectedOAuthClientKey || undefined}
|
||||
onValueChange={onOAuthClientChange}
|
||||
disabled={isOAuthClientsLoading || isSetActiveOAuthClientPending}
|
||||
>
|
||||
<SelectTrigger id="oauth-client-select">
|
||||
<SelectValue placeholder={t('cloud.authDialog.oauthClientPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{oauthClients.map((client) => (
|
||||
<SelectItem key={client.key} value={client.key}>
|
||||
{client.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Button variant="outline" className="col-span-4" onClick={onOpenGoogleAuth}>
|
||||
<Cloud className="mr-2 h-4 w-4" />
|
||||
{t('cloud.authDialog.openLogin')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="code">{t('cloud.authDialog.authCode')}</Label>
|
||||
<Input
|
||||
id="code"
|
||||
placeholder={t('cloud.authDialog.placeholder')}
|
||||
value={authCode}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onAuthCodeChange(e.target.value)
|
||||
}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">{t('cloud.authDialog.instruction')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={onSubmitAuthCode} disabled={isAddPending || !authCode}>
|
||||
{isAddPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{t('cloud.authDialog.verify')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RefreshCw, Trash2, X } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface BatchActionBarProps {
|
||||
selectedCount: number;
|
||||
onClearSelection: () => void;
|
||||
onRefreshSelected: () => void;
|
||||
onDeleteSelected: () => void;
|
||||
}
|
||||
|
||||
export function BatchActionBar({
|
||||
selectedCount,
|
||||
onClearSelection,
|
||||
onRefreshSelected,
|
||||
onDeleteSelected,
|
||||
}: BatchActionBarProps) {
|
||||
const { t } = useTranslation();
|
||||
if (selectedCount <= 0) return null;
|
||||
return (
|
||||
<div className="bg-card animate-in fade-in slide-in-from-bottom-4 fixed bottom-6 left-1/2 z-50 flex -translate-x-1/2 items-center gap-4 rounded-full border border-white/[0.1] px-6 py-2 shadow-2xl shadow-black/30">
|
||||
<div className="flex items-center gap-2 border-r pr-4">
|
||||
<span className="text-sm font-semibold">
|
||||
{t('cloud.batch.selected', { count: selectedCount })}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 rounded-full"
|
||||
onClick={onClearSelection}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={onRefreshSelected}>
|
||||
<RefreshCw className="mr-2 h-3 w-3" />
|
||||
{t('cloud.batch.refresh')}
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={onDeleteSelected}>
|
||||
<Trash2 className="mr-2 h-3 w-3" />
|
||||
{t('cloud.batch.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import type React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
interface ExportImportDialogsProps {
|
||||
isExportDialogOpen: boolean;
|
||||
onExportDialogChange: (open: boolean) => void;
|
||||
onExport: (stripTokens: boolean) => void;
|
||||
isExportPending: boolean;
|
||||
isImportDialogOpen: boolean;
|
||||
onImportDialogChange: (open: boolean) => void;
|
||||
importStrategy: 'merge' | 'overwrite' | 'skip-existing';
|
||||
onImportStrategyChange: (strategy: 'merge' | 'overwrite' | 'skip-existing') => void;
|
||||
importFileName: string;
|
||||
onImportFileSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
importFileContent: string | null;
|
||||
onImport: () => void;
|
||||
isImportPending: boolean;
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
}
|
||||
|
||||
export function ExportImportDialogs(props: ExportImportDialogsProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
isExportDialogOpen,
|
||||
onExportDialogChange,
|
||||
onExport,
|
||||
isExportPending,
|
||||
isImportDialogOpen,
|
||||
onImportDialogChange,
|
||||
importStrategy,
|
||||
onImportStrategyChange,
|
||||
importFileName,
|
||||
onImportFileSelect,
|
||||
importFileContent,
|
||||
onImport,
|
||||
isImportPending,
|
||||
fileInputRef,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isExportDialogOpen} onOpenChange={onExportDialogChange}>
|
||||
<DialogTrigger asChild>
|
||||
<span />
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('cloud.exportImport.exportTitle')}</DialogTitle>
|
||||
<DialogDescription>{t('cloud.exportImport.exportDesc')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="flex w-full flex-col gap-2 sm:flex-row sm:justify-center sm:space-x-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onExport(false)}
|
||||
disabled={isExportPending}
|
||||
className="w-full cursor-pointer sm:flex-1"
|
||||
>
|
||||
{isExportPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{t('cloud.exportImport.includeTokens')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onExport(true)}
|
||||
disabled={isExportPending}
|
||||
className="w-full cursor-pointer sm:flex-1"
|
||||
>
|
||||
{isExportPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{t('cloud.exportImport.stripTokens')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={isImportDialogOpen} onOpenChange={onImportDialogChange}>
|
||||
<DialogTrigger asChild>
|
||||
<span />
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('cloud.exportImport.importTitle')}</DialogTitle>
|
||||
<DialogDescription>{t('cloud.exportImport.importDesc')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>{t('cloud.exportImport.selectFile')}</Label>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={onImportFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{importFileName || t('cloud.exportImport.selectFile')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>{t('cloud.exportImport.importStrategy')}</Label>
|
||||
<Select value={importStrategy} onValueChange={onImportStrategyChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="merge">{t('cloud.exportImport.strategyMerge')}</SelectItem>
|
||||
<SelectItem value="overwrite">
|
||||
{t('cloud.exportImport.strategyOverwrite')}
|
||||
</SelectItem>
|
||||
<SelectItem value="skip-existing">
|
||||
{t('cloud.exportImport.strategySkip')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={onImport} disabled={!importFileContent || isImportPending}>
|
||||
{isImportPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{isImportPending ? t('cloud.exportImport.importing') : t('cloud.exportImport.import')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export default function DragWindowRegion({ title }: DragWindowRegionProps) {
|
||||
<div className="flex w-screen items-stretch justify-between">
|
||||
<div className="draglayer w-full">
|
||||
{title && !isMacOS && (
|
||||
<div className="flex flex-1 p-2 text-xs whitespace-nowrap text-muted-foreground select-none">
|
||||
<div className="text-muted-foreground flex flex-1 p-2 text-xs whitespace-nowrap select-none">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { MonitorTab } from './advanced/MonitorTab';
|
||||
import { ToolsTab } from './advanced/ToolsTab';
|
||||
import { ReplayTab } from './advanced/ReplayTab';
|
||||
import { CapabilitiesTab } from './advanced/CapabilitiesTab';
|
||||
import type { ProxyConfig } from '@/types/config';
|
||||
|
||||
interface ProxyAdvancedTabProps {
|
||||
proxyConfig: ProxyConfig;
|
||||
}
|
||||
|
||||
export const ProxyAdvancedTab: React.FC<ProxyAdvancedTabProps> = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="monitor" className="space-y-5">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="monitor">{t('proxy.advanced.monitor')}</TabsTrigger>
|
||||
<TabsTrigger value="tools">{t('proxy.advanced.tabs.tools')}</TabsTrigger>
|
||||
<TabsTrigger value="replay">{t('proxy.advanced.tabs.replay')}</TabsTrigger>
|
||||
<TabsTrigger value="capabilities">{t('proxy.advanced.tabs.capabilities')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="monitor">
|
||||
<MonitorTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="tools">
|
||||
<ToolsTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="replay">
|
||||
<ReplayTab />
|
||||
</TabsContent>
|
||||
<TabsContent value="capabilities">
|
||||
<CapabilitiesTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -98,7 +98,7 @@ export const CapabilitiesTab: React.FC = () => {
|
||||
<h3 className="text-base font-semibold">
|
||||
{t('proxy.advanced.capabilities.title', 'Model Capabilities')}
|
||||
</h3>
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
<p className="text-muted-foreground text-[13px]">
|
||||
{t('proxy.advanced.capabilities.description', 'Discover what each model can do')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -113,7 +113,7 @@ export const CapabilitiesTab: React.FC = () => {
|
||||
{/* Content */}
|
||||
{isLoading ? (
|
||||
<div className="flex h-40 items-center justify-center">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
<Loader2 className="text-muted-foreground h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
@@ -121,11 +121,11 @@ export const CapabilitiesTab: React.FC = () => {
|
||||
<div key={provider} className="space-y-3">
|
||||
{/* Provider header */}
|
||||
<div className="flex items-center gap-3">
|
||||
<h4 className="text-[13px] font-semibold tracking-wide uppercase text-muted-foreground">
|
||||
<h4 className="text-muted-foreground text-[13px] font-semibold tracking-wide uppercase">
|
||||
{provider}
|
||||
</h4>
|
||||
<div className="h-px flex-1 bg-white/[0.06]" />
|
||||
<span className="text-[11px] text-muted-foreground/60 tabular-nums">
|
||||
<span className="text-muted-foreground/60 text-[11px] tabular-nums">
|
||||
{groupedModels[provider].length}
|
||||
</span>
|
||||
</div>
|
||||
@@ -140,23 +140,23 @@ export const CapabilitiesTab: React.FC = () => {
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
className="group flex flex-col rounded-xl border border-white/[0.06] bg-card p-4 transition-all hover:border-white/[0.12] hover:bg-white/[0.02]"
|
||||
className="group bg-card flex flex-col rounded-xl border border-white/[0.06] p-4 transition-all hover:border-white/[0.12] hover:bg-white/[0.02]"
|
||||
>
|
||||
{/* Model header */}
|
||||
<div className="mb-3 space-y-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h5
|
||||
className="text-[13px] font-semibold leading-tight truncate"
|
||||
className="truncate text-[13px] leading-tight font-semibold"
|
||||
title={model.displayName || model.id}
|
||||
>
|
||||
{model.displayName || model.id}
|
||||
</h5>
|
||||
<span className="shrink-0 text-[10px] font-medium text-muted-foreground/60 rounded-md bg-white/[0.04] px-1.5 py-0.5">
|
||||
<span className="text-muted-foreground/60 shrink-0 rounded-md bg-white/[0.04] px-1.5 py-0.5 text-[10px] font-medium">
|
||||
{enabledCount}/{totalCount}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className="font-mono text-[11px] text-muted-foreground truncate"
|
||||
className="text-muted-foreground truncate font-mono text-[11px]"
|
||||
title={model.id}
|
||||
>
|
||||
{model.id}
|
||||
@@ -164,7 +164,7 @@ export const CapabilitiesTab: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Capability pills */}
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
<div className="mb-3 flex flex-wrap gap-1.5">
|
||||
{Object.entries(model.capabilities).map(([key, supported]) => {
|
||||
const iconKey = key as keyof typeof CAPABILITY_ICONS;
|
||||
const Icon = CAPABILITY_ICONS[iconKey];
|
||||
@@ -174,8 +174,8 @@ export const CapabilitiesTab: React.FC = () => {
|
||||
key={key}
|
||||
className={`inline-flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-medium transition-colors ${
|
||||
supported
|
||||
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/15'
|
||||
: 'bg-white/[0.02] text-muted-foreground/50 border border-white/[0.04]'
|
||||
? 'border border-emerald-500/15 bg-emerald-500/10 text-emerald-400'
|
||||
: 'text-muted-foreground/50 border border-white/[0.04] bg-white/[0.02]'
|
||||
}`}
|
||||
title={label}
|
||||
>
|
||||
@@ -190,17 +190,17 @@ export const CapabilitiesTab: React.FC = () => {
|
||||
{hasLimits && (
|
||||
<div className="mt-auto flex flex-wrap gap-1.5 border-t border-white/[0.04] pt-3">
|
||||
{model.limits.maxTokens != null && (
|
||||
<span className="inline-flex items-center rounded-md bg-white/[0.04] px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
|
||||
<span className="text-muted-foreground inline-flex items-center rounded-md bg-white/[0.04] px-1.5 py-0.5 font-mono text-[10px]">
|
||||
CTX {formatTokenCount(model.limits.maxTokens)}
|
||||
</span>
|
||||
)}
|
||||
{model.limits.maxOutputTokens != null && (
|
||||
<span className="inline-flex items-center rounded-md bg-white/[0.04] px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
|
||||
<span className="text-muted-foreground inline-flex items-center rounded-md bg-white/[0.04] px-1.5 py-0.5 font-mono text-[10px]">
|
||||
OUT {formatTokenCount(model.limits.maxOutputTokens)}
|
||||
</span>
|
||||
)}
|
||||
{model.limits.thinkingBudget != null && (
|
||||
<span className="inline-flex items-center rounded-md bg-blue-500/10 border border-blue-500/15 px-1.5 py-0.5 font-mono text-[10px] text-blue-300">
|
||||
<span className="inline-flex items-center rounded-md border border-blue-500/15 bg-blue-500/10 px-1.5 py-0.5 font-mono text-[10px] text-blue-300">
|
||||
THINK {formatTokenCount(model.limits.thinkingBudget)}
|
||||
</span>
|
||||
)}
|
||||
@@ -215,12 +215,10 @@ export const CapabilitiesTab: React.FC = () => {
|
||||
|
||||
{models.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-white/[0.1] py-16 text-center">
|
||||
<Layers className="h-8 w-8 text-muted-foreground/30" />
|
||||
<Layers className="text-muted-foreground/30 h-8 w-8" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-[13px] font-medium">No models found</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Check your provider configurations
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">Check your provider configurations</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -54,7 +54,8 @@ export const ReplayTab: React.FC = () => {
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: t('proxy.advanced.replay.error'),
|
||||
description: error instanceof Error ? error.message : t('proxy.advanced.replay.unknownError'),
|
||||
description:
|
||||
error instanceof Error ? error.message : t('proxy.advanced.replay.unknownError'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
setSelectedRequestId(null);
|
||||
@@ -76,7 +77,10 @@ export const ReplayTab: React.FC = () => {
|
||||
if (!status) return <Badge variant="outline">{t('proxy.advanced.replay.pending')}</Badge>;
|
||||
if (status >= 200 && status < 300) {
|
||||
return (
|
||||
<Badge variant="default" className="bg-emerald-500/15 text-emerald-400 border-emerald-500/20 hover:bg-emerald-500/20">
|
||||
<Badge
|
||||
variant="default"
|
||||
className="border-emerald-500/20 bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/20"
|
||||
>
|
||||
<CheckCircle className="mr-1 h-3 w-3" />
|
||||
{status}
|
||||
</Badge>
|
||||
@@ -99,7 +103,9 @@ export const ReplayTab: React.FC = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold">{t('proxy.advanced.replay.title')}</h3>
|
||||
<p className="text-[13px] text-muted-foreground">{t('proxy.advanced.replay.description')}</p>
|
||||
<p className="text-muted-foreground text-[13px]">
|
||||
{t('proxy.advanced.replay.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -113,10 +119,12 @@ export const ReplayTab: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Requests List */}
|
||||
<div className="overflow-hidden rounded-xl border border-white/[0.06] bg-card">
|
||||
<div className="border-b border-white/[0.06] px-5 py-4 space-y-1">
|
||||
<div className="bg-card overflow-hidden rounded-xl border border-white/[0.06]">
|
||||
<div className="space-y-1 border-b border-white/[0.06] px-5 py-4">
|
||||
<h4 className="text-[13px] font-semibold">{t('proxy.advanced.replay.recentRequests')}</h4>
|
||||
<p className="text-xs text-muted-foreground">{t('proxy.advanced.replay.clickToReplay')}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('proxy.advanced.replay.clickToReplay')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
{requestsLoading ? (
|
||||
@@ -125,8 +133,8 @@ export const ReplayTab: React.FC = () => {
|
||||
</div>
|
||||
) : requests.length === 0 ? (
|
||||
<div className="flex h-32 flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-white/[0.1]">
|
||||
<Clock className="h-6 w-6 text-muted-foreground/30" />
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
<Clock className="text-muted-foreground/30 h-6 w-6" />
|
||||
<p className="text-muted-foreground text-[13px]">
|
||||
{t('proxy.advanced.replay.noRequests')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -143,7 +151,7 @@ export const ReplayTab: React.FC = () => {
|
||||
<span className="text-[13px] font-medium">
|
||||
{request.method} {request.endpoint}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{formatTimestamp(request.timestamp)}
|
||||
{request.duration && ` · ${request.duration}ms`}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery, useQueries } from '@tanstack/react-query';
|
||||
import { ipc } from '@/ipc/manager';
|
||||
@@ -6,17 +6,42 @@ import { ipc } from '@/ipc/manager';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { StatCard } from '@/components/usage/StatCard';
|
||||
import { Loader2, Copy, CheckCircle, ShieldAlert, XCircle, Terminal, Code2, Braces } from 'lucide-react';
|
||||
import {
|
||||
Loader2,
|
||||
Copy,
|
||||
CheckCircle,
|
||||
ShieldAlert,
|
||||
XCircle,
|
||||
Terminal,
|
||||
Code2,
|
||||
Braces,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
const IDE_CONFIGS = [
|
||||
{ key: 'opencode', label: 'OpenCode', icon: Sparkles },
|
||||
{ key: 'vscode', label: 'VS Code', icon: Code2 },
|
||||
{ key: 'cursor', label: 'Cursor', icon: Terminal },
|
||||
{ key: 'claude-code', label: 'Claude Code', icon: Braces },
|
||||
] as const;
|
||||
|
||||
export const ToolsTab: React.FC = () => {
|
||||
interface ToolsTabProps {
|
||||
availableModelIds?: string[];
|
||||
}
|
||||
|
||||
export const ToolsTab: React.FC<ToolsTabProps> = ({
|
||||
availableModelIds = ['gemini-3.1-pro-high'],
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [selectedModel, setSelectedModel] = useState<string>(availableModelIds[0]);
|
||||
|
||||
const { data: parityData, isLoading: parityLoading } = useQuery({
|
||||
queryKey: ['proxyAdvanced', 'parityCounters'],
|
||||
@@ -25,16 +50,11 @@ export const ToolsTab: React.FC = () => {
|
||||
});
|
||||
|
||||
const ideQueries = useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['proxyAdvanced', 'ideConfig', 'vscode'],
|
||||
queryFn: () => ipc.client.proxyAdvanced.generateIdeConfig({ ide: 'vscode' }),
|
||||
},
|
||||
{
|
||||
queryKey: ['proxyAdvanced', 'ideConfig', 'cursor'],
|
||||
queryFn: () => ipc.client.proxyAdvanced.generateIdeConfig({ ide: 'cursor' }),
|
||||
},
|
||||
],
|
||||
queries: IDE_CONFIGS.map(({ key }) => ({
|
||||
queryKey: ['proxyAdvanced', 'ideConfig', key, selectedModel],
|
||||
queryFn: () =>
|
||||
ipc.client.proxyAdvanced.generateIdeConfig({ ide: key, defaultModel: selectedModel }),
|
||||
})),
|
||||
});
|
||||
|
||||
const parity = parityData?.success ? parityData.data : undefined;
|
||||
@@ -44,15 +64,21 @@ export const ToolsTab: React.FC = () => {
|
||||
toast({ title: t('proxy.advanced.tools.copyConfig') });
|
||||
};
|
||||
|
||||
const getIdeInstructions = (key: string): string => {
|
||||
const getIdeInstructions = (key: string, index: number): string => {
|
||||
if (key === 'claude-code') {
|
||||
return 'export ANTHROPIC_BASE_URL=http://localhost:8045/v1\nexport ANTHROPIC_API_KEY=YOUR_API_KEY';
|
||||
}
|
||||
const index = key === 'vscode' ? 0 : 1;
|
||||
const result = ideQueries[index].data;
|
||||
const result = ideQueries[index]?.data;
|
||||
if (result?.success && result.data) {
|
||||
const d = result.data;
|
||||
return `${d.instructions}\n${d.proxySetting}: http://localhost:8045\n${d.apiKeySetting}: YOUR_API_KEY`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const d = result.data as any;
|
||||
if (d.format === 'json') {
|
||||
return JSON.stringify(d.content, null, 2);
|
||||
}
|
||||
if (d.format === 'env' || d.format === 'shell') {
|
||||
return typeof d.content === 'string' ? d.content : JSON.stringify(d.content, null, 2);
|
||||
}
|
||||
return `${d.instructions || ''}\n${d.proxySetting || 'URL'}: http://localhost:8045\n${d.apiKeySetting || 'Key'}: YOUR_API_KEY`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
@@ -87,24 +113,45 @@ export const ToolsTab: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* IDE Quick Setup */}
|
||||
<div className="overflow-hidden rounded-xl border border-white/[0.06] bg-card">
|
||||
<div className="border-b border-white/[0.06] px-5 py-4 space-y-1">
|
||||
<h4 className="text-[13px] font-semibold">{t('proxy.advanced.tools.ideSetup')}</h4>
|
||||
<p className="text-xs text-muted-foreground">Quick configuration for popular IDEs</p>
|
||||
<div className="bg-card overflow-hidden rounded-xl border border-white/[0.06]">
|
||||
<div className="flex items-center justify-between border-b border-white/[0.06] px-5 py-4">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-[13px] font-semibold">{t('proxy.advanced.tools.ideSetup')}</h4>
|
||||
<p className="text-muted-foreground text-xs">Quick configuration for popular IDEs</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">Default Model:</span>
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel}>
|
||||
<SelectTrigger className="h-8 w-[180px] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableModelIds.map((id) => (
|
||||
<SelectItem key={id} value={id}>
|
||||
{id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="space-y-3 p-5">
|
||||
{isIdeLoading ? (
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
IDE_CONFIGS.map(({ key, label, icon: Icon }) => {
|
||||
const instructions = getIdeInstructions(key);
|
||||
IDE_CONFIGS.map(({ key, label, icon: Icon }, index) => {
|
||||
const instructions = getIdeInstructions(key, index);
|
||||
const resultData = ideQueries[index]?.data;
|
||||
const hasError = resultData && !resultData.success;
|
||||
const errorMsg = resultData?.error || '';
|
||||
|
||||
return (
|
||||
<div key={key} className="overflow-hidden rounded-xl border border-white/[0.06]">
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
<Icon className="text-muted-foreground h-4 w-4" />
|
||||
<span className="text-[13px] font-medium">{label}</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -112,13 +159,19 @@ export const ToolsTab: React.FC = () => {
|
||||
size="sm"
|
||||
className="h-8 text-xs"
|
||||
onClick={() => copyToClipboard(instructions)}
|
||||
disabled={!instructions || hasError}
|
||||
>
|
||||
<Copy className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t('proxy.advanced.tools.copyConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
{instructions && (
|
||||
<pre className="overflow-x-auto border-t border-white/[0.06] bg-card p-3 font-mono text-xs whitespace-pre-wrap text-foreground/90">
|
||||
{hasError && (
|
||||
<div className="bg-card border-t border-white/[0.06] p-3 font-mono text-xs text-amber-500/80">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
{instructions && !hasError && (
|
||||
<pre className="bg-card text-foreground/90 overflow-x-auto border-t border-white/[0.06] p-3 font-mono text-xs whitespace-pre-wrap">
|
||||
{instructions}
|
||||
</pre>
|
||||
)}
|
||||
@@ -128,7 +181,6 @@ export const ToolsTab: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,190 +0,0 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader2, ArrowUp, ArrowDown } from 'lucide-react';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface TrafficLogEntry {
|
||||
timestamp: string;
|
||||
direction: string;
|
||||
requestId: string;
|
||||
endpoint: string;
|
||||
method?: string;
|
||||
status?: number;
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
interface TrafficLogTableProps {
|
||||
logs: TrafficLogEntry[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
type SortKey = 'timestamp' | 'direction' | 'endpoint' | 'method' | 'status' | 'duration';
|
||||
type SortDir = 'asc' | 'desc';
|
||||
|
||||
function SortIcon({ column, sortKey, sortDir }: { column: SortKey; sortKey: SortKey; sortDir: SortDir }) {
|
||||
if (sortKey !== column) return <span className="ml-1 inline-block w-3" />;
|
||||
return sortDir === 'asc' ? (
|
||||
<ArrowUp className="ml-1 inline h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="ml-1 inline h-3 w-3" />
|
||||
);
|
||||
}
|
||||
|
||||
export const TrafficLogTable: React.FC<TrafficLogTableProps> = ({ logs, isLoading }) => {
|
||||
const { t } = useTranslation();
|
||||
const [sortKey, setSortKey] = useState<SortKey>('timestamp');
|
||||
const [sortDir, setSortDir] = useState<SortDir>('desc');
|
||||
|
||||
const toggleSort = (key: SortKey) => {
|
||||
if (sortKey === key) {
|
||||
setSortDir((prev) => (prev === 'asc' ? 'desc' : 'asc'));
|
||||
} else {
|
||||
setSortKey(key);
|
||||
setSortDir('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const sortedLogs = useMemo(() => {
|
||||
const dir = sortDir === 'asc' ? 1 : -1;
|
||||
return [...logs].sort((a, b) => {
|
||||
switch (sortKey) {
|
||||
case 'timestamp':
|
||||
return (new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) * dir;
|
||||
case 'direction':
|
||||
return (a.direction || '').localeCompare(b.direction || '') * dir;
|
||||
case 'endpoint':
|
||||
return (a.endpoint || '').localeCompare(b.endpoint || '') * dir;
|
||||
case 'method':
|
||||
return (a.method || '').localeCompare(b.method || '') * dir;
|
||||
case 'status': {
|
||||
const aStatus = a.status ?? -1;
|
||||
const bStatus = b.status ?? -1;
|
||||
return (aStatus - bStatus) * dir;
|
||||
}
|
||||
case 'duration': {
|
||||
const aDur = a.durationMs ?? -1;
|
||||
const bDur = b.durationMs ?? -1;
|
||||
return (aDur - bDur) * dir;
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}, [logs, sortKey, sortDir]);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-white/[0.06] bg-card">
|
||||
<div className="border-b border-white/[0.06] px-5 py-4">
|
||||
<h4 className="text-[13px] font-semibold">
|
||||
{t('proxy.advanced.trafficLog.title')}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
{isLoading ? (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
) : logs.length === 0 ? (
|
||||
<div className="py-8 text-center text-[13px] text-muted-foreground">
|
||||
{t('proxy.advanced.trafficLog.noLogs')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] text-left text-[10px] font-medium tracking-wider uppercase text-muted-foreground">
|
||||
<th
|
||||
className="cursor-pointer pb-2 pr-4 font-medium select-none"
|
||||
onClick={() => toggleSort('timestamp')}
|
||||
>
|
||||
{t('proxy.advanced.trafficLog.timestamp')}
|
||||
<SortIcon column="timestamp" sortKey={sortKey} sortDir={sortDir} />
|
||||
</th>
|
||||
<th
|
||||
className="cursor-pointer pb-2 pr-4 font-medium select-none"
|
||||
onClick={() => toggleSort('direction')}
|
||||
>
|
||||
{t('proxy.advanced.trafficLog.direction')}
|
||||
<SortIcon column="direction" sortKey={sortKey} sortDir={sortDir} />
|
||||
</th>
|
||||
<th
|
||||
className="cursor-pointer pb-2 pr-4 font-medium select-none"
|
||||
onClick={() => toggleSort('endpoint')}
|
||||
>
|
||||
{t('proxy.advanced.trafficLog.endpoint')}
|
||||
<SortIcon column="endpoint" sortKey={sortKey} sortDir={sortDir} />
|
||||
</th>
|
||||
<th
|
||||
className="cursor-pointer pb-2 pr-4 font-medium select-none"
|
||||
onClick={() => toggleSort('method')}
|
||||
>
|
||||
{t('proxy.advanced.trafficLog.method')}
|
||||
<SortIcon column="method" sortKey={sortKey} sortDir={sortDir} />
|
||||
</th>
|
||||
<th
|
||||
className="cursor-pointer pb-2 pr-4 font-medium select-none"
|
||||
onClick={() => toggleSort('status')}
|
||||
>
|
||||
{t('proxy.advanced.trafficLog.status')}
|
||||
<SortIcon column="status" sortKey={sortKey} sortDir={sortDir} />
|
||||
</th>
|
||||
<th
|
||||
className="cursor-pointer pb-2 font-medium select-none"
|
||||
onClick={() => toggleSort('duration')}
|
||||
>
|
||||
{t('proxy.advanced.trafficLog.duration')}
|
||||
<SortIcon column="duration" sortKey={sortKey} sortDir={sortDir} />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedLogs.map((log, i) => (
|
||||
<tr
|
||||
key={`${log.requestId}-${i}`}
|
||||
className="border-b border-white/[0.04] last:border-0"
|
||||
>
|
||||
<td className="py-2 pr-4 font-mono text-xs text-muted-foreground">
|
||||
{new Date(log.timestamp).toLocaleTimeString()}
|
||||
</td>
|
||||
<td className="py-2 pr-4">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
log.direction === 'inbound'
|
||||
? 'border-blue-500/30 text-blue-400'
|
||||
: log.direction === 'outbound'
|
||||
? 'border-emerald-500/30 text-emerald-400'
|
||||
: 'border-slate-500/30 text-slate-400'
|
||||
}
|
||||
>
|
||||
{log.direction}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-2 pr-4 font-mono text-xs">{log.endpoint}</td>
|
||||
<td className="py-2 pr-4 text-xs">{log.method || '—'}</td>
|
||||
<td className="py-2 pr-4 text-xs">
|
||||
{log.status != null ? (
|
||||
<Badge
|
||||
variant={log.status >= 400 ? 'destructive' : 'secondary'}
|
||||
className="text-xs"
|
||||
>
|
||||
{log.status}
|
||||
</Badge>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 text-xs">
|
||||
{log.durationMs != null ? `${log.durationMs}ms` : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
|
||||
));
|
||||
Alert.displayName = 'Alert';
|
||||
|
||||
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('mb-1 leading-none font-medium tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
AlertTitle.displayName = 'AlertTitle';
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
|
||||
));
|
||||
AlertDescription.displayName = 'AlertDescription';
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
@@ -9,16 +9,14 @@ const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.98]',
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.98]',
|
||||
destructive:
|
||||
'bg-destructive/90 text-white hover:bg-destructive active:scale-[0.98] focus-visible:ring-destructive/30',
|
||||
outline:
|
||||
'border border-white/[0.08] bg-transparent hover:bg-white/[0.04] hover:border-white/[0.12] text-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80 active:scale-[0.98]',
|
||||
ghost:
|
||||
'hover:bg-white/[0.06] text-muted-foreground hover:text-foreground',
|
||||
ghost: 'hover:bg-white/[0.06] text-muted-foreground hover:text-foreground',
|
||||
link: 'text-accent underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
|
||||
@@ -26,7 +26,7 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HT
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn('text-sm font-semibold leading-none tracking-tight', className)}
|
||||
className={cn('text-sm leading-none font-semibold tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer h-4 w-4 shrink-0 rounded-[4px] border border-white/[0.15] bg-transparent ring-offset-background transition-colors focus-visible:ring-2 focus-visible:ring-accent/30 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-accent data-[state=checked]:bg-accent data-[state=checked]:text-background',
|
||||
'peer ring-offset-background focus-visible:ring-accent/30 data-[state=checked]:border-accent data-[state=checked]:bg-accent data-[state=checked]:text-background h-4 w-4 shrink-0 rounded-[4px] border border-white/[0.15] bg-transparent transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]',
|
||||
'gap-4 rounded-xl border border-white/[0.08] bg-card p-6 shadow-2xl shadow-black/30',
|
||||
'bg-card gap-4 rounded-xl border border-white/[0.08] p-6 shadow-2xl shadow-black/30',
|
||||
'duration-200',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
'data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
|
||||
@@ -47,7 +47,7 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute top-4 right-4 flex h-6 w-6 items-center justify-center rounded-md opacity-50 transition-all hover:bg-white/[0.06] hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring/30 disabled:pointer-events-none">
|
||||
<DialogPrimitive.Close className="focus:ring-ring/30 absolute top-4 right-4 flex h-6 w-6 items-center justify-center rounded-md opacity-50 transition-all hover:bg-white/[0.06] hover:opacity-100 focus:ring-2 focus:outline-none disabled:pointer-events-none">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
||||