Compare commits
1 Commits
api-change
...
backport-6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
195b766f86 |
@@ -25,6 +25,3 @@ e3bb29ceb8174b8bbca9e48ec7d42cd540f40efa
|
||||
|
||||
# [refactor] Improve updates/notifications domain organization (#5590)
|
||||
27ab355f9c73415dc39f4d3f512b02308f847801
|
||||
|
||||
# Migrate Tailwind styles to design-system package
|
||||
9f19d8fb4bd22518879343b49c05634dca777df0
|
||||
|
||||
234
.github/workflows/README-manual-api-changelog.md
vendored
@@ -1,234 +0,0 @@
|
||||
# Manual API Changelog Generation
|
||||
|
||||
This workflow allows you to generate API changelogs by comparing any two versions of the ComfyUI Frontend package.
|
||||
|
||||
## Usage
|
||||
|
||||
### Via GitHub Actions UI
|
||||
|
||||
1. Go to **Actions** tab in the repository
|
||||
2. Select **Manual API Changelog Generation** from the workflows list
|
||||
3. Click **Run workflow** button
|
||||
4. Fill in the inputs:
|
||||
- **Previous version**: The earlier version (e.g., `1.29.0` or `v1.29.0`)
|
||||
- **Current version**: The later version (e.g., `1.30.2` or `v1.30.2`)
|
||||
- **Create PR**: Check this to automatically create a pull request with the changelog
|
||||
|
||||
### Via GitHub CLI
|
||||
|
||||
```bash
|
||||
# Basic usage - just generate changelog
|
||||
gh workflow run manual-api-changelog.yaml \
|
||||
-f from_version=1.29.0 \
|
||||
-f to_version=1.30.2 \
|
||||
-f create_pr=false
|
||||
|
||||
# Generate changelog and create PR
|
||||
gh workflow run manual-api-changelog.yaml \
|
||||
-f from_version=1.29.0 \
|
||||
-f to_version=1.30.2 \
|
||||
-f create_pr=true
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
1. **Validates Inputs**: Checks that version formats are valid (X.Y.Z) and tags exist
|
||||
2. **Builds Both Versions**: Checks out each version tag, installs dependencies, and builds TypeScript types
|
||||
3. **Generates Snapshots**: Creates structured JSON snapshots of the public API surface for each version
|
||||
4. **Compares APIs**: Analyzes differences and categorizes as:
|
||||
- ⚠️ **Breaking changes** (removals, signature changes)
|
||||
- ✨ **Additions** (new interfaces, methods, properties)
|
||||
- 🔄 **Modifications** (non-breaking changes)
|
||||
5. **Uploads Artifact**: Saves the changelog and snapshots as a workflow artifact (90-day retention)
|
||||
6. **Creates PR** (optional): Generates a draft PR to update `docs/API-CHANGELOG.md`
|
||||
|
||||
## Output
|
||||
|
||||
### Workflow Artifacts
|
||||
|
||||
Every run produces an artifact containing:
|
||||
- `CHANGELOG-{from}-to-{to}.md` - Human-readable changelog
|
||||
- `from.json` - API snapshot of the earlier version
|
||||
- `to.json` - API snapshot of the later version
|
||||
|
||||
**Retention**: 90 days
|
||||
|
||||
### Pull Request (Optional)
|
||||
|
||||
If `create_pr` is enabled and changes are detected:
|
||||
- Creates a draft PR with title: `[docs] API Changelog: v{from} → v{to}`
|
||||
- Updates `docs/API-CHANGELOG.md` with the new changelog entry
|
||||
- Includes detailed metadata and review instructions
|
||||
- Labeled with `documentation`
|
||||
|
||||
## Example Changelog Output
|
||||
|
||||
```markdown
|
||||
## v1.30.2 (2025-11-04)
|
||||
|
||||
Comparing v1.29.0 → v1.30.2. This changelog documents changes to the public API surface.
|
||||
|
||||
### ✨ Additions
|
||||
|
||||
**Type Aliases**
|
||||
- `WorkflowId`
|
||||
|
||||
**Interfaces**
|
||||
- `ExtensionMetadata`
|
||||
- Members: `id`, `name`, `version`, `description`
|
||||
|
||||
### 🔄 Modifications
|
||||
|
||||
> **Note**: Some modifications may be breaking changes.
|
||||
|
||||
**Interfaces**
|
||||
- `ComfyApi`
|
||||
- ✨ Added member: `queuePromptAsync`
|
||||
- ✨ Added member: `cancelPrompt`
|
||||
- ⚠️ **Breaking**: Removed member: `queuePrompt`
|
||||
|
||||
**Enums**
|
||||
- `NodeStatus`
|
||||
- ✨ Added enum value: `ERROR`
|
||||
- ✨ Added enum value: `COMPLETED`
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Generate Changelog for Missed Releases
|
||||
|
||||
If the automatic workflow failed or was skipped for a release:
|
||||
|
||||
```bash
|
||||
gh workflow run manual-api-changelog.yaml \
|
||||
-f from_version=1.28.0 \
|
||||
-f to_version=1.29.0 \
|
||||
-f create_pr=true
|
||||
```
|
||||
|
||||
### 2. Compare Non-Adjacent Versions
|
||||
|
||||
To see cumulative changes across multiple releases:
|
||||
|
||||
```bash
|
||||
gh workflow run manual-api-changelog.yaml \
|
||||
-f from_version=1.25.0 \
|
||||
-f to_version=1.30.2 \
|
||||
-f create_pr=false
|
||||
```
|
||||
|
||||
### 3. Test Upcoming Changes
|
||||
|
||||
Compare current `main` branch against the latest release (requires creating a temporary tag):
|
||||
|
||||
```bash
|
||||
# Create temporary tag for current main
|
||||
git tag v1.31.0-preview
|
||||
git push origin v1.31.0-preview
|
||||
|
||||
# Run comparison
|
||||
gh workflow run manual-api-changelog.yaml \
|
||||
-f from_version=1.30.2 \
|
||||
-f to_version=1.31.0-preview \
|
||||
-f create_pr=false
|
||||
|
||||
# Clean up temporary tag
|
||||
git tag -d v1.31.0-preview
|
||||
git push origin :refs/tags/v1.31.0-preview
|
||||
```
|
||||
|
||||
### 4. Audit Historical Changes
|
||||
|
||||
Generate changelogs for documentation purposes:
|
||||
|
||||
```bash
|
||||
# Compare multiple version pairs
|
||||
for from in 1.26.0 1.27.0 1.28.0 1.29.0; do
|
||||
to=$(echo "$from" | awk -F. '{print $1"."$2+1".0"}')
|
||||
gh workflow run manual-api-changelog.yaml \
|
||||
-f from_version=$from \
|
||||
-f to_version=$to \
|
||||
-f create_pr=false
|
||||
done
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
The workflow validates:
|
||||
- ✅ Version format matches semantic versioning (X.Y.Z)
|
||||
- ✅ Both version tags exist in the repository
|
||||
- ✅ Tags reference valid commits with buildable code
|
||||
|
||||
If validation fails, the workflow exits early with a clear error message.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Tag requirement**: Both versions must have corresponding `vX.Y.Z` git tags
|
||||
- **Build requirement**: Both versions must have functional build processes
|
||||
- **Type files**: Requires `dist/index.d.ts` to exist after building
|
||||
- **Scripts**: Requires `scripts/snapshot-api.js` and `scripts/compare-api-snapshots.js` to be present
|
||||
|
||||
## Related Workflows
|
||||
|
||||
- **[Release API Changelogs](.github/workflows/release-api-changelogs.yaml)**: Automatic changelog generation triggered by NPM releases
|
||||
- **[Release NPM Types](.github/workflows/release-npm-types.yaml)**: Publishes type definitions and triggers automatic changelog
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Tag does not exist" error
|
||||
|
||||
Ensure the version exists as a git tag:
|
||||
|
||||
```bash
|
||||
git tag -l 'v*' | grep 1.29.0
|
||||
```
|
||||
|
||||
If missing, the version may not have been released yet.
|
||||
|
||||
### "Build failed" error
|
||||
|
||||
Check that the version can be built successfully:
|
||||
|
||||
```bash
|
||||
git checkout v1.29.0
|
||||
pnpm install
|
||||
pnpm build:types
|
||||
```
|
||||
|
||||
### No changes detected
|
||||
|
||||
If the workflow reports no changes but you expect some:
|
||||
1. Check the artifact snapshots to verify they're different
|
||||
2. Ensure you're comparing the correct versions
|
||||
3. Review the comparison script logic in `scripts/compare-api-snapshots.js`
|
||||
|
||||
### PR not created
|
||||
|
||||
PR creation requires:
|
||||
- `create_pr` input set to `true`
|
||||
- Significant changes detected (more than just headers)
|
||||
- `PR_GH_TOKEN` secret configured with appropriate permissions
|
||||
|
||||
## Security
|
||||
|
||||
- **Permissions**: Workflow requires `contents: write` and `pull-requests: write`
|
||||
- **Token**: Uses `secrets.PR_GH_TOKEN` for PR creation
|
||||
- **Isolation**: Each workflow run uses a unique concurrency group
|
||||
- **Artifacts**: Retained for 90 days, accessible to repository collaborators
|
||||
|
||||
## Monitoring
|
||||
|
||||
View workflow runs:
|
||||
```bash
|
||||
gh run list --workflow=manual-api-changelog.yaml
|
||||
```
|
||||
|
||||
View specific run details:
|
||||
```bash
|
||||
gh run view <run-id>
|
||||
```
|
||||
|
||||
Download artifacts:
|
||||
```bash
|
||||
gh run download <run-id>
|
||||
```
|
||||
255
.github/workflows/manual-api-changelog.yaml
vendored
@@ -1,255 +0,0 @@
|
||||
name: Manual API Changelog Generation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
from_version:
|
||||
description: 'Previous version (e.g., 1.29.0 or v1.29.0)'
|
||||
required: true
|
||||
type: string
|
||||
to_version:
|
||||
description: 'Current version (e.g., 1.30.2 or v1.30.2)'
|
||||
required: true
|
||||
type: string
|
||||
create_pr:
|
||||
description: 'Create a pull request with the changelog'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: manual-api-changelog-${{ github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
generate_changelog:
|
||||
name: Generate API Changelog
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for comparing versions
|
||||
|
||||
- name: Validate version inputs
|
||||
id: validate_versions
|
||||
run: |
|
||||
# Normalize version strings (remove 'v' prefix if present)
|
||||
FROM_VERSION="${{ github.event.inputs.from_version }}"
|
||||
TO_VERSION="${{ github.event.inputs.to_version }}"
|
||||
|
||||
FROM_VERSION=${FROM_VERSION#v}
|
||||
TO_VERSION=${TO_VERSION#v}
|
||||
|
||||
# Validate version format (semantic versioning)
|
||||
if ! [[ "$FROM_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Invalid from_version format: $FROM_VERSION"
|
||||
echo "Expected format: X.Y.Z (e.g., 1.29.0)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$TO_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Invalid to_version format: $TO_VERSION"
|
||||
echo "Expected format: X.Y.Z (e.g., 1.30.2)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if tags exist
|
||||
if ! git rev-parse "v$FROM_VERSION" >/dev/null 2>&1; then
|
||||
echo "Error: Tag v$FROM_VERSION does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git rev-parse "v$TO_VERSION" >/dev/null 2>&1; then
|
||||
echo "Error: Tag v$TO_VERSION does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "from_version=$FROM_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "to_version=$TO_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "from_tag=v$FROM_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "to_tag=v$TO_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "✅ Validated versions: v$FROM_VERSION → v$TO_VERSION"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
|
||||
- name: Create snapshots directory
|
||||
run: mkdir -p .api-snapshots
|
||||
|
||||
- name: Preserve scripts
|
||||
run: |
|
||||
# Copy scripts to temporary location
|
||||
mkdir -p /tmp/api-changelog-scripts
|
||||
cp scripts/snapshot-api.js scripts/compare-api-snapshots.js /tmp/api-changelog-scripts/
|
||||
|
||||
- name: Build and snapshot TO version
|
||||
run: |
|
||||
echo "Building types for v${{ steps.validate_versions.outputs.to_version }}"
|
||||
git checkout ${{ steps.validate_versions.outputs.to_tag }}
|
||||
|
||||
# Restore scripts
|
||||
mkdir -p scripts
|
||||
cp /tmp/api-changelog-scripts/*.js scripts/
|
||||
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build:types
|
||||
|
||||
# Generate snapshot
|
||||
node scripts/snapshot-api.js dist/index.d.ts > /tmp/api-snapshots-to.json
|
||||
|
||||
echo "✅ Created snapshot for v${{ steps.validate_versions.outputs.to_version }}"
|
||||
|
||||
- name: Build and snapshot FROM version
|
||||
run: |
|
||||
echo "Building types for v${{ steps.validate_versions.outputs.from_version }}"
|
||||
git checkout ${{ steps.validate_versions.outputs.from_tag }}
|
||||
|
||||
# Restore scripts
|
||||
mkdir -p scripts
|
||||
cp /tmp/api-changelog-scripts/*.js scripts/
|
||||
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build:types
|
||||
|
||||
# Generate snapshot
|
||||
node scripts/snapshot-api.js dist/index.d.ts > /tmp/api-snapshots-from.json
|
||||
|
||||
echo "✅ Created snapshot for v${{ steps.validate_versions.outputs.from_version }}"
|
||||
|
||||
- name: Return to original branch
|
||||
run: |
|
||||
git checkout ${{ github.ref_name }}
|
||||
|
||||
# Restore scripts
|
||||
mkdir -p scripts
|
||||
cp /tmp/api-changelog-scripts/*.js scripts/
|
||||
|
||||
# Copy snapshots to working directory
|
||||
cp /tmp/api-snapshots-from.json .api-snapshots/from.json
|
||||
cp /tmp/api-snapshots-to.json .api-snapshots/to.json
|
||||
|
||||
- name: Compare API snapshots and generate changelog
|
||||
id: generate_changelog
|
||||
run: |
|
||||
# Get git ref for TO version
|
||||
GIT_REF=$(git rev-parse ${{ steps.validate_versions.outputs.to_tag }})
|
||||
|
||||
# Run the comparison script
|
||||
CHANGELOG_OUTPUT=$(node scripts/compare-api-snapshots.js \
|
||||
.api-snapshots/from.json \
|
||||
.api-snapshots/to.json \
|
||||
${{ steps.validate_versions.outputs.from_version }} \
|
||||
${{ steps.validate_versions.outputs.to_version }} \
|
||||
Comfy-Org \
|
||||
ComfyUI_frontend \
|
||||
"$GIT_REF")
|
||||
|
||||
# Save changelog to file for artifact
|
||||
echo "$CHANGELOG_OUTPUT" > .api-snapshots/CHANGELOG-${{ steps.validate_versions.outputs.from_version }}-to-${{ steps.validate_versions.outputs.to_version }}.md
|
||||
|
||||
# Also output to step summary
|
||||
echo "## 📊 Generated API Changelog" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$CHANGELOG_OUTPUT" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Check if changelog is empty or just header
|
||||
if [ $(echo "$CHANGELOG_OUTPUT" | wc -l) -lt 5 ]; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "⚠️ No significant API changes detected" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ API changes detected and documented" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "✅ Changelog generated successfully"
|
||||
|
||||
- name: Upload changelog artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: api-changelog-v${{ steps.validate_versions.outputs.from_version }}-to-v${{ steps.validate_versions.outputs.to_version }}
|
||||
path: |
|
||||
.api-snapshots/CHANGELOG-*.md
|
||||
.api-snapshots/from.json
|
||||
.api-snapshots/to.json
|
||||
retention-days: 90
|
||||
|
||||
- name: Create Pull Request
|
||||
if: github.event.inputs.create_pr == 'true' && steps.generate_changelog.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[docs] Update API changelog for v${{ steps.validate_versions.outputs.from_version }} → v${{ steps.validate_versions.outputs.to_version }}'
|
||||
title: '[docs] API Changelog: v${{ steps.validate_versions.outputs.from_version }} → v${{ steps.validate_versions.outputs.to_version }}'
|
||||
body: |
|
||||
## API Changelog Update (Manual)
|
||||
|
||||
This PR documents public API changes between v${{ steps.validate_versions.outputs.from_version }} and v${{ steps.validate_versions.outputs.to_version }}.
|
||||
|
||||
The changelog has been manually generated by comparing TypeScript type definitions between versions.
|
||||
|
||||
### Version Comparison
|
||||
- **From:** v${{ steps.validate_versions.outputs.from_version }}
|
||||
- **To:** v${{ steps.validate_versions.outputs.to_version }}
|
||||
- **Requested by:** @${{ github.actor }}
|
||||
- **Workflow run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
### Review Instructions
|
||||
- Review the changes in `docs/API-CHANGELOG.md`
|
||||
- Verify accuracy of breaking changes
|
||||
- Add any additional context or migration notes if needed
|
||||
- Merge when ready to publish changelog
|
||||
|
||||
### Artifacts
|
||||
The full changelog and snapshots are available as workflow artifacts for 90 days.
|
||||
|
||||
---
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
branch: api-changelog-manual-${{ steps.validate_versions.outputs.from_version }}-to-${{ steps.validate_versions.outputs.to_version }}
|
||||
base: ${{ github.ref_name }}
|
||||
labels: documentation
|
||||
delete-branch: true
|
||||
draft: true
|
||||
add-paths: |
|
||||
docs/API-CHANGELOG.md
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## 🎉 Workflow Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **From version:** v${{ steps.validate_versions.outputs.from_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **To version:** v${{ steps.validate_versions.outputs.to_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Changes detected:** ${{ steps.generate_changelog.outputs.has_changes }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Create PR:** ${{ github.event.inputs.create_pr }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📦 Artifact" >> $GITHUB_STEP_SUMMARY
|
||||
echo "The generated changelog and API snapshots have been uploaded as artifacts." >> $GITHUB_STEP_SUMMARY
|
||||
echo "Retention: 90 days" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${{ github.event.inputs.create_pr }}" == "true" ] && [ "${{ steps.generate_changelog.outputs.has_changes }}" == "true" ]; then
|
||||
echo "### 🔀 Pull Request" >> $GITHUB_STEP_SUMMARY
|
||||
echo "A draft pull request has been created with the changelog updates." >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ github.event.inputs.create_pr }}" == "true" ]; then
|
||||
echo "### ℹ️ No PR Created" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No significant changes were detected, so no PR was created." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "### ℹ️ PR Creation Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Pull request creation was not requested. Enable 'Create PR' option to automatically create a PR." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
33
.github/workflows/pr-claude-review.yaml
vendored
@@ -17,9 +17,40 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
wait-for-ci:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'claude-review'
|
||||
outputs:
|
||||
should-proceed: ${{ steps.check-status.outputs.proceed }}
|
||||
steps:
|
||||
- name: Wait for other CI checks
|
||||
uses: lewagon/wait-on-check-action@e106e5c43e8ca1edea6383a39a01c5ca495fd812
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
check-regexp: '^(lint-and-format|test|playwright-tests)'
|
||||
allowed-conclusions: success,skipped,failure,cancelled,neutral,action_required,timed_out,stale
|
||||
wait-interval: 30
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if we should proceed
|
||||
id: check-status
|
||||
run: |
|
||||
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format")) | {name, conclusion}')
|
||||
|
||||
if echo "$CHECK_RUNS" | grep -Eq '"conclusion": "(failure|cancelled|timed_out|action_required)"'; then
|
||||
echo "Some CI checks failed - skipping Claude review"
|
||||
echo "proceed=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "All CI checks passed - proceeding with Claude review"
|
||||
echo "proceed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
claude-review:
|
||||
needs: wait-for-ci
|
||||
if: needs.wait-for-ci.outputs.should-proceed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
203
.github/workflows/release-api-changelogs.yaml
vendored
@@ -1,203 +0,0 @@
|
||||
name: Release API Changelogs
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Release NPM Types']
|
||||
types:
|
||||
- completed
|
||||
push:
|
||||
branches:
|
||||
- sno-api-changelog
|
||||
|
||||
concurrency:
|
||||
group: release-api-changelogs-${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
generate_changelog:
|
||||
name: Generate API Changelog
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on successful completion of the Release NPM Types workflow or on push to sno-api-changelog
|
||||
if: ${{ github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for comparing versions
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Current version: $VERSION"
|
||||
|
||||
- name: Get previous version
|
||||
id: previous_version
|
||||
run: |
|
||||
# Get the two most recent version tags sorted
|
||||
CURRENT_VERSION="${{ steps.current_version.outputs.version }}"
|
||||
TAGS=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -2)
|
||||
|
||||
# Find the previous version tag (skip current if it exists)
|
||||
PREVIOUS_TAG=""
|
||||
for tag in $TAGS; do
|
||||
TAG_VERSION=${tag#v}
|
||||
if [ "$TAG_VERSION" != "$CURRENT_VERSION" ]; then
|
||||
PREVIOUS_TAG=$tag
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PREVIOUS_TAG" ]; then
|
||||
echo "No previous version found, this may be the first release"
|
||||
echo "version=" >> $GITHUB_OUTPUT
|
||||
echo "tag=" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "version=${PREVIOUS_TAG#v}" >> $GITHUB_OUTPUT
|
||||
echo "tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Previous version: ${PREVIOUS_TAG#v}"
|
||||
fi
|
||||
|
||||
- name: Build current types
|
||||
run: pnpm build:types
|
||||
|
||||
- name: Snapshot current API
|
||||
id: current_snapshot
|
||||
run: |
|
||||
# Create snapshots directory
|
||||
mkdir -p .api-snapshots
|
||||
|
||||
# Generate snapshot of current types
|
||||
node scripts/snapshot-api.js dist/index.d.ts > .api-snapshots/current.json
|
||||
|
||||
echo "Current API snapshot created"
|
||||
|
||||
- name: Preserve scripts for previous version
|
||||
if: steps.previous_version.outputs.tag != ''
|
||||
run: |
|
||||
# Copy scripts to temporary location to use with previous version
|
||||
mkdir -p /tmp/api-changelog-scripts
|
||||
cp scripts/snapshot-api.js scripts/compare-api-snapshots.js /tmp/api-changelog-scripts/
|
||||
|
||||
- name: Checkout previous version
|
||||
if: steps.previous_version.outputs.tag != ''
|
||||
run: |
|
||||
# Stash current changes
|
||||
git stash
|
||||
|
||||
# Checkout previous version
|
||||
git checkout ${{ steps.previous_version.outputs.tag }}
|
||||
|
||||
# Restore scripts
|
||||
mkdir -p scripts
|
||||
cp /tmp/api-changelog-scripts/*.js scripts/
|
||||
|
||||
- name: Build previous types
|
||||
if: steps.previous_version.outputs.tag != ''
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build:types
|
||||
|
||||
- name: Snapshot previous API
|
||||
if: steps.previous_version.outputs.tag != ''
|
||||
run: |
|
||||
# Generate snapshot of previous types
|
||||
node scripts/snapshot-api.js dist/index.d.ts > .api-snapshots/previous.json
|
||||
|
||||
echo "Previous API snapshot created"
|
||||
|
||||
- name: Return to current version
|
||||
if: steps.previous_version.outputs.tag != ''
|
||||
run: |
|
||||
# Remove copied scripts to avoid conflicts
|
||||
rm -f scripts/snapshot-api.js scripts/compare-api-snapshots.js
|
||||
|
||||
git checkout -
|
||||
git stash pop || true
|
||||
|
||||
- name: Compare API snapshots and generate changelog
|
||||
id: generate_changelog
|
||||
run: |
|
||||
# Create docs directory if it doesn't exist
|
||||
mkdir -p docs
|
||||
|
||||
# Get current git ref (commit SHA)
|
||||
GIT_REF=$(git rev-parse HEAD)
|
||||
|
||||
# Run the comparison script
|
||||
if [ -f .api-snapshots/previous.json ]; then
|
||||
node scripts/compare-api-snapshots.js \
|
||||
.api-snapshots/previous.json \
|
||||
.api-snapshots/current.json \
|
||||
${{ steps.previous_version.outputs.version }} \
|
||||
${{ steps.current_version.outputs.version }} \
|
||||
Comfy-Org \
|
||||
ComfyUI_frontend \
|
||||
"$GIT_REF" \
|
||||
>> docs/API-CHANGELOG.md
|
||||
else
|
||||
# First release - just document the initial API surface
|
||||
echo "## v${{ steps.current_version.outputs.version }} ($(date +%Y-%m-%d))" >> docs/API-CHANGELOG.md
|
||||
echo "" >> docs/API-CHANGELOG.md
|
||||
echo "Initial API release." >> docs/API-CHANGELOG.md
|
||||
echo "" >> docs/API-CHANGELOG.md
|
||||
fi
|
||||
|
||||
# Check if there are any changes
|
||||
if git diff --quiet docs/API-CHANGELOG.md; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "No API changes detected"
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "API changes detected"
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.generate_changelog.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[docs] Update API changelog for v${{ steps.current_version.outputs.version }}'
|
||||
title: '[docs] API Changelog for v${{ steps.current_version.outputs.version }}'
|
||||
body: |
|
||||
## API Changelog Update
|
||||
|
||||
This PR documents public API changes between v${{ steps.previous_version.outputs.version }} and v${{ steps.current_version.outputs.version }}.
|
||||
|
||||
The changelog has been automatically generated by comparing TypeScript type definitions between versions.
|
||||
|
||||
### Review Instructions
|
||||
- Review the changes in `docs/API-CHANGELOG.md`
|
||||
- Verify accuracy of breaking changes
|
||||
- Add any additional context or migration notes if needed
|
||||
- Merge when ready to publish changelog
|
||||
|
||||
---
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
branch: api-changelog-v${{ steps.current_version.outputs.version }}
|
||||
base: ${{ github.event_name == 'push' && github.ref_name || 'main' }}
|
||||
labels: documentation
|
||||
delete-branch: true
|
||||
draft: true
|
||||
add-paths: |
|
||||
docs/API-CHANGELOG.md
|
||||
4
.gitignore
vendored
@@ -18,7 +18,6 @@ yarn.lock
|
||||
.stylelintcache
|
||||
|
||||
node_modules
|
||||
.pnpm-store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
@@ -93,6 +92,3 @@ storybook-static
|
||||
.github/instructions/nx.instructions.md
|
||||
vite.config.*.timestamp*
|
||||
vitest.config.*.timestamp*
|
||||
|
||||
# Weekly docs check output
|
||||
/output.txt
|
||||
|
||||
@@ -16,7 +16,8 @@ export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
|
||||
execute: async () => await electron.setBasePath(),
|
||||
name: 'Base path',
|
||||
shortDescription: 'Change the application base path.',
|
||||
errorDescription: 'Unable to open the base path. Please select a new one.',
|
||||
errorDescription:
|
||||
'The current base path is invalid or unsafe. Please select a new location.',
|
||||
description:
|
||||
'The base path is the default location where ComfyUI stores data. It is the location for the python environment, and may also contain models, custom nodes, and other extensions.',
|
||||
isInstallationFix: true,
|
||||
|
||||
@@ -85,6 +85,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
const electron = electronAPI()
|
||||
|
||||
// Reactive state
|
||||
const lastUpdate = ref<InstallValidation | null>(null)
|
||||
const isRefreshing = ref(false)
|
||||
const isRunningTerminalCommand = computed(() =>
|
||||
tasks.value
|
||||
@@ -97,6 +98,13 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
.some((task) => getRunner(task)?.executing)
|
||||
)
|
||||
|
||||
const unsafeBasePath = computed(
|
||||
() => lastUpdate.value?.unsafeBasePath === true
|
||||
)
|
||||
const unsafeBasePathReason = computed(
|
||||
() => lastUpdate.value?.unsafeBasePathReason
|
||||
)
|
||||
|
||||
// Task list
|
||||
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
|
||||
|
||||
@@ -123,6 +131,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
* @param validationUpdate Update details passed in by electron
|
||||
*/
|
||||
const processUpdate = (validationUpdate: InstallValidation) => {
|
||||
lastUpdate.value = validationUpdate
|
||||
const update = validationUpdate as IndexedUpdate
|
||||
isRefreshing.value = true
|
||||
|
||||
@@ -155,7 +164,11 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
}
|
||||
|
||||
const execute = async (task: MaintenanceTask) => {
|
||||
return getRunner(task).execute(task)
|
||||
const success = await getRunner(task).execute(task)
|
||||
if (success && task.isInstallationFix) {
|
||||
await refreshDesktopTasks()
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -163,6 +176,8 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
isRefreshing,
|
||||
isRunningTerminalCommand,
|
||||
isRunningInstallationFix,
|
||||
unsafeBasePath,
|
||||
unsafeBasePathReason,
|
||||
execute,
|
||||
getRunner,
|
||||
processUpdate,
|
||||
|
||||
159
apps/desktop-ui/src/views/MaintenanceView.stories.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
type UnsafeReason = 'appInstallDir' | 'updaterCache' | 'oneDrive' | null
|
||||
type ValidationIssueState = 'OK' | 'warning' | 'error' | 'skipped'
|
||||
|
||||
type ValidationState = {
|
||||
inProgress: boolean
|
||||
installState: string
|
||||
basePath?: ValidationIssueState
|
||||
unsafeBasePath: boolean
|
||||
unsafeBasePathReason: UnsafeReason
|
||||
venvDirectory?: ValidationIssueState
|
||||
pythonInterpreter?: ValidationIssueState
|
||||
pythonPackages?: ValidationIssueState
|
||||
uv?: ValidationIssueState
|
||||
git?: ValidationIssueState
|
||||
vcRedist?: ValidationIssueState
|
||||
upgradePackages?: ValidationIssueState
|
||||
}
|
||||
|
||||
const validationState: ValidationState = {
|
||||
inProgress: false,
|
||||
installState: 'installed',
|
||||
basePath: 'OK',
|
||||
unsafeBasePath: false,
|
||||
unsafeBasePathReason: null,
|
||||
venvDirectory: 'OK',
|
||||
pythonInterpreter: 'OK',
|
||||
pythonPackages: 'OK',
|
||||
uv: 'OK',
|
||||
git: 'OK',
|
||||
vcRedist: 'OK',
|
||||
upgradePackages: 'OK'
|
||||
}
|
||||
|
||||
const createMockElectronAPI = () => {
|
||||
const logListeners: Array<(message: string) => void> = []
|
||||
|
||||
const getValidationUpdate = () => ({
|
||||
...validationState
|
||||
})
|
||||
|
||||
return {
|
||||
getPlatform: () => 'darwin',
|
||||
changeTheme: (_theme: unknown) => {},
|
||||
onLogMessage: (listener: (message: string) => void) => {
|
||||
logListeners.push(listener)
|
||||
},
|
||||
showContextMenu: (_options: unknown) => {},
|
||||
Events: {
|
||||
trackEvent: (_eventName: string, _data?: unknown) => {}
|
||||
},
|
||||
Validation: {
|
||||
onUpdate: (_callback: (update: unknown) => void) => {},
|
||||
async getStatus() {
|
||||
return getValidationUpdate()
|
||||
},
|
||||
async validateInstallation(callback: (update: unknown) => void) {
|
||||
callback(getValidationUpdate())
|
||||
},
|
||||
async complete() {
|
||||
// Only allow completion when the base path is safe
|
||||
return !validationState.unsafeBasePath
|
||||
},
|
||||
dispose: () => {}
|
||||
},
|
||||
setBasePath: () => Promise.resolve(true),
|
||||
reinstall: () => Promise.resolve(),
|
||||
uv: {
|
||||
installRequirements: () => Promise.resolve(),
|
||||
clearCache: () => Promise.resolve(),
|
||||
resetVenv: () => Promise.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ensureElectronAPI = () => {
|
||||
const globalWindow = window as unknown as { electronAPI?: unknown }
|
||||
if (!globalWindow.electronAPI) {
|
||||
globalWindow.electronAPI = createMockElectronAPI()
|
||||
}
|
||||
|
||||
return globalWindow.electronAPI
|
||||
}
|
||||
|
||||
const MaintenanceView = defineAsyncComponent(async () => {
|
||||
ensureElectronAPI()
|
||||
const module = await import('./MaintenanceView.vue')
|
||||
return module.default
|
||||
})
|
||||
|
||||
const meta: Meta<typeof MaintenanceView> = {
|
||||
title: 'Desktop/Views/MaintenanceView',
|
||||
component: MaintenanceView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'neutral-900', value: '#171717' },
|
||||
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
name: 'All tasks OK',
|
||||
render: () => ({
|
||||
components: { MaintenanceView },
|
||||
setup() {
|
||||
validationState.inProgress = false
|
||||
validationState.installState = 'installed'
|
||||
validationState.basePath = 'OK'
|
||||
validationState.unsafeBasePath = false
|
||||
validationState.unsafeBasePathReason = null
|
||||
validationState.venvDirectory = 'OK'
|
||||
validationState.pythonInterpreter = 'OK'
|
||||
validationState.pythonPackages = 'OK'
|
||||
validationState.uv = 'OK'
|
||||
validationState.git = 'OK'
|
||||
validationState.vcRedist = 'OK'
|
||||
validationState.upgradePackages = 'OK'
|
||||
ensureElectronAPI()
|
||||
return {}
|
||||
},
|
||||
template: '<MaintenanceView />'
|
||||
})
|
||||
}
|
||||
|
||||
export const UnsafeBasePathOneDrive: Story = {
|
||||
name: 'Unsafe base path (OneDrive)',
|
||||
render: () => ({
|
||||
components: { MaintenanceView },
|
||||
setup() {
|
||||
validationState.inProgress = false
|
||||
validationState.installState = 'installed'
|
||||
validationState.basePath = 'error'
|
||||
validationState.unsafeBasePath = true
|
||||
validationState.unsafeBasePathReason = 'oneDrive'
|
||||
validationState.venvDirectory = 'OK'
|
||||
validationState.pythonInterpreter = 'OK'
|
||||
validationState.pythonPackages = 'OK'
|
||||
validationState.uv = 'OK'
|
||||
validationState.git = 'OK'
|
||||
validationState.vcRedist = 'OK'
|
||||
validationState.upgradePackages = 'OK'
|
||||
ensureElectronAPI()
|
||||
return {}
|
||||
},
|
||||
template: '<MaintenanceView />'
|
||||
})
|
||||
}
|
||||
@@ -47,6 +47,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unsafe migration warning -->
|
||||
<div v-if="taskStore.unsafeBasePath" class="my-4">
|
||||
<p class="flex items-start gap-3 text-neutral-300">
|
||||
<Tag
|
||||
icon="pi pi-exclamation-triangle"
|
||||
severity="warn"
|
||||
:value="t('icon.exclamation-triangle')"
|
||||
/>
|
||||
<span>
|
||||
<strong class="block mb-1">
|
||||
{{ t('maintenance.unsafeMigration.title') }}
|
||||
</strong>
|
||||
<span class="block mb-1">
|
||||
{{ unsafeReasonText }}
|
||||
</span>
|
||||
<span class="block text-sm text-neutral-400">
|
||||
{{ t('maintenance.unsafeMigration.action') }}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Tasks -->
|
||||
<TaskListPanel
|
||||
class="border-neutral-700 border-solid border-x-0 border-y"
|
||||
@@ -89,10 +111,10 @@
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import Button from 'primevue/button'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import Tag from 'primevue/tag'
|
||||
import Toast from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import RefreshButton from '@/components/common/RefreshButton.vue'
|
||||
import StatusTag from '@/components/maintenance/StatusTag.vue'
|
||||
@@ -139,6 +161,27 @@ const filterOptions = ref([
|
||||
/** Filter binding; can be set to show all tasks, or only errors. */
|
||||
const filter = ref<MaintenanceFilter>(filterOptions.value[0])
|
||||
|
||||
const unsafeReasonText = computed(() => {
|
||||
const reason = taskStore.unsafeBasePathReason
|
||||
if (!reason) {
|
||||
return t('maintenance.unsafeMigration.generic')
|
||||
}
|
||||
|
||||
if (reason === 'appInstallDir') {
|
||||
return t('maintenance.unsafeMigration.appInstallDir')
|
||||
}
|
||||
|
||||
if (reason === 'updaterCache') {
|
||||
return t('maintenance.unsafeMigration.updaterCache')
|
||||
}
|
||||
|
||||
if (reason === 'oneDrive') {
|
||||
return t('maintenance.unsafeMigration.oneDrive')
|
||||
}
|
||||
|
||||
return t('maintenance.unsafeMigration.generic')
|
||||
})
|
||||
|
||||
/** If valid, leave the validation window. */
|
||||
const completeValidation = async () => {
|
||||
const isValid = await electron.Validation.complete()
|
||||
|
||||
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 82 KiB |
@@ -1,49 +0,0 @@
|
||||
## v1.30.2 (2025-11-01)
|
||||
|
||||
Comparing v1.29.0 → v1.30.2. This changelog documents changes to the public API surface that third-party extensions and custom nodes depend on.
|
||||
|
||||
### ✨ Additions
|
||||
|
||||
**Type Aliases**
|
||||
|
||||
- `WorkflowId`
|
||||
|
||||
**Interfaces**
|
||||
|
||||
- `ExtensionMetadata`
|
||||
- Members: `id`, `name`, `version`, `description`
|
||||
|
||||
### 🔄 Modifications
|
||||
|
||||
> **Note**: Some modifications may be breaking changes.
|
||||
|
||||
**Interfaces**
|
||||
|
||||
- `ComfyApi`
|
||||
- ✨ Added member: `queuePromptAsync`
|
||||
- ✨ Added member: `cancelPrompt`
|
||||
- ✨ Added member: `getQueueStatus`
|
||||
- ⚠️ **Breaking**: Removed member: `queuePrompt`
|
||||
- `NodeDef`
|
||||
- ✨ Added member: `input`
|
||||
- ✨ Added member: `output`
|
||||
- ✨ Added member: `output_name`
|
||||
- `WorkflowMetadata`
|
||||
- ✨ Added member: `tags`
|
||||
- ✨ Added member: `thumbnail`
|
||||
|
||||
**Enums**
|
||||
|
||||
- `NodeStatus`
|
||||
- ✨ Added enum value: `ERROR`
|
||||
- ✨ Added enum value: `COMPLETED`
|
||||
|
||||
**Classes**
|
||||
|
||||
- `WorkflowManager`
|
||||
- ✨ Added member: `cache`
|
||||
- ✨ Added method: `deleteWorkflow()`
|
||||
- ✨ Added method: `searchWorkflows()`
|
||||
|
||||
---
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
# API Changelog Generation Demo
|
||||
|
||||
This demo showcases the automated API changelog generation system comparing two versions of the ComfyUI Frontend public API.
|
||||
|
||||
## Overview
|
||||
|
||||
The demo compares **v1.29.0** → **v1.30.2** to demonstrate:
|
||||
- Breaking change detection
|
||||
- API additions tracking
|
||||
- Non-breaking modifications
|
||||
- Human-readable changelog generation
|
||||
|
||||
## Demo Files
|
||||
|
||||
### Input Files
|
||||
- **`v1.29.0.d.ts`** - TypeScript definitions representing the v1.29.0 API surface
|
||||
- **`v1.30.2.d.ts`** - TypeScript definitions representing the v1.30.2 API surface
|
||||
|
||||
### Generated Files
|
||||
- **`v1.29.0.json`** - Structured API snapshot from v1.29.0
|
||||
- **`v1.30.2.json`** - Structured API snapshot from v1.30.2
|
||||
- **`CHANGELOG-DEMO.md`** - Generated changelog comparing the two versions
|
||||
|
||||
## Running the Demo
|
||||
|
||||
```bash
|
||||
# Generate API snapshots
|
||||
node scripts/snapshot-api.js demo-snapshots/v1.29.0.d.ts > demo-snapshots/v1.29.0.json
|
||||
node scripts/snapshot-api.js demo-snapshots/v1.30.2.d.ts > demo-snapshots/v1.30.2.json
|
||||
|
||||
# Compare snapshots and generate changelog
|
||||
node scripts/compare-api-snapshots.js \
|
||||
demo-snapshots/v1.29.0.json \
|
||||
demo-snapshots/v1.30.2.json \
|
||||
1.29.0 \
|
||||
1.30.2 \
|
||||
> demo-snapshots/CHANGELOG-DEMO.md
|
||||
```
|
||||
|
||||
## Key Changes Detected
|
||||
|
||||
### ⚠️ Breaking Changes
|
||||
|
||||
1. **`ComfyApi.queuePrompt()` removed**
|
||||
- Replaced with `queuePromptAsync()` which includes additional options
|
||||
- Extension developers need to update their code to use the new async method
|
||||
|
||||
### ✨ New Additions
|
||||
|
||||
1. **New Interface: `ExtensionMetadata`**
|
||||
- Provides metadata for extensions
|
||||
- Fields: `id`, `name`, `version`, `description`
|
||||
|
||||
2. **New Type: `WorkflowId`**
|
||||
- Type alias for workflow identifiers
|
||||
|
||||
3. **Enhanced `ComfyApi` Interface**
|
||||
- `queuePromptAsync()` - Async queue with priority support
|
||||
- `cancelPrompt()` - Cancel queued prompts
|
||||
- `getQueueStatus()` - Query queue state
|
||||
|
||||
4. **Extended `NodeDef` Interface**
|
||||
- `input` - Input specification
|
||||
- `output` - Output types
|
||||
- `output_name` - Output names
|
||||
|
||||
5. **Enhanced `NodeStatus` Enum**
|
||||
- Added `ERROR` state
|
||||
- Added `COMPLETED` state
|
||||
|
||||
6. **Extended `WorkflowManager` Class**
|
||||
- `cache` property for workflow caching
|
||||
- `deleteWorkflow()` method
|
||||
- `searchWorkflows()` method
|
||||
|
||||
### 🔄 Non-Breaking Modifications
|
||||
|
||||
1. **`WorkflowMetadata` enhancements**
|
||||
- Added optional `tags` field
|
||||
- Added optional `thumbnail` field
|
||||
|
||||
## Real-World Usage
|
||||
|
||||
In production, this system will:
|
||||
|
||||
1. **Automatic Triggering**: Run after each NPM types release
|
||||
2. **Version Detection**: Automatically detect current and previous versions from git tags
|
||||
3. **Build Integration**: Build actual TypeScript types from the repository
|
||||
4. **PR Creation**: Generate draft pull requests with the changelog
|
||||
5. **Human Review**: Allow maintainers to review and enhance before merging
|
||||
|
||||
## Benefits for Extension Developers
|
||||
|
||||
### Clear Breaking Change Visibility
|
||||
Extension developers can immediately see:
|
||||
- What APIs were removed
|
||||
- What signatures changed
|
||||
- How to migrate their code
|
||||
|
||||
### Migration Planning
|
||||
With clear documentation of additions and changes, developers can:
|
||||
- Plan updates around breaking changes
|
||||
- Adopt new features when ready
|
||||
- Understand version compatibility
|
||||
|
||||
### Historical Reference
|
||||
The cumulative `docs/API-CHANGELOG.md` provides:
|
||||
- Complete API evolution history
|
||||
- Context for design decisions
|
||||
- Migration guides for major versions
|
||||
|
||||
## Example Extension Migration
|
||||
|
||||
### Before (v1.29.0)
|
||||
```typescript
|
||||
// Old code using queuePrompt
|
||||
const result = await api.queuePrompt(workflow);
|
||||
console.log('Queued:', result.prompt_id);
|
||||
```
|
||||
|
||||
### After (v1.30.2)
|
||||
```typescript
|
||||
// New code using queuePromptAsync with priority
|
||||
const result = await api.queuePromptAsync(workflow, { priority: 1 });
|
||||
console.log('Queued:', result.prompt_id, 'Position:', result.number);
|
||||
```
|
||||
|
||||
## Snapshot Structure
|
||||
|
||||
The JSON snapshots contain structured representations of:
|
||||
|
||||
```json
|
||||
{
|
||||
"types": { /* Type aliases */ },
|
||||
"interfaces": { /* Interface definitions with members */ },
|
||||
"enums": { /* Enum values */ },
|
||||
"functions": { /* Exported functions */ },
|
||||
"classes": { /* Class definitions with methods */ },
|
||||
"constants": { /* Exported constants */ }
|
||||
}
|
||||
```
|
||||
|
||||
Each entry includes:
|
||||
- **Name**: Identifier
|
||||
- **Kind**: Type of declaration
|
||||
- **Members/Methods**: Properties and functions
|
||||
- **Types**: Parameter and return types
|
||||
- **Visibility**: Public/private/protected modifiers
|
||||
- **Optional**: Whether parameters/properties are optional
|
||||
|
||||
## Comparison Algorithm
|
||||
|
||||
The comparison script:
|
||||
|
||||
1. **Categorizes changes** into breaking, additions, and modifications
|
||||
2. **Detects breaking changes**:
|
||||
- Removed interfaces, classes, functions
|
||||
- Removed methods or properties
|
||||
- Changed method signatures
|
||||
- Changed return types
|
||||
- Removed enum values
|
||||
3. **Tracks additions**:
|
||||
- New interfaces, classes, types
|
||||
- New methods and properties
|
||||
- New enum values
|
||||
4. **Identifies modifications**:
|
||||
- Type changes
|
||||
- Optionality changes
|
||||
- Signature changes
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned improvements include:
|
||||
|
||||
- **LLM Enhancement**: Use AI to generate better descriptions and migration guides
|
||||
- **Email Notifications**: Alert developers on mailing list for major changes
|
||||
- **Release Notes Integration**: Auto-include in GitHub releases
|
||||
- **Deprecation Tracking**: Mark APIs as deprecated before removal
|
||||
- **Example Code**: Generate migration code snippets automatically
|
||||
|
||||
## Conclusion
|
||||
|
||||
This automated system ensures:
|
||||
- ✅ Zero manual effort for changelog generation
|
||||
- ✅ Consistent documentation format
|
||||
- ✅ Clear breaking change visibility
|
||||
- ✅ Historical API evolution tracking
|
||||
- ✅ Better extension developer experience
|
||||
58
demo-snapshots/v1.29.0.d.ts
vendored
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Mock TypeScript definitions representing v1.29.0 API surface
|
||||
* This represents the public API as it existed in version 1.29.0
|
||||
*/
|
||||
|
||||
export interface ComfyApi {
|
||||
/**
|
||||
* Get API URL for backend calls
|
||||
*/
|
||||
apiURL(path: string): string
|
||||
|
||||
/**
|
||||
* Get file URL for static resources
|
||||
*/
|
||||
fileURL(path: string): string
|
||||
|
||||
/**
|
||||
* Queue a prompt for execution
|
||||
*/
|
||||
queuePrompt(prompt: object): Promise<{ prompt_id: string }>
|
||||
|
||||
/**
|
||||
* Interrupt current execution
|
||||
*/
|
||||
interrupt(): Promise<void>
|
||||
}
|
||||
|
||||
export interface NodeDef {
|
||||
name: string
|
||||
category: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
python_module: string
|
||||
}
|
||||
|
||||
export enum NodeStatus {
|
||||
IDLE = 'idle',
|
||||
QUEUED = 'queued',
|
||||
RUNNING = 'running'
|
||||
}
|
||||
|
||||
export interface WorkflowMetadata {
|
||||
title?: string
|
||||
description?: string
|
||||
author?: string
|
||||
version?: string
|
||||
}
|
||||
|
||||
export class WorkflowManager {
|
||||
workflows: Map<string, object>
|
||||
|
||||
constructor()
|
||||
|
||||
loadWorkflow(id: string): Promise<object>
|
||||
saveWorkflow(id: string, data: object): Promise<void>
|
||||
}
|
||||
|
||||
export type NodeId = string
|
||||
@@ -1,192 +0,0 @@
|
||||
{
|
||||
"types": {
|
||||
"NodeId": {
|
||||
"kind": "type",
|
||||
"name": "NodeId",
|
||||
"text": "export type NodeId = string;",
|
||||
"exported": true
|
||||
}
|
||||
},
|
||||
"interfaces": {
|
||||
"ComfyApi": {
|
||||
"kind": "interface",
|
||||
"name": "ComfyApi",
|
||||
"members": [
|
||||
{
|
||||
"name": "apiURL",
|
||||
"kind": "method",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "path",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "string"
|
||||
},
|
||||
{
|
||||
"name": "fileURL",
|
||||
"kind": "method",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "path",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "string"
|
||||
},
|
||||
{
|
||||
"name": "queuePrompt",
|
||||
"kind": "method",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "prompt",
|
||||
"type": "object",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "Promise<{ prompt_id: string }>"
|
||||
},
|
||||
{
|
||||
"name": "interrupt",
|
||||
"kind": "method",
|
||||
"parameters": [],
|
||||
"returnType": "Promise<void>"
|
||||
}
|
||||
],
|
||||
"exported": true,
|
||||
"heritage": []
|
||||
},
|
||||
"NodeDef": {
|
||||
"kind": "interface",
|
||||
"name": "NodeDef",
|
||||
"members": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "display_name",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "python_module",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"exported": true,
|
||||
"heritage": []
|
||||
},
|
||||
"WorkflowMetadata": {
|
||||
"kind": "interface",
|
||||
"name": "WorkflowMetadata",
|
||||
"members": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
],
|
||||
"exported": true,
|
||||
"heritage": []
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"NodeStatus": {
|
||||
"kind": "enum",
|
||||
"name": "NodeStatus",
|
||||
"members": [
|
||||
{
|
||||
"name": "IDLE",
|
||||
"value": "\"idle\""
|
||||
},
|
||||
{
|
||||
"name": "QUEUED",
|
||||
"value": "\"queued\""
|
||||
},
|
||||
{
|
||||
"name": "RUNNING",
|
||||
"value": "\"running\""
|
||||
}
|
||||
],
|
||||
"exported": true
|
||||
}
|
||||
},
|
||||
"functions": {},
|
||||
"classes": {
|
||||
"WorkflowManager": {
|
||||
"kind": "class",
|
||||
"name": "WorkflowManager",
|
||||
"members": [
|
||||
{
|
||||
"name": "workflows",
|
||||
"type": "Map<string, object>",
|
||||
"visibility": "public"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"name": "loadWorkflow",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "Promise<object>",
|
||||
"visibility": "public"
|
||||
},
|
||||
{
|
||||
"name": "saveWorkflow",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"type": "object",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "Promise<void>",
|
||||
"visibility": "public"
|
||||
}
|
||||
],
|
||||
"exported": true,
|
||||
"heritage": []
|
||||
}
|
||||
},
|
||||
"constants": {}
|
||||
}
|
||||
92
demo-snapshots/v1.30.2.d.ts
vendored
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* Mock TypeScript definitions representing v1.30.2 API surface
|
||||
* This represents the public API with several breaking changes and additions
|
||||
*/
|
||||
|
||||
export interface ComfyApi {
|
||||
/**
|
||||
* Get API URL for backend calls
|
||||
*/
|
||||
apiURL(path: string): string
|
||||
|
||||
/**
|
||||
* Get file URL for static resources
|
||||
*/
|
||||
fileURL(path: string): string
|
||||
|
||||
/**
|
||||
* Queue a prompt for execution (async version)
|
||||
*/
|
||||
queuePromptAsync(
|
||||
prompt: object,
|
||||
options?: { priority?: number }
|
||||
): Promise<{ prompt_id: string; number: number }>
|
||||
|
||||
/**
|
||||
* Cancel a queued prompt
|
||||
*/
|
||||
cancelPrompt(prompt_id: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Interrupt current execution
|
||||
*/
|
||||
interrupt(): Promise<void>
|
||||
|
||||
/**
|
||||
* Get queue status
|
||||
*/
|
||||
getQueueStatus(): Promise<{ queue_running: any[]; queue_pending: any[] }>
|
||||
}
|
||||
|
||||
export interface NodeDef {
|
||||
name: string
|
||||
category: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
python_module: string
|
||||
input: {
|
||||
required?: Record<string, any>
|
||||
optional?: Record<string, any>
|
||||
}
|
||||
output: string[]
|
||||
output_name: string[]
|
||||
}
|
||||
|
||||
export enum NodeStatus {
|
||||
IDLE = 'idle',
|
||||
QUEUED = 'queued',
|
||||
RUNNING = 'running',
|
||||
ERROR = 'error',
|
||||
COMPLETED = 'completed'
|
||||
}
|
||||
|
||||
export interface WorkflowMetadata {
|
||||
title?: string
|
||||
description?: string
|
||||
author?: string
|
||||
version?: string
|
||||
tags?: string[]
|
||||
thumbnail?: string
|
||||
}
|
||||
|
||||
export interface ExtensionMetadata {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export class WorkflowManager {
|
||||
workflows: Map<string, object>
|
||||
cache: Map<string, object>
|
||||
|
||||
constructor()
|
||||
|
||||
loadWorkflow(id: string): Promise<object>
|
||||
saveWorkflow(id: string, data: object): Promise<void>
|
||||
deleteWorkflow(id: string): Promise<void>
|
||||
searchWorkflows(query: string): Promise<object[]>
|
||||
}
|
||||
|
||||
export type NodeId = string
|
||||
export type WorkflowId = string
|
||||
@@ -1,311 +0,0 @@
|
||||
{
|
||||
"types": {
|
||||
"NodeId": {
|
||||
"kind": "type",
|
||||
"name": "NodeId",
|
||||
"text": "export type NodeId = string;",
|
||||
"exported": true
|
||||
},
|
||||
"WorkflowId": {
|
||||
"kind": "type",
|
||||
"name": "WorkflowId",
|
||||
"text": "export type WorkflowId = string;",
|
||||
"exported": true
|
||||
}
|
||||
},
|
||||
"interfaces": {
|
||||
"ComfyApi": {
|
||||
"kind": "interface",
|
||||
"name": "ComfyApi",
|
||||
"members": [
|
||||
{
|
||||
"name": "apiURL",
|
||||
"kind": "method",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "path",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "string"
|
||||
},
|
||||
{
|
||||
"name": "fileURL",
|
||||
"kind": "method",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "path",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "string"
|
||||
},
|
||||
{
|
||||
"name": "queuePromptAsync",
|
||||
"kind": "method",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "prompt",
|
||||
"type": "object",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"type": "{ priority?: number }",
|
||||
"optional": true
|
||||
}
|
||||
],
|
||||
"returnType": "Promise<{ prompt_id: string; number: number }>"
|
||||
},
|
||||
{
|
||||
"name": "cancelPrompt",
|
||||
"kind": "method",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "prompt_id",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "Promise<void>"
|
||||
},
|
||||
{
|
||||
"name": "interrupt",
|
||||
"kind": "method",
|
||||
"parameters": [],
|
||||
"returnType": "Promise<void>"
|
||||
},
|
||||
{
|
||||
"name": "getQueueStatus",
|
||||
"kind": "method",
|
||||
"parameters": [],
|
||||
"returnType": "Promise<{ queue_running: any[]; queue_pending: any[] }>"
|
||||
}
|
||||
],
|
||||
"exported": true,
|
||||
"heritage": []
|
||||
},
|
||||
"NodeDef": {
|
||||
"kind": "interface",
|
||||
"name": "NodeDef",
|
||||
"members": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "display_name",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "python_module",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "input",
|
||||
"type": "{\n required?: Record<string, any>;\n optional?: Record<string, any>;\n }",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "output",
|
||||
"type": "string[]",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "output_name",
|
||||
"type": "string[]",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"exported": true,
|
||||
"heritage": []
|
||||
},
|
||||
"WorkflowMetadata": {
|
||||
"kind": "interface",
|
||||
"name": "WorkflowMetadata",
|
||||
"members": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"type": "string[]",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "thumbnail",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
],
|
||||
"exported": true,
|
||||
"heritage": []
|
||||
},
|
||||
"ExtensionMetadata": {
|
||||
"kind": "interface",
|
||||
"name": "ExtensionMetadata",
|
||||
"members": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
],
|
||||
"exported": true,
|
||||
"heritage": []
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"NodeStatus": {
|
||||
"kind": "enum",
|
||||
"name": "NodeStatus",
|
||||
"members": [
|
||||
{
|
||||
"name": "IDLE",
|
||||
"value": "\"idle\""
|
||||
},
|
||||
{
|
||||
"name": "QUEUED",
|
||||
"value": "\"queued\""
|
||||
},
|
||||
{
|
||||
"name": "RUNNING",
|
||||
"value": "\"running\""
|
||||
},
|
||||
{
|
||||
"name": "ERROR",
|
||||
"value": "\"error\""
|
||||
},
|
||||
{
|
||||
"name": "COMPLETED",
|
||||
"value": "\"completed\""
|
||||
}
|
||||
],
|
||||
"exported": true
|
||||
}
|
||||
},
|
||||
"functions": {},
|
||||
"classes": {
|
||||
"WorkflowManager": {
|
||||
"kind": "class",
|
||||
"name": "WorkflowManager",
|
||||
"members": [
|
||||
{
|
||||
"name": "workflows",
|
||||
"type": "Map<string, object>",
|
||||
"visibility": "public"
|
||||
},
|
||||
{
|
||||
"name": "cache",
|
||||
"type": "Map<string, object>",
|
||||
"visibility": "public"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"name": "loadWorkflow",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "Promise<object>",
|
||||
"visibility": "public"
|
||||
},
|
||||
{
|
||||
"name": "saveWorkflow",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"type": "object",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "Promise<void>",
|
||||
"visibility": "public"
|
||||
},
|
||||
{
|
||||
"name": "deleteWorkflow",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "Promise<void>",
|
||||
"visibility": "public"
|
||||
},
|
||||
{
|
||||
"name": "searchWorkflows",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "query",
|
||||
"type": "string",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"returnType": "Promise<object[]>",
|
||||
"visibility": "public"
|
||||
}
|
||||
],
|
||||
"exported": true,
|
||||
"heritage": []
|
||||
}
|
||||
},
|
||||
"constants": {}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
# Public API Changelog
|
||||
|
||||
This changelog documents changes to the ComfyUI Frontend public API surface across versions. The public API surface includes types, interfaces, and objects used by third-party extensions and custom nodes.
|
||||
|
||||
**Important**: This is an automatically generated changelog based on TypeScript type definitions. Breaking changes are marked with ⚠️.
|
||||
|
||||
## What is tracked
|
||||
|
||||
This changelog tracks changes to the following public API components exported from `@comfyorg/comfyui-frontend-types`:
|
||||
|
||||
- **Type Aliases**: Type definitions used by extensions
|
||||
- **Interfaces**: Object shapes and contracts
|
||||
- **Enums**: Enumerated values
|
||||
- **Functions**: Public utility functions
|
||||
- **Classes**: Exported classes and their public members
|
||||
- **Constants**: Public constant values
|
||||
|
||||
## Migration Guide
|
||||
|
||||
When breaking changes occur, refer to the specific version section below for:
|
||||
- What changed
|
||||
- Why it changed (if applicable)
|
||||
- How to migrate your code
|
||||
|
||||
---
|
||||
|
||||
<!-- Automated changelog entries will be added below -->
|
||||
## v1.32.1 (2025-11-05)
|
||||
|
||||
Comparing v1.32.0 → v1.32.1. This changelog documents changes to the public API surface that third-party extensions and custom nodes depend on.
|
||||
|
||||
### 🔄 Modifications
|
||||
|
||||
> **Note**: Some modifications may be breaking changes.
|
||||
|
||||
**Interfaces**
|
||||
|
||||
- [`ComfyCommand`](https://github.com/Comfy-Org/ComfyUI_frontend/blob/f844d3e95b52501b308aa399e3765f9ed79918cc/src/stores/commandStore.ts#L10)
|
||||
- ⚠️ **Breaking**: Member `function` type changed: `() => void | Promise<void>` → `(metadata?: Record<string, unknown>) => void | Promise<void>`
|
||||
- [`TemplateInfo`](https://github.com/Comfy-Org/ComfyUI_frontend/blob/f844d3e95b52501b308aa399e3765f9ed79918cc/src/platform/workflow/templates/types/template.ts#L1)
|
||||
- ✨ Added member: `openSource`
|
||||
|
||||
**Classes**
|
||||
|
||||
- [`ComfyApp`](https://github.com/Comfy-Org/ComfyUI_frontend/blob/f844d3e95b52501b308aa399e3765f9ed79918cc/src/scripts/app.ts#L123)
|
||||
- ⚠️ **Breaking**: Method `loadGraphData()` signature changed
|
||||
- ⚠️ **Breaking**: Method `handleFile()` signature changed
|
||||
- [`LGraphCanvas`](https://github.com/Comfy-Org/ComfyUI_frontend/blob/f844d3e95b52501b308aa399e3765f9ed79918cc/src/lib/litegraph/src/LGraphCanvas.ts#L250)
|
||||
- ⚠️ **Breaking**: Method `processMouseDown()` signature changed
|
||||
|
||||
---
|
||||
|
||||
@@ -58,8 +58,7 @@ export default defineConfig([
|
||||
'src/extensions/core/*',
|
||||
'src/scripts/*',
|
||||
'src/types/generatedManagerTypes.ts',
|
||||
'src/types/vue-shim.d.ts',
|
||||
'demo-snapshots/*'
|
||||
'src/types/vue-shim.d.ts'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -41,9 +41,7 @@ const config: KnipConfig = {
|
||||
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
|
||||
'packages/registry-types/src/comfyRegistryTypes.ts',
|
||||
// Used by a custom node (that should move off of this)
|
||||
'src/scripts/ui/components/splitButton.ts',
|
||||
// Demo snapshots for API changelog system
|
||||
'demo-snapshots/**'
|
||||
'src/scripts/ui/components/splitButton.ts'
|
||||
],
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.32.1",
|
||||
"version": "1.31.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
|
||||
--color-jade-400: #47e469;
|
||||
--color-jade-600: #00cd72;
|
||||
--color-graphite-400: #9C9EAB;
|
||||
|
||||
--color-gold-400: #fcbf64;
|
||||
--color-gold-500: #fdab34;
|
||||
@@ -158,8 +157,6 @@
|
||||
--button-surface: var(--color-white);
|
||||
--button-surface-contrast: var(--color-black);
|
||||
|
||||
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
|
||||
|
||||
--modal-card-button-surface: var(--color-smoke-300);
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
@@ -228,7 +225,7 @@
|
||||
--brand-yellow: var(--color-electric-400);
|
||||
--brand-blue: var(--color-sapphire-700);
|
||||
--secondary-background: var(--color-smoke-200);
|
||||
--secondary-background-hover: var(--color-smoke-200);
|
||||
--secondary-background-hover: var(--color-smoke-400);
|
||||
--secondary-background-selected: var(--color-smoke-600);
|
||||
--base-background: var(--color-white);
|
||||
--primary-background: var(--color-azure-400);
|
||||
@@ -242,29 +239,6 @@
|
||||
--border-subtle: var(--color-smoke-400);
|
||||
--muted-background: var(--color-smoke-700);
|
||||
--accent-background: var(--color-smoke-800);
|
||||
|
||||
/* Component/Node tokens from design system light */
|
||||
--component-node-background: var(--color-white);
|
||||
--component-node-border: var(--color-border-default);
|
||||
--component-node-foreground: var(--base-foreground);
|
||||
--component-node-foreground-secondary: var(--color-muted-foreground);
|
||||
--component-node-widget-background: var(--secondary-background);
|
||||
--component-node-widget-background-hovered: var(--secondary-background-hover);
|
||||
--component-node-widget-background-selected: var(--secondary-background-selected);
|
||||
--component-node-widget-background-disabled: var(--color-alpha-ash-500-20);
|
||||
--component-node-widget-background-highlighted: var(--color-ash-500);
|
||||
|
||||
/* Default UI element color palette variables */
|
||||
--palette-contrast-mix-color: #fff;
|
||||
--palette-interface-panel-surface: var(--comfy-menu-bg);
|
||||
--palette-interface-stroke: color-mix(in srgb, var(--interface-panel-surface) 75.5%, var(--contrast-mix-color));
|
||||
|
||||
--palette-interface-panel-box-shadow: 1px 1px 8px 0 rgb(0 0 0 / 0.4);
|
||||
--palette-interface-panel-drop-shadow: 1px 1px 4px rgb(0 0 0 / 0.4);
|
||||
--palette-interface-panel-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 92.5%, var(--contrast-mix-color));
|
||||
--palette-interface-panel-selected-surface: color-mix(in srgb, var(--interface-panel-surface) 87.5%, var(--contrast-mix-color));
|
||||
--palette-interface-button-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 82%, var(--contrast-mix-color));
|
||||
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
@@ -284,8 +258,6 @@
|
||||
--button-active-surface: var(--color-charcoal-600);
|
||||
--button-icon: var(--color-smoke-800);
|
||||
|
||||
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
|
||||
|
||||
--modal-card-button-surface: var(--color-charcoal-300);
|
||||
|
||||
--dialog-surface: var(--color-neutral-700);
|
||||
@@ -313,7 +285,7 @@
|
||||
--node-component-surface-highlight: var(--color-slate-100);
|
||||
--node-component-surface-hovered: var(--color-charcoal-600);
|
||||
--node-component-surface-selected: var(--color-charcoal-200);
|
||||
--node-component-surface: var(--color-charcoal-600);
|
||||
--node-component-surface: var(--color-charcoal-800);
|
||||
--node-component-tooltip: var(--color-white);
|
||||
--node-component-tooltip-border: var(--color-slate-300);
|
||||
--node-component-tooltip-surface: var(--color-charcoal-800);
|
||||
@@ -351,17 +323,6 @@
|
||||
--border-subtle: var(--color-charcoal-300);
|
||||
--muted-background: var(--color-charcoal-100);
|
||||
--accent-background: var(--color-charcoal-100);
|
||||
|
||||
/* Component/Node tokens from design dark system */
|
||||
--component-node-background: var(--color-charcoal-600);
|
||||
--component-node-border: var(--color-charcoal-100);
|
||||
--component-node-foreground: var(--base-foreground);
|
||||
--component-node-foreground-secondary: var(--color-muted-foreground);
|
||||
--component-node-widget-background: var(--secondary-background-hover);
|
||||
--component-node-widget-background-hovered: var(--secondary-background-selected);
|
||||
--component-node-widget-background-selected: var(--color-charcoal-100);
|
||||
--component-node-widget-background-disabled: var(--color-alpha-charcoal-600-30);
|
||||
--component-node-widget-background-highlighted: var(--color-graphite-400);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -371,7 +332,6 @@
|
||||
--color-button-icon: var(--button-icon);
|
||||
--color-button-surface: var(--button-surface);
|
||||
--color-button-surface-contrast: var(--button-surface-contrast);
|
||||
--color-subscription-button-gradient: var(--subscription-button-gradient);
|
||||
--color-modal-card-button-surface: var(--modal-card-button-surface);
|
||||
--color-dialog-surface: var(--dialog-surface);
|
||||
--color-interface-menu-component-surface-hovered: var(
|
||||
@@ -429,17 +389,6 @@
|
||||
--color-text-primary: var(--text-primary);
|
||||
--color-input-surface: var(--input-surface);
|
||||
|
||||
/* Component/Node design tokens */
|
||||
--color-component-node-background: var(--component-node-background);
|
||||
--color-component-node-border: var(--component-node-border);
|
||||
--color-component-node-foreground: var(--component-node-foreground);
|
||||
--color-component-node-foreground-secondary: var(--component-node-foreground-secondary);
|
||||
--color-component-node-widget-background: var(--component-node-widget-background);
|
||||
--color-component-node-widget-background-hovered: var(--component-node-widget-background-hovered);
|
||||
--color-component-node-widget-background-selected: var(--component-node-widget-background-selected);
|
||||
--color-component-node-widget-background-disabled: var(--component-node-widget-background-disabled);
|
||||
--color-component-node-widget-background-highlighted: var(--component-node-widget-background-highlighted);
|
||||
|
||||
/* Semantic tokens */
|
||||
--color-base-foreground: var(--base-foreground);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
@@ -543,40 +492,36 @@ body {
|
||||
.comfy-markdown {
|
||||
/* We assign the textarea and the Tiptap editor to the same CSS grid area to stack them on top of one another. */
|
||||
display: grid;
|
||||
& > textarea,
|
||||
.tiptap {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
}
|
||||
}
|
||||
|
||||
& > textarea {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.comfy-markdown > textarea {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
& > textarea {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
.comfy-markdown .tiptap {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
background-color: var(--comfy-input-bg);
|
||||
color: var(--input-text);
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
resize: none;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
font-size: var(--comfy-textarea-font-size);
|
||||
height: 100%;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.tiptap {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.comfy-markdown.editing .tiptap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tiptap {
|
||||
overflow-y: auto;
|
||||
font-size: var(--comfy-textarea-font-size);
|
||||
.comfy-markdown .tiptap :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.comfy-markdown .tiptap :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.comfy-markdown .tiptap blockquote {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.13727 11.2201L6.27454 14.4398" stroke="var(--pin-color, var(--color-smoke-800, #8a8a8a))" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M6.28085 6.68385C6.21652 6.92342 6.08664 7.1403 5.9058 7.31009C5.72497 7.47989 5.50035 7.59587 5.25721 7.645L3.95569 7.91742C3.71254 7.96655 3.48793 8.08253 3.30709 8.25233C3.12626 8.42212 2.99637 8.639 2.93204 8.87857L2.80091 9.36797C2.75515 9.53876 2.7791 9.72073 2.86751 9.87385C2.95591 10.027 3.10153 10.1387 3.27231 10.1845L10.9997 12.255C11.1705 12.3008 11.3525 12.2768 11.5056 12.1884C11.6587 12.1 11.7705 11.9544 11.8162 11.7836L11.9474 11.2942C12.0114 11.0546 12.0074 10.8018 11.9357 10.5643C11.864 10.3269 11.7274 10.1141 11.5414 9.95001L10.5505 9.06333C10.3645 8.89921 10.2279 8.68646 10.1562 8.44899C10.0845 8.21153 10.0805 7.95877 10.1446 7.71913L10.7933 5.29787C10.8391 5.12709 10.9508 4.98148 11.1039 4.89307C11.2571 4.80466 11.439 4.78071 11.6098 4.82647C11.9514 4.91799 12.3153 4.87008 12.6216 4.69327C12.9278 4.51646 13.1513 4.22523 13.2428 3.88366C13.3343 3.54209 13.2864 3.17815 13.1096 2.8719C12.9328 2.56566 12.6416 2.34219 12.3 2.25067L7.1484 0.870299C6.80683 0.778775 6.44289 0.826689 6.13665 1.0035C5.8304 1.18031 5.60694 1.47154 5.51541 1.81311C5.42389 2.15468 5.4718 2.51862 5.64861 2.82487C5.82542 3.13111 6.11665 3.35458 6.45822 3.4461C6.62901 3.49186 6.77462 3.6036 6.86302 3.75672C6.95143 3.90984 6.97539 4.09181 6.92962 4.2626L6.28085 6.68385Z" fill="var(--handle-color, var(--color-gold-600, #fd9903))" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -4,13 +4,6 @@ import { addDynamicIconSelectors } from '@iconify/tailwind'
|
||||
import { iconCollection } from './src/iconCollection'
|
||||
|
||||
export default {
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
interface: 'var(--interface-panel-box-shadow)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
addDynamicIconSelectors({
|
||||
iconSets: {
|
||||
|
||||
40399
packages/registry-types/src/comfyRegistryTypes.ts
generated
6
pnpm-lock.yaml
generated
@@ -12,9 +12,15 @@ catalogs:
|
||||
'@eslint/js':
|
||||
specifier: ^9.35.0
|
||||
version: 9.35.0
|
||||
'@iconify-json/lucide':
|
||||
specifier: ^1.1.178
|
||||
version: 1.2.66
|
||||
'@iconify/json':
|
||||
specifier: ^2.2.380
|
||||
version: 2.2.380
|
||||
'@iconify/tailwind':
|
||||
specifier: ^1.1.3
|
||||
version: 1.2.0
|
||||
'@intlify/eslint-plugin-vue-i18n':
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
|
||||
@@ -112,7 +112,6 @@ onlyBuiltDependencies:
|
||||
- '@playwright/browser-chromium'
|
||||
- '@playwright/browser-firefox'
|
||||
- '@playwright/browser-webkit'
|
||||
- '@sentry/cli'
|
||||
- '@tailwindcss/oxide'
|
||||
- esbuild
|
||||
- nx
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 325 KiB |
@@ -1,444 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Compares two API snapshots and generates a human-readable changelog
|
||||
* documenting additions, removals, and modifications to the public API.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length < 4) {
|
||||
console.error(
|
||||
'Usage: compare-api-snapshots.js <previous.json> <current.json> <previous-version> <current-version> [repo-owner] [repo-name] [git-ref]'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const [
|
||||
previousPath,
|
||||
currentPath,
|
||||
previousVersion,
|
||||
currentVersion,
|
||||
repoOwner = 'Comfy-Org',
|
||||
repoName = 'ComfyUI_frontend',
|
||||
gitRef = 'main'
|
||||
] = args
|
||||
|
||||
if (!fs.existsSync(previousPath)) {
|
||||
console.error(`Previous snapshot not found: ${previousPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(currentPath)) {
|
||||
console.error(`Current snapshot not found: ${currentPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const previousApi = JSON.parse(fs.readFileSync(previousPath, 'utf-8'))
|
||||
const currentApi = JSON.parse(fs.readFileSync(currentPath, 'utf-8'))
|
||||
|
||||
/**
|
||||
* Generate GitHub permalink to source code
|
||||
* Prefers source file location over dist .d.ts location
|
||||
*/
|
||||
function generateGitHubLink(name, item) {
|
||||
// If we have source file information, use that
|
||||
if (item?.sourceFile && item?.sourceLine) {
|
||||
return `[\`${name}\`](https://github.com/${repoOwner}/${repoName}/blob/${gitRef}/${item.sourceFile}#L${item.sourceLine})`
|
||||
}
|
||||
|
||||
// Fallback to .d.ts location if available
|
||||
if (item?.line) {
|
||||
return `[\`${name}\`](https://github.com/${repoOwner}/${repoName}/blob/${gitRef}/dist/index.d.ts#L${item.line})`
|
||||
}
|
||||
|
||||
// No location info available
|
||||
return `\`${name}\``
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two API snapshots and generate changelog
|
||||
*/
|
||||
function compareApis(previous, current) {
|
||||
const changes = {
|
||||
breaking: [],
|
||||
additions: [],
|
||||
modifications: [],
|
||||
deprecations: []
|
||||
}
|
||||
|
||||
const categories = [
|
||||
'types',
|
||||
'interfaces',
|
||||
'enums',
|
||||
'functions',
|
||||
'classes',
|
||||
'constants'
|
||||
]
|
||||
|
||||
for (const category of categories) {
|
||||
const prevItems = previous[category] || {}
|
||||
const currItems = current[category] || {}
|
||||
|
||||
// Find additions
|
||||
for (const name in currItems) {
|
||||
if (!prevItems[name]) {
|
||||
changes.additions.push({
|
||||
category,
|
||||
name,
|
||||
item: currItems[name]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Find removals and modifications
|
||||
for (const name in prevItems) {
|
||||
if (!currItems[name]) {
|
||||
changes.breaking.push({
|
||||
category,
|
||||
name,
|
||||
type: 'removed',
|
||||
item: prevItems[name]
|
||||
})
|
||||
} else {
|
||||
// Check for modifications
|
||||
const diff = compareItems(prevItems[name], currItems[name], category)
|
||||
if (diff.length > 0) {
|
||||
changes.modifications.push({
|
||||
category,
|
||||
name,
|
||||
changes: diff
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two items and return differences
|
||||
*/
|
||||
function compareItems(prev, curr, category) {
|
||||
const differences = []
|
||||
|
||||
if (category === 'interfaces' || category === 'classes') {
|
||||
// Compare members
|
||||
const prevMembers = new Map(prev.members?.map((m) => [m.name, m]) || [])
|
||||
const currMembers = new Map(curr.members?.map((m) => [m.name, m]) || [])
|
||||
|
||||
// Find added members
|
||||
for (const [name, member] of currMembers) {
|
||||
if (!prevMembers.has(name)) {
|
||||
differences.push({
|
||||
type: 'member_added',
|
||||
name,
|
||||
member
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Find removed members
|
||||
for (const [name, member] of prevMembers) {
|
||||
if (!currMembers.has(name)) {
|
||||
differences.push({
|
||||
type: 'member_removed',
|
||||
name,
|
||||
member
|
||||
})
|
||||
} else {
|
||||
// Check if member type changed
|
||||
const prevMember = prevMembers.get(name)
|
||||
const currMember = currMembers.get(name)
|
||||
|
||||
if (prevMember.type !== currMember.type) {
|
||||
differences.push({
|
||||
type: 'member_type_changed',
|
||||
name,
|
||||
from: prevMember.type,
|
||||
to: currMember.type
|
||||
})
|
||||
}
|
||||
|
||||
if (prevMember.optional !== currMember.optional) {
|
||||
differences.push({
|
||||
type: 'member_optionality_changed',
|
||||
name,
|
||||
from: prevMember.optional ? 'optional' : 'required',
|
||||
to: currMember.optional ? 'optional' : 'required'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compare methods (for classes and interfaces)
|
||||
if (category === 'classes') {
|
||||
const prevMethods = new Map(prev.methods?.map((m) => [m.name, m]) || [])
|
||||
const currMethods = new Map(curr.methods?.map((m) => [m.name, m]) || [])
|
||||
|
||||
for (const [name, method] of currMethods) {
|
||||
if (!prevMethods.has(name)) {
|
||||
differences.push({
|
||||
type: 'method_added',
|
||||
name,
|
||||
method
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, method] of prevMethods) {
|
||||
if (!currMethods.has(name)) {
|
||||
differences.push({
|
||||
type: 'method_removed',
|
||||
name,
|
||||
method
|
||||
})
|
||||
} else {
|
||||
const prevMethod = prevMethods.get(name)
|
||||
const currMethod = currMethods.get(name)
|
||||
|
||||
if (prevMethod.returnType !== currMethod.returnType) {
|
||||
differences.push({
|
||||
type: 'method_return_type_changed',
|
||||
name,
|
||||
from: prevMethod.returnType,
|
||||
to: currMethod.returnType
|
||||
})
|
||||
}
|
||||
|
||||
// Compare parameters
|
||||
if (
|
||||
JSON.stringify(prevMethod.parameters) !==
|
||||
JSON.stringify(currMethod.parameters)
|
||||
) {
|
||||
differences.push({
|
||||
type: 'method_signature_changed',
|
||||
name,
|
||||
from: prevMethod.parameters,
|
||||
to: currMethod.parameters
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (category === 'functions') {
|
||||
// Compare function signatures
|
||||
if (prev.returnType !== curr.returnType) {
|
||||
differences.push({
|
||||
type: 'return_type_changed',
|
||||
from: prev.returnType,
|
||||
to: curr.returnType
|
||||
})
|
||||
}
|
||||
|
||||
if (JSON.stringify(prev.parameters) !== JSON.stringify(curr.parameters)) {
|
||||
differences.push({
|
||||
type: 'parameters_changed',
|
||||
from: prev.parameters,
|
||||
to: curr.parameters
|
||||
})
|
||||
}
|
||||
} else if (category === 'enums') {
|
||||
// Compare enum members
|
||||
const prevMembers = new Set(prev.members?.map((m) => m.name) || [])
|
||||
const currMembers = new Set(curr.members?.map((m) => m.name) || [])
|
||||
|
||||
for (const member of currMembers) {
|
||||
if (!prevMembers.has(member)) {
|
||||
differences.push({
|
||||
type: 'enum_member_added',
|
||||
name: member
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const member of prevMembers) {
|
||||
if (!currMembers.has(member)) {
|
||||
differences.push({
|
||||
type: 'enum_member_removed',
|
||||
name: member
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return differences
|
||||
}
|
||||
|
||||
/**
|
||||
* Format changelog as markdown
|
||||
*/
|
||||
function formatChangelog(changes, prevVersion, currVersion) {
|
||||
const lines = []
|
||||
|
||||
lines.push(`## v${currVersion} (${new Date().toISOString().split('T')[0]})`)
|
||||
lines.push('')
|
||||
lines.push(
|
||||
`Comparing v${prevVersion} → v${currVersion}. This changelog documents changes to the public API surface that third-party extensions and custom nodes depend on.`
|
||||
)
|
||||
lines.push('')
|
||||
|
||||
// Breaking changes
|
||||
if (changes.breaking.length > 0) {
|
||||
lines.push('### ⚠️ Breaking Changes')
|
||||
lines.push('')
|
||||
|
||||
const grouped = groupByCategory(changes.breaking)
|
||||
for (const [category, items] of Object.entries(grouped)) {
|
||||
lines.push(`**${categoryToTitle(category)}**`)
|
||||
lines.push('')
|
||||
for (const item of items) {
|
||||
const displayName = generateGitHubLink(item.name, item.item)
|
||||
lines.push(`- **Removed**: ${displayName}`)
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
}
|
||||
|
||||
// Additions - commented out as per feedback
|
||||
// if (changes.additions.length > 0) {
|
||||
// lines.push('### ✨ Additions')
|
||||
// lines.push('')
|
||||
//
|
||||
// const grouped = groupByCategory(changes.additions)
|
||||
// for (const [category, items] of Object.entries(grouped)) {
|
||||
// lines.push(`**${categoryToTitle(category)}**`)
|
||||
// lines.push('')
|
||||
// for (const item of items) {
|
||||
// lines.push(`- \`${item.name}\``)
|
||||
// if (item.item.members && item.item.members.length > 0) {
|
||||
// const publicMembers = item.item.members.filter(
|
||||
// (m) => !m.visibility || m.visibility === 'public'
|
||||
// )
|
||||
// if (publicMembers.length > 0 && publicMembers.length <= 5) {
|
||||
// lines.push(
|
||||
// ` - Members: ${publicMembers.map((m) => `\`${m.name}\``).join(', ')}`
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// lines.push('')
|
||||
// }
|
||||
// }
|
||||
|
||||
// Modifications
|
||||
if (changes.modifications.length > 0) {
|
||||
lines.push('### 🔄 Modifications')
|
||||
lines.push('')
|
||||
|
||||
const hasBreakingMods = changes.modifications.some((mod) =>
|
||||
mod.changes.some((c) => isBreakingChange(c))
|
||||
)
|
||||
|
||||
if (hasBreakingMods) {
|
||||
lines.push('> **Note**: Some modifications may be breaking changes.')
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(changes.modifications)
|
||||
for (const [category, items] of Object.entries(grouped)) {
|
||||
lines.push(`**${categoryToTitle(category)}**`)
|
||||
lines.push('')
|
||||
for (const item of items) {
|
||||
// Get the current item to access source location
|
||||
const currItem =
|
||||
currentApi[item.category] && currentApi[item.category][item.name]
|
||||
const displayName = generateGitHubLink(item.name, currItem)
|
||||
lines.push(`- ${displayName}`)
|
||||
for (const change of item.changes) {
|
||||
const formatted = formatChange(change)
|
||||
if (formatted) {
|
||||
lines.push(` ${formatted}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.breaking.length === 0 && changes.modifications.length === 0) {
|
||||
lines.push('_No API changes detected._')
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
lines.push('---')
|
||||
lines.push('')
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function groupByCategory(items) {
|
||||
const grouped = {}
|
||||
for (const item of items) {
|
||||
if (!grouped[item.category]) {
|
||||
grouped[item.category] = []
|
||||
}
|
||||
grouped[item.category].push(item)
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
function categoryToTitle(category) {
|
||||
const titles = {
|
||||
types: 'Type Aliases',
|
||||
interfaces: 'Interfaces',
|
||||
enums: 'Enums',
|
||||
functions: 'Functions',
|
||||
classes: 'Classes',
|
||||
constants: 'Constants'
|
||||
}
|
||||
return titles[category] || category
|
||||
}
|
||||
|
||||
function isBreakingChange(change) {
|
||||
const breakingTypes = [
|
||||
'member_removed',
|
||||
'method_removed',
|
||||
'member_type_changed',
|
||||
'method_return_type_changed',
|
||||
'method_signature_changed',
|
||||
'return_type_changed',
|
||||
'parameters_changed',
|
||||
'enum_member_removed'
|
||||
]
|
||||
return breakingTypes.includes(change.type)
|
||||
}
|
||||
|
||||
function formatChange(change) {
|
||||
switch (change.type) {
|
||||
case 'member_added':
|
||||
return `- ✨ Added member: \`${change.name}\``
|
||||
case 'member_removed':
|
||||
return `- ⚠️ **Breaking**: Removed member: \`${change.name}\``
|
||||
case 'member_type_changed':
|
||||
return `- ⚠️ **Breaking**: Member \`${change.name}\` type changed: \`${change.from}\` → \`${change.to}\``
|
||||
case 'member_optionality_changed':
|
||||
return `- ${change.to === 'required' ? '⚠️ **Breaking**' : '✨'}: Member \`${change.name}\` is now ${change.to}`
|
||||
case 'method_added':
|
||||
return `- ✨ Added method: \`${change.name}()\``
|
||||
case 'method_removed':
|
||||
return `- ⚠️ **Breaking**: Removed method: \`${change.name}()\``
|
||||
case 'method_return_type_changed':
|
||||
return `- ⚠️ **Breaking**: Method \`${change.name}()\` return type changed: \`${change.from}\` → \`${change.to}\``
|
||||
case 'method_signature_changed':
|
||||
return `- ⚠️ **Breaking**: Method \`${change.name}()\` signature changed`
|
||||
case 'return_type_changed':
|
||||
return `- ⚠️ **Breaking**: Return type changed: \`${change.from}\` → \`${change.to}\``
|
||||
case 'parameters_changed':
|
||||
return `- ⚠️ **Breaking**: Function parameters changed`
|
||||
case 'enum_member_added':
|
||||
return `- ✨ Added enum value: \`${change.name}\``
|
||||
case 'enum_member_removed':
|
||||
return `- ⚠️ **Breaking**: Removed enum value: \`${change.name}\``
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const changes = compareApis(previousApi, currentApi)
|
||||
const changelog = formatChangelog(changes, previousVersion, currentVersion)
|
||||
|
||||
console.log(changelog)
|
||||
@@ -1,313 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generates a JSON snapshot of the public API surface from TypeScript definitions.
|
||||
* This snapshot is used to track API changes between versions.
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as ts from 'typescript'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: snapshot-api.js <path-to-index.d.ts>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const filePath = args[0]
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`File not found: ${filePath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for the declaration in source files
|
||||
* Returns {file, line} or null if not found
|
||||
*/
|
||||
function findInSourceFiles(declarationName, kind, sourceRoot = 'src') {
|
||||
const searchPattern = getSearchPattern(declarationName, kind)
|
||||
if (!searchPattern) return null
|
||||
|
||||
try {
|
||||
// Search for the declaration pattern in source files
|
||||
const result = execSync(
|
||||
`grep -rn "${searchPattern}" ${sourceRoot} --include="*.ts" --include="*.tsx" | head -1`,
|
||||
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }
|
||||
).trim()
|
||||
|
||||
if (result) {
|
||||
// Parse grep output: filepath:line:content
|
||||
const match = result.match(/^([^:]+):(\d+):/)
|
||||
if (match) {
|
||||
return {
|
||||
file: match[1],
|
||||
line: parseInt(match[2], 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// grep returns non-zero exit code if no match found
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate search pattern for finding declaration in source
|
||||
*/
|
||||
function getSearchPattern(name, kind) {
|
||||
switch (kind) {
|
||||
case 'interface':
|
||||
return `export interface ${name}`
|
||||
case 'class':
|
||||
return `export class ${name}`
|
||||
case 'type':
|
||||
return `export type ${name}`
|
||||
case 'enum':
|
||||
return `export enum ${name}`
|
||||
case 'function':
|
||||
return `export function ${name}`
|
||||
case 'constant':
|
||||
return `export const ${name}`
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract API surface from TypeScript definitions
|
||||
*/
|
||||
function extractApiSurface(sourceFile) {
|
||||
const api = {
|
||||
types: {},
|
||||
interfaces: {},
|
||||
enums: {},
|
||||
functions: {},
|
||||
classes: {},
|
||||
constants: {}
|
||||
}
|
||||
|
||||
function visit(node) {
|
||||
// Extract type aliases
|
||||
if (ts.isTypeAliasDeclaration(node) && node.name) {
|
||||
const name = node.name.text
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
|
||||
const sourceLocation = findInSourceFiles(name, 'type')
|
||||
api.types[name] = {
|
||||
kind: 'type',
|
||||
name,
|
||||
text: node.getText(sourceFile),
|
||||
exported: hasExportModifier(node),
|
||||
line: line + 1, // Convert to 1-indexed
|
||||
sourceFile: sourceLocation?.file,
|
||||
sourceLine: sourceLocation?.line
|
||||
}
|
||||
}
|
||||
|
||||
// Extract interfaces
|
||||
if (ts.isInterfaceDeclaration(node) && node.name) {
|
||||
const name = node.name.text
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
|
||||
const members = []
|
||||
|
||||
node.members.forEach((member) => {
|
||||
if (ts.isPropertySignature(member) && member.name) {
|
||||
members.push({
|
||||
name: member.name.getText(sourceFile),
|
||||
type: member.type ? member.type.getText(sourceFile) : 'any',
|
||||
optional: !!member.questionToken
|
||||
})
|
||||
} else if (ts.isMethodSignature(member) && member.name) {
|
||||
members.push({
|
||||
name: member.name.getText(sourceFile),
|
||||
kind: 'method',
|
||||
parameters: member.parameters.map((p) => ({
|
||||
name: p.name.getText(sourceFile),
|
||||
type: p.type ? p.type.getText(sourceFile) : 'any',
|
||||
optional: !!p.questionToken
|
||||
})),
|
||||
returnType: member.type ? member.type.getText(sourceFile) : 'void'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const sourceLocation = findInSourceFiles(name, 'interface')
|
||||
api.interfaces[name] = {
|
||||
kind: 'interface',
|
||||
name,
|
||||
members,
|
||||
exported: hasExportModifier(node),
|
||||
heritage: node.heritageClauses
|
||||
? node.heritageClauses
|
||||
.map((clause) =>
|
||||
clause.types.map((type) => type.getText(sourceFile))
|
||||
)
|
||||
.flat()
|
||||
: [],
|
||||
line: line + 1, // Convert to 1-indexed
|
||||
sourceFile: sourceLocation?.file,
|
||||
sourceLine: sourceLocation?.line
|
||||
}
|
||||
}
|
||||
|
||||
// Extract enums
|
||||
if (ts.isEnumDeclaration(node) && node.name) {
|
||||
const name = node.name.text
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
|
||||
const members = node.members.map((member) => ({
|
||||
name: member.name.getText(sourceFile),
|
||||
value: member.initializer
|
||||
? member.initializer.getText(sourceFile)
|
||||
: undefined
|
||||
}))
|
||||
|
||||
const sourceLocation = findInSourceFiles(name, 'enum')
|
||||
api.enums[name] = {
|
||||
kind: 'enum',
|
||||
name,
|
||||
members,
|
||||
exported: hasExportModifier(node),
|
||||
line: line + 1, // Convert to 1-indexed
|
||||
sourceFile: sourceLocation?.file,
|
||||
sourceLine: sourceLocation?.line
|
||||
}
|
||||
}
|
||||
|
||||
// Extract functions
|
||||
if (ts.isFunctionDeclaration(node) && node.name) {
|
||||
const name = node.name.text
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
|
||||
const sourceLocation = findInSourceFiles(name, 'function')
|
||||
api.functions[name] = {
|
||||
kind: 'function',
|
||||
name,
|
||||
parameters: node.parameters.map((p) => ({
|
||||
name: p.name.getText(sourceFile),
|
||||
type: p.type ? p.type.getText(sourceFile) : 'any',
|
||||
optional: !!p.questionToken
|
||||
})),
|
||||
returnType: node.type ? node.type.getText(sourceFile) : 'any',
|
||||
exported: hasExportModifier(node),
|
||||
line: line + 1, // Convert to 1-indexed
|
||||
sourceFile: sourceLocation?.file,
|
||||
sourceLine: sourceLocation?.line
|
||||
}
|
||||
}
|
||||
|
||||
// Extract classes
|
||||
if (ts.isClassDeclaration(node) && node.name) {
|
||||
const name = node.name.text
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
|
||||
const members = []
|
||||
const methods = []
|
||||
|
||||
node.members.forEach((member) => {
|
||||
if (ts.isPropertyDeclaration(member) && member.name) {
|
||||
members.push({
|
||||
name: member.name.getText(sourceFile),
|
||||
type: member.type ? member.type.getText(sourceFile) : 'any',
|
||||
static: hasStaticModifier(member),
|
||||
visibility: getVisibility(member)
|
||||
})
|
||||
} else if (ts.isMethodDeclaration(member) && member.name) {
|
||||
methods.push({
|
||||
name: member.name.getText(sourceFile),
|
||||
parameters: member.parameters.map((p) => ({
|
||||
name: p.name.getText(sourceFile),
|
||||
type: p.type ? p.type.getText(sourceFile) : 'any',
|
||||
optional: !!p.questionToken
|
||||
})),
|
||||
returnType: member.type ? member.type.getText(sourceFile) : 'any',
|
||||
static: hasStaticModifier(member),
|
||||
visibility: getVisibility(member)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const sourceLocation = findInSourceFiles(name, 'class')
|
||||
api.classes[name] = {
|
||||
kind: 'class',
|
||||
name,
|
||||
members,
|
||||
methods,
|
||||
exported: hasExportModifier(node),
|
||||
heritage: node.heritageClauses
|
||||
? node.heritageClauses
|
||||
.map((clause) =>
|
||||
clause.types.map((type) => type.getText(sourceFile))
|
||||
)
|
||||
.flat()
|
||||
: [],
|
||||
line: line + 1, // Convert to 1-indexed
|
||||
sourceFile: sourceLocation?.file,
|
||||
sourceLine: sourceLocation?.line
|
||||
}
|
||||
}
|
||||
|
||||
// Extract variable declarations (constants)
|
||||
if (ts.isVariableStatement(node)) {
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
|
||||
node.declarationList.declarations.forEach((decl) => {
|
||||
if (decl.name && ts.isIdentifier(decl.name)) {
|
||||
const name = decl.name.text
|
||||
const sourceLocation = findInSourceFiles(name, 'constant')
|
||||
api.constants[name] = {
|
||||
kind: 'constant',
|
||||
name,
|
||||
type: decl.type ? decl.type.getText(sourceFile) : 'unknown',
|
||||
exported: hasExportModifier(node),
|
||||
line: line + 1, // Convert to 1-indexed
|
||||
sourceFile: sourceLocation?.file,
|
||||
sourceLine: sourceLocation?.line
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
|
||||
function hasExportModifier(node) {
|
||||
return (
|
||||
node.modifiers &&
|
||||
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)
|
||||
)
|
||||
}
|
||||
|
||||
function hasStaticModifier(node) {
|
||||
return (
|
||||
node.modifiers &&
|
||||
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword)
|
||||
)
|
||||
}
|
||||
|
||||
function getVisibility(node) {
|
||||
if (!node.modifiers) return 'public'
|
||||
if (node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword))
|
||||
return 'private'
|
||||
if (
|
||||
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.ProtectedKeyword)
|
||||
)
|
||||
return 'protected'
|
||||
return 'public'
|
||||
}
|
||||
|
||||
visit(sourceFile)
|
||||
return api
|
||||
}
|
||||
|
||||
// Read and parse the file
|
||||
const sourceCode = fs.readFileSync(filePath, 'utf-8')
|
||||
const sourceFile = ts.createSourceFile(
|
||||
path.basename(filePath),
|
||||
sourceCode,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
|
||||
const apiSurface = extractApiSurface(sourceFile)
|
||||
|
||||
// Output as JSON
|
||||
console.log(JSON.stringify(apiSurface, null, 2))
|
||||
@@ -52,7 +52,7 @@
|
||||
"comfy_base": {
|
||||
"fg-color": "#fff",
|
||||
"bg-color": "#202020",
|
||||
"comfy-menu-bg": "#11141a",
|
||||
"comfy-menu-bg": "#353535",
|
||||
"comfy-menu-secondary-bg": "#303030",
|
||||
"comfy-input-bg": "#222",
|
||||
"input-text": "#ddd",
|
||||
|
||||
@@ -68,12 +68,7 @@
|
||||
"content-fg": "#222",
|
||||
"content-hover-bg": "#adadad",
|
||||
"content-hover-fg": "#222",
|
||||
"bar-shadow": "rgba(16, 16, 16, 0.25) 0 0 0.5rem",
|
||||
"interface-panel-box-shadow": "1px 1px 8px 0 rgba(0, 0, 0, 0.2)",
|
||||
"interface-panel-drop-shadow": "1px 1px 4px rgba(0, 0, 0, 0.4)",
|
||||
"interface-panel-hover-surface": "var(--color-gray-200)",
|
||||
"interface-panel-selected-surface": "color-mix(in srgb, var(--interface-panel-surface) 78%, var(--contrast-mix-color))",
|
||||
"contrast-mix-color": "#000"
|
||||
"bar-shadow": "rgba(16, 16, 16, 0.25) 0 0 0.5rem"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto mx-1 flex h-12 items-center rounded-lg border border-[var(--interface-stroke)] px-2 shadow-interface"
|
||||
class="actionbar-container pointer-events-auto mx-1 flex h-12 items-center rounded-lg px-2 shadow-md"
|
||||
>
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
@@ -48,5 +48,6 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
.actionbar-container {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-panel-border-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,7 +48,6 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import ComfyRunButton from './ComfyRunButton'
|
||||
@@ -133,15 +132,6 @@ watch(visible, async (newVisible) => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Track run button handle drag start using mousedown on the drag handle.
|
||||
*/
|
||||
useEventListener(dragHandleRef, 'mousedown', () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'actionbar_run_handle_drag_start'
|
||||
})
|
||||
})
|
||||
|
||||
const lastDragState = ref({
|
||||
x: x.value,
|
||||
y: y.value,
|
||||
@@ -268,9 +258,7 @@ const panelClass = computed(() =>
|
||||
cn(
|
||||
'actionbar pointer-events-auto z1000',
|
||||
isDragging.value && 'select-none pointer-events-none',
|
||||
isDocked.value
|
||||
? 'p-0 static mr-2 border-none bg-transparent'
|
||||
: 'fixed shadow-interface'
|
||||
isDocked.value ? 'p-0 static mr-2 border-none bg-transparent' : 'fixed'
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -100,7 +100,7 @@ import BatchCountEdit from '../BatchCountEdit.vue'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const { t } = useI18n()
|
||||
const queueModeMenuItemLookup = computed(() => {
|
||||
@@ -118,9 +118,6 @@ const queueModeMenuItemLookup = computed(() => {
|
||||
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
||||
tooltip: t('menu.onChangeTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_on_change_selected'
|
||||
})
|
||||
queueMode.value = 'change'
|
||||
}
|
||||
}
|
||||
@@ -131,9 +128,6 @@ const queueModeMenuItemLookup = computed(() => {
|
||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_instant_selected'
|
||||
})
|
||||
queueMode.value = 'instant'
|
||||
}
|
||||
}
|
||||
@@ -164,18 +158,11 @@ const queuePrompt = async (e: Event) => {
|
||||
? 'Comfy.QueuePromptFront'
|
||||
: 'Comfy.QueuePrompt'
|
||||
|
||||
if (batchCount.value > 1) {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_run_multiple_batches_submitted'
|
||||
})
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
|
||||
}
|
||||
|
||||
await commandStore.execute(commandId, {
|
||||
metadata: {
|
||||
subscribe_to_run: false,
|
||||
trigger_source: 'button'
|
||||
}
|
||||
})
|
||||
await commandStore.execute(commandId)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="subgraph-breadcrumb w-auto drop-shadow-[var(--interface-panel-drop-shadow)]"
|
||||
class="subgraph-breadcrumb w-auto drop-shadow-md"
|
||||
:class="{
|
||||
'subgraph-breadcrumb-collapse': collapseTabs,
|
||||
'subgraph-breadcrumb-overflow': overflowingTabs
|
||||
@@ -40,7 +40,6 @@ import { computed, onUpdated, ref, watch } from 'vue'
|
||||
|
||||
import SubgraphBreadcrumbItem from '@/components/breadcrumb/SubgraphBreadcrumbItem.vue'
|
||||
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
@@ -74,9 +73,6 @@ const items = computed(() => {
|
||||
const items = navigationStore.navigationStack.map<MenuItem>((subgraph) => ({
|
||||
label: subgraph.name,
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'breadcrumb_subgraph_item_selected'
|
||||
})
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||
|
||||
@@ -101,9 +97,6 @@ const home = computed(() => ({
|
||||
key: 'root',
|
||||
isBlueprint: isBlueprint.value,
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'breadcrumb_subgraph_root_selected'
|
||||
})
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||
|
||||
@@ -208,8 +201,8 @@ onUpdated(() => {
|
||||
:deep(.p-breadcrumb-separator),
|
||||
:deep(.p-breadcrumb-item) {
|
||||
@apply h-12;
|
||||
border-top: 1px solid var(--interface-stroke);
|
||||
border-bottom: 1px solid var(--interface-stroke);
|
||||
border-top: 1px solid var(--p-panel-border-color);
|
||||
border-bottom: 1px solid var(--p-panel-border-color);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
@@ -221,7 +214,7 @@ onUpdated(() => {
|
||||
@apply rounded-l-lg;
|
||||
/* Then collapse the root workflow */
|
||||
flex-shrink: 5000;
|
||||
border-left: 1px solid var(--interface-stroke);
|
||||
border-left: 1px solid var(--p-panel-border-color);
|
||||
|
||||
.p-breadcrumb-item-link {
|
||||
padding-left: var(--p-breadcrumb-item-padding);
|
||||
@@ -232,7 +225,7 @@ onUpdated(() => {
|
||||
@apply rounded-r-lg;
|
||||
/* Then collapse the active item */
|
||||
flex-shrink: 1;
|
||||
border-right: 1px solid var(--interface-stroke);
|
||||
border-right: 1px solid var(--p-panel-border-color);
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item-link:hover),
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('UserAvatar', () => {
|
||||
const avatar = wrapper.findComponent(Avatar)
|
||||
expect(avatar.exists()).toBe(true)
|
||||
expect(avatar.props('image')).toBeNull()
|
||||
expect(avatar.props('icon')).toBe('icon-[lucide--user]')
|
||||
expect(avatar.props('icon')).toBe('pi pi-user')
|
||||
})
|
||||
|
||||
it('renders with default icon when provided photo Url is null', () => {
|
||||
@@ -67,7 +67,7 @@ describe('UserAvatar', () => {
|
||||
const avatar = wrapper.findComponent(Avatar)
|
||||
expect(avatar.exists()).toBe(true)
|
||||
expect(avatar.props('image')).toBeNull()
|
||||
expect(avatar.props('icon')).toBe('icon-[lucide--user]')
|
||||
expect(avatar.props('icon')).toBe('pi pi-user')
|
||||
})
|
||||
|
||||
it('falls back to icon when image fails to load', async () => {
|
||||
@@ -82,7 +82,7 @@ describe('UserAvatar', () => {
|
||||
avatar.vm.$emit('error')
|
||||
await nextTick()
|
||||
|
||||
expect(avatar.props('icon')).toBe('icon-[lucide--user]')
|
||||
expect(avatar.props('icon')).toBe('pi pi-user')
|
||||
})
|
||||
|
||||
it('uses provided ariaLabel', () => {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<Avatar
|
||||
class="bg-gray-200 dark-theme:bg-[var(--interface-panel-selected-surface)]"
|
||||
:image="photoUrl ?? undefined"
|
||||
:icon="hasAvatar ? undefined : 'icon-[lucide--user]'"
|
||||
:pt:icon:class="{ 'size-4': !hasAvatar }"
|
||||
:icon="hasAvatar ? undefined : 'pi pi-user'"
|
||||
shape="circle"
|
||||
:aria-label="ariaLabel ?? $t('auth.login.userAvatar')"
|
||||
@error="handleImageError"
|
||||
|
||||
@@ -68,17 +68,17 @@
|
||||
</template>
|
||||
</MultiSelect>
|
||||
|
||||
<!-- Runs On Filter -->
|
||||
<!-- License Filter -->
|
||||
<MultiSelect
|
||||
v-model="selectedRunsOnObjects"
|
||||
:label="runsOnFilterLabel"
|
||||
:options="runsOnOptions"
|
||||
v-model="selectedLicenseObjects"
|
||||
:label="licenseFilterLabel"
|
||||
:options="licenseOptions"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--server]" />
|
||||
<i class="icon-[lucide--file-text]" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
</div>
|
||||
@@ -382,7 +382,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, provide, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
@@ -403,8 +403,6 @@ import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
|
||||
import { useLazyPagination } from '@/composables/useLazyPagination'
|
||||
import { useTemplateFiltering } from '@/composables/useTemplateFiltering'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useTemplateWorkflows } from '@/platform/workflow/templates/composables/useTemplateWorkflows'
|
||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
@@ -414,34 +412,10 @@ import { createGridStyle } from '@/utils/gridUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { onClose: originalOnClose } = defineProps<{
|
||||
const { onClose } = defineProps<{
|
||||
onClose: () => void
|
||||
}>()
|
||||
|
||||
// Track session time for telemetry
|
||||
const sessionStartTime = ref<number>(0)
|
||||
const templateWasSelected = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
sessionStartTime.value = Date.now()
|
||||
})
|
||||
|
||||
// Wrap onClose to track session end
|
||||
const onClose = () => {
|
||||
if (isCloud) {
|
||||
const timeSpentSeconds = Math.floor(
|
||||
(Date.now() - sessionStartTime.value) / 1000
|
||||
)
|
||||
|
||||
useTelemetry()?.trackTemplateLibraryClosed({
|
||||
template_selected: templateWasSelected.value,
|
||||
time_spent_seconds: timeSpentSeconds
|
||||
})
|
||||
}
|
||||
|
||||
originalOnClose()
|
||||
}
|
||||
|
||||
provide(OnCloseKey, onClose)
|
||||
|
||||
// Workflow templates store and composable
|
||||
@@ -528,12 +502,12 @@ const {
|
||||
searchQuery,
|
||||
selectedModels,
|
||||
selectedUseCases,
|
||||
selectedRunsOn,
|
||||
selectedLicenses,
|
||||
sortBy,
|
||||
filteredTemplates,
|
||||
availableModels,
|
||||
availableUseCases,
|
||||
availableRunsOn,
|
||||
availableLicenses,
|
||||
filteredCount,
|
||||
totalCount,
|
||||
resetFilters
|
||||
@@ -561,15 +535,15 @@ const selectedUseCaseObjects = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const selectedRunsOnObjects = computed({
|
||||
const selectedLicenseObjects = computed({
|
||||
get() {
|
||||
return selectedRunsOn.value.map((runsOn) => ({
|
||||
name: runsOn,
|
||||
value: runsOn
|
||||
return selectedLicenses.value.map((license) => ({
|
||||
name: license,
|
||||
value: license
|
||||
}))
|
||||
},
|
||||
set(value: { name: string; value: string }[]) {
|
||||
selectedRunsOn.value = value.map((item) => item.value)
|
||||
selectedLicenses.value = value.map((item) => item.value)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -602,10 +576,10 @@ const useCaseOptions = computed(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const runsOnOptions = computed(() =>
|
||||
availableRunsOn.value.map((runsOn) => ({
|
||||
name: runsOn,
|
||||
value: runsOn
|
||||
const licenseOptions = computed(() =>
|
||||
availableLicenses.value.map((license) => ({
|
||||
name: license,
|
||||
value: license
|
||||
}))
|
||||
)
|
||||
|
||||
@@ -634,14 +608,14 @@ const useCaseFilterLabel = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const runsOnFilterLabel = computed(() => {
|
||||
if (selectedRunsOnObjects.value.length === 0) {
|
||||
return t('templateWorkflows.runsOnFilter', 'Runs On')
|
||||
} else if (selectedRunsOnObjects.value.length === 1) {
|
||||
return selectedRunsOnObjects.value[0].name
|
||||
const licenseFilterLabel = computed(() => {
|
||||
if (selectedLicenseObjects.value.length === 0) {
|
||||
return t('templateWorkflows.licenseFilter', 'License')
|
||||
} else if (selectedLicenseObjects.value.length === 1) {
|
||||
return selectedLicenseObjects.value[0].name
|
||||
} else {
|
||||
return t('templateWorkflows.runsOnSelected', {
|
||||
count: selectedRunsOnObjects.value.length
|
||||
return t('templateWorkflows.licensesSelected', {
|
||||
count: selectedLicenseObjects.value.length
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -708,7 +682,7 @@ watch(
|
||||
sortBy,
|
||||
selectedModels,
|
||||
selectedUseCases,
|
||||
selectedRunsOn
|
||||
selectedLicenses
|
||||
],
|
||||
() => {
|
||||
resetPagination()
|
||||
@@ -726,7 +700,6 @@ const onLoadWorkflow = async (template: any) => {
|
||||
template.name,
|
||||
getEffectiveSourceModule(template)
|
||||
)
|
||||
templateWasSelected.value = true
|
||||
onClose()
|
||||
} finally {
|
||||
loadingTemplate.value = null
|
||||
|
||||
@@ -61,7 +61,6 @@ import { useI18n } from 'vue-i18n'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
@@ -87,33 +86,18 @@ const repoOwner = 'comfyanonymous'
|
||||
const repoName = 'ComfyUI'
|
||||
const reportContent = ref('')
|
||||
const reportOpen = ref(false)
|
||||
/**
|
||||
* Open the error report content and track telemetry.
|
||||
*/
|
||||
const showReport = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'error_dialog_show_report_clicked'
|
||||
})
|
||||
reportOpen.value = true
|
||||
}
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
const telemetry = useTelemetry()
|
||||
|
||||
const title = computed<string>(
|
||||
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
|
||||
)
|
||||
|
||||
/**
|
||||
* Open contact support flow from error dialog and track telemetry.
|
||||
*/
|
||||
const showContactSupport = async () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'help_feedback',
|
||||
is_external: true,
|
||||
source: 'error_dialog'
|
||||
})
|
||||
await useCommandStore().execute('Comfy.ContactSupport')
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
|
||||
const props = defineProps<{
|
||||
errorMessage: string
|
||||
repoOwner: string
|
||||
@@ -21,13 +19,7 @@ const props = defineProps<{
|
||||
|
||||
const queryString = computed(() => props.errorMessage + ' is:issue')
|
||||
|
||||
/**
|
||||
* Open GitHub issues search and track telemetry.
|
||||
*/
|
||||
const openGitHubIssues = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'error_dialog_find_existing_issues_clicked'
|
||||
})
|
||||
const query = encodeURIComponent(queryString.value)
|
||||
const url = `https://github.com/${props.repoOwner}/${props.repoName}/issues?q=${query}`
|
||||
window.open(url, '_blank')
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<UserCredit text-class="text-3xl font-bold" />
|
||||
<Skeleton v-if="loading" width="2rem" height="2rem" />
|
||||
<Button
|
||||
v-else-if="isActiveSubscription"
|
||||
v-else
|
||||
:label="$t('credits.purchaseCredits')"
|
||||
:loading="loading"
|
||||
@click="handlePurchaseCreditsClick"
|
||||
@@ -92,13 +92,6 @@
|
||||
icon="pi pi-question-circle"
|
||||
@click="handleFaqClick"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('subscription.partnerNodesCredits')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-question-circle"
|
||||
@click="handleOpenPartnerNodesInfo"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('credits.messageSupport')"
|
||||
text
|
||||
@@ -123,8 +116,6 @@ import { computed, ref, watch } from 'vue'
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
@@ -141,8 +132,6 @@ const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const telemetry = useTelemetry()
|
||||
const { isActiveSubscription } = useSubscription()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
@@ -164,8 +153,6 @@ watch(
|
||||
)
|
||||
|
||||
const handlePurchaseCreditsClick = () => {
|
||||
// Track purchase credits entry from Settings > Credits panel
|
||||
useTelemetry()?.trackAddApiCreditButtonClicked()
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
@@ -174,11 +161,6 @@ const handleCreditsHistoryClick = async () => {
|
||||
}
|
||||
|
||||
const handleMessageSupport = async () => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: 'help_feedback',
|
||||
is_external: true,
|
||||
source: 'credits_panel'
|
||||
})
|
||||
await commandStore.execute('Comfy.ContactSupport')
|
||||
}
|
||||
|
||||
@@ -186,12 +168,5 @@ const handleFaqClick = () => {
|
||||
window.open('https://docs.comfy.org/tutorials/api-nodes/faq', '_blank')
|
||||
}
|
||||
|
||||
const handleOpenPartnerNodesInfo = () => {
|
||||
window.open(
|
||||
'https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
const creditHistory = ref<CreditHistoryItemData[]>([])
|
||||
</script>
|
||||
|
||||
@@ -96,7 +96,6 @@ import Message from 'primevue/message'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { AuditLog } from '@/services/customerEventsService'
|
||||
import {
|
||||
EventType,
|
||||
@@ -160,9 +159,6 @@ const loadEvents = async () => {
|
||||
if (response.totalPages) {
|
||||
pagination.value.totalPages = response.totalPages
|
||||
}
|
||||
|
||||
// Check if a pending top-up has completed
|
||||
useTelemetry()?.checkForCompletedTopup(response.events)
|
||||
} else {
|
||||
error.value = customerEventService.error.value || 'Failed to load events'
|
||||
}
|
||||
|
||||
@@ -10,10 +10,8 @@
|
||||
></div>
|
||||
|
||||
<ButtonGroup
|
||||
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-[var(--interface-stroke)] bg-interface-panel-surface p-2"
|
||||
:style="{
|
||||
...stringifiedMinimapStyles.buttonGroupStyles
|
||||
}"
|
||||
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
|
||||
:style="stringifiedMinimapStyles.buttonGroupStyles"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
>
|
||||
<CanvasModeSelector
|
||||
@@ -63,7 +61,7 @@
|
||||
data-testid="toggle-minimap-button"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
:class="minimapButtonClass"
|
||||
@click="onMinimapToggleClick"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--map] h-4 w-4" />
|
||||
@@ -84,7 +82,7 @@
|
||||
:aria-label="linkVisibilityAriaLabel"
|
||||
data-testid="toggle-link-visibility-button"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
@click="onLinkVisibilityToggleClick"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--route-off] h-4 w-4" />
|
||||
@@ -103,7 +101,6 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useZoomControls } from '@/composables/useZoomControls'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
|
||||
@@ -221,26 +218,6 @@ onMounted(() => {
|
||||
canvasStore.initScaleSync()
|
||||
})
|
||||
|
||||
/**
|
||||
* Track minimap toggle button click and execute the command.
|
||||
*/
|
||||
const onMinimapToggleClick = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'graph_menu_minimap_toggle_clicked'
|
||||
})
|
||||
void commandStore.execute('Comfy.Canvas.ToggleMinimap')
|
||||
}
|
||||
|
||||
/**
|
||||
* Track hide/show links button click and execute the command.
|
||||
*/
|
||||
const onLinkVisibilityToggleClick = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'graph_menu_hide_links_toggle_clicked'
|
||||
})
|
||||
void commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
canvasStore.cleanupScaleSync()
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@click="toggleBypass"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--redo-dot] h-4 w-4" />
|
||||
<i class="icon-[lucide--ban] h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
data-testid="info-button"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="onInfoClick"
|
||||
@click="toggleHelp"
|
||||
>
|
||||
<i class="icon-[lucide--info] h-4 w-4" />
|
||||
</Button>
|
||||
@@ -17,17 +17,6 @@
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
|
||||
const { showNodeHelp: toggleHelp } = useSelectionState()
|
||||
|
||||
/**
|
||||
* Track node info button click and toggle node help.
|
||||
*/
|
||||
const onInfoClick = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'selection_toolbox_node_info_opened'
|
||||
})
|
||||
toggleHelp()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -27,33 +27,7 @@ const props = defineProps<{
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const isParentNodeExecuting = ref(true)
|
||||
const formattedText = computed(() => {
|
||||
const src = modelValue.value
|
||||
// Turn [[label|url]] into placeholders to avoid interfering with linkifyHtml
|
||||
const tokens: { label: string; url: string }[] = []
|
||||
const holed = src.replace(
|
||||
/\[\[([^|\]]+)\|([^\]]+)\]\]/g,
|
||||
(_m, label, url) => {
|
||||
tokens.push({ label: String(label), url: String(url) })
|
||||
return `__LNK${tokens.length - 1}__`
|
||||
}
|
||||
)
|
||||
|
||||
// Keep current behavior (auto-link bare URLs + \n -> <br>)
|
||||
let html = nl2br(linkifyHtml(holed))
|
||||
|
||||
// Restore placeholders as <a>...</a> (minimal escaping + http default)
|
||||
html = html.replace(/__LNK(\d+)__/g, (_m, i) => {
|
||||
const { label, url } = tokens[+i]
|
||||
const safeHref = url.replace(/"/g, '"')
|
||||
const safeLabel = label.replace(/</g, '<').replace(/>/g, '>')
|
||||
return /^https?:\/\//i.test(url)
|
||||
? `<a href="${safeHref}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`
|
||||
: safeLabel
|
||||
})
|
||||
|
||||
return html
|
||||
})
|
||||
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
||||
|
||||
let parentNodeId: NodeId | null = null
|
||||
onMounted(() => {
|
||||
|
||||
@@ -138,14 +138,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import type { CSSProperties, Component } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { ReleaseNote } from '@/platform/updates/common/releaseService'
|
||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
@@ -197,7 +196,6 @@ const { t, locale } = useI18n()
|
||||
const releaseStore = useReleaseStore()
|
||||
const commandStore = useCommandStore()
|
||||
const settingStore = useSettingStore()
|
||||
const telemetry = useTelemetry()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
@@ -209,7 +207,6 @@ const isSubmenuVisible = ref(false)
|
||||
const submenuRef = ref<HTMLElement | null>(null)
|
||||
const submenuStyle = ref<CSSProperties>({})
|
||||
let hoverTimeout: number | null = null
|
||||
const openedAt = ref<number>(Date.now())
|
||||
|
||||
// Computed
|
||||
const hasReleases = computed(() => releaseStore.releases.length > 0)
|
||||
@@ -229,7 +226,6 @@ const moreItems = computed<MenuItem[]>(() => {
|
||||
label: t('helpCenter.desktopUserGuide'),
|
||||
visible: isElectron(),
|
||||
action: () => {
|
||||
trackResourceClick('docs', true)
|
||||
const docsUrl =
|
||||
electronAPI().getPlatform() === 'darwin'
|
||||
? EXTERNAL_LINKS.DESKTOP_GUIDE_MACOS
|
||||
@@ -285,7 +281,6 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
icon: 'pi pi-book',
|
||||
label: t('helpCenter.docs'),
|
||||
action: () => {
|
||||
trackResourceClick('docs', true)
|
||||
openExternalLink(EXTERNAL_LINKS.DOCS)
|
||||
emit('close')
|
||||
}
|
||||
@@ -296,7 +291,6 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
icon: 'pi pi-discord',
|
||||
label: 'Discord',
|
||||
action: () => {
|
||||
trackResourceClick('discord', true)
|
||||
openExternalLink(EXTERNAL_LINKS.DISCORD)
|
||||
emit('close')
|
||||
}
|
||||
@@ -307,7 +301,6 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
icon: 'pi pi-github',
|
||||
label: t('helpCenter.github'),
|
||||
action: () => {
|
||||
trackResourceClick('github', true)
|
||||
openExternalLink(EXTERNAL_LINKS.GITHUB)
|
||||
emit('close')
|
||||
}
|
||||
@@ -318,7 +311,6 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
icon: 'pi pi-question-circle',
|
||||
label: t('helpCenter.helpFeedback'),
|
||||
action: () => {
|
||||
trackResourceClick('help_feedback', false)
|
||||
void commandStore.execute('Comfy.ContactSupport')
|
||||
emit('close')
|
||||
}
|
||||
@@ -334,7 +326,6 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
label: t('helpCenter.managerExtension'),
|
||||
showRedDot: shouldShowManagerRedDot.value,
|
||||
action: async () => {
|
||||
trackResourceClick('manager', false)
|
||||
await useManagerState().openManager({
|
||||
initialTab: ManagerTab.All,
|
||||
showToastOnLegacyError: false
|
||||
@@ -358,23 +349,6 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
})
|
||||
|
||||
// Utility Functions
|
||||
const trackResourceClick = (
|
||||
resourceType:
|
||||
| 'docs'
|
||||
| 'discord'
|
||||
| 'github'
|
||||
| 'help_feedback'
|
||||
| 'manager'
|
||||
| 'release_notes',
|
||||
isExternal: boolean
|
||||
): void => {
|
||||
telemetry?.trackHelpResourceClicked({
|
||||
resource_type: resourceType,
|
||||
is_external: isExternal,
|
||||
source: 'help_center'
|
||||
})
|
||||
}
|
||||
|
||||
const openExternalLink = (url: string): void => {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
@@ -530,7 +504,6 @@ const onReinstall = (): void => {
|
||||
}
|
||||
|
||||
const onReleaseClick = (release: ReleaseNote): void => {
|
||||
trackResourceClick('release_notes', true)
|
||||
void releaseStore.handleShowChangelog(release.version)
|
||||
const versionAnchor = formatVersionAnchor(release.version)
|
||||
const changelogUrl = `${getChangelogUrl()}#${versionAnchor}`
|
||||
@@ -539,7 +512,6 @@ const onReleaseClick = (release: ReleaseNote): void => {
|
||||
}
|
||||
|
||||
const onUpdate = (_: ReleaseNote): void => {
|
||||
trackResourceClick('docs', true)
|
||||
openExternalLink(EXTERNAL_LINKS.UPDATE_GUIDE)
|
||||
emit('close')
|
||||
}
|
||||
@@ -554,16 +526,10 @@ const getChangelogUrl = (): string => {
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
telemetry?.trackHelpCenterOpened({ source: 'sidebar' })
|
||||
if (!hasReleases.value) {
|
||||
await releaseStore.fetchReleases()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const timeSpentSeconds = Math.round((Date.now() - openedAt.value) / 1000)
|
||||
telemetry?.trackHelpCenterClosed({ time_spent_seconds: timeSpentSeconds })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.8193 0.600586C15.1248 0.600586 15.3296 0.70893 15.459 0.881836C15.5914 1.05888 15.6471 1.33774 15.5527 1.66895L14.8037 4.30176C14.7063 4.64386 14.4729 4.97024 14.1641 5.21191C13.8544 5.45415 13.496 5.58984 13.1699 5.58984H13.1689L9.5791 5.59668H7.90625C7.52654 5.59668 7.19496 5.84986 7.09082 6.21289L5.69434 11.0889C5.63007 11.3133 5.66134 11.5534 5.77734 11.7529L5.83203 11.8359C5.99177 12.0491 6.24252 12.1758 6.50977 12.1758H6.51074L8.88281 12.1709H11.4971C11.7643 12.171 11.9541 12.254 12.084 12.3906L12.1357 12.4521C12.2685 12.6295 12.3249 12.9089 12.2305 13.2402L11.4805 15.8721C11.383 16.2144 11.1498 16.5415 10.8408 16.7832C10.5314 17.0252 10.1736 17.161 9.84766 17.1611H9.84668L6.25684 17.168H3.64258C3.33762 17.1679 3.13349 17.0588 3.00391 16.8857C2.87135 16.7087 2.81482 16.43 2.90918 16.0986L3.39551 14.3887C3.46841 14.1327 3.41794 13.8576 3.25879 13.6445V13.6436C3.09901 13.4303 2.84745 13.3037 2.58008 13.3037H1.18066C0.875088 13.3037 0.670398 13.1953 0.541016 13.0225C0.408483 12.8451 0.351891 12.5655 0.446289 12.2344L2.11914 6.38965L2.30371 5.74707V5.74609C2.40139 5.40341 2.63456 5.07671 2.94336 4.83496C3.25302 4.59258 3.61143 4.45705 3.9375 4.45703H5.6123C5.94484 4.45703 6.24083 4.26316 6.37891 3.9707L6.42773 3.83984L6.98145 1.89551C7.07894 1.55317 7.31212 1.22614 7.62109 0.984375C7.93074 0.742127 8.2892 0.606445 8.61523 0.606445H8.61621L12.1982 0.600586H14.8193Z"
|
||||
v-bind="attributes"
|
||||
:stroke="color"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -20,18 +22,11 @@ interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
class?: string
|
||||
mode?: 'outline' | 'fill'
|
||||
}
|
||||
const {
|
||||
size = 16,
|
||||
color = 'currentColor',
|
||||
mode = 'outline',
|
||||
class: className
|
||||
} = defineProps<Props>()
|
||||
const iconClass = computed(() => className || '')
|
||||
const attributes = computed(() => ({
|
||||
stroke: mode === 'outline' ? color : undefined,
|
||||
strokeWidth: mode === 'outline' ? 1 : undefined,
|
||||
fill: mode === 'fill' ? color : 'none'
|
||||
}))
|
||||
</script>
|
||||
@@ -1,48 +1,71 @@
|
||||
<template>
|
||||
<div
|
||||
class="widget-expands relative h-full w-full"
|
||||
class="relative h-full w-full"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@pointerdown.stop
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
>
|
||||
<Load3DScene
|
||||
v-if="node"
|
||||
ref="load3DSceneRef"
|
||||
:initialize-load3d="initializeLoad3d"
|
||||
:cleanup="cleanup"
|
||||
:loading="loading"
|
||||
:loading-message="loadingMessage"
|
||||
:on-model-drop="isPreview ? undefined : handleModelDrop"
|
||||
:is-preview="isPreview"
|
||||
:node="node"
|
||||
:input-spec="inputSpec"
|
||||
:background-color="backgroundColor"
|
||||
:show-grid="showGrid"
|
||||
:light-intensity="lightIntensity"
|
||||
:fov="fov"
|
||||
:camera-type="cameraType"
|
||||
:show-preview="showPreview"
|
||||
:background-image="backgroundImage"
|
||||
:up-direction="upDirection"
|
||||
:material-mode="materialMode"
|
||||
:edge-threshold="edgeThreshold"
|
||||
@material-mode-change="listenMaterialModeChange"
|
||||
@background-color-change="listenBackgroundColorChange"
|
||||
@light-intensity-change="listenLightIntensityChange"
|
||||
@fov-change="listenFOVChange"
|
||||
@camera-type-change="listenCameraTypeChange"
|
||||
@show-grid-change="listenShowGridChange"
|
||||
@show-preview-change="listenShowPreviewChange"
|
||||
@background-image-change="listenBackgroundImageChange"
|
||||
@up-direction-change="listenUpDirectionChange"
|
||||
@edge-threshold-change="listenEdgeThresholdChange"
|
||||
@recording-status-change="listenRecordingStatusChange"
|
||||
/>
|
||||
<Load3DControls
|
||||
:input-spec="inputSpec"
|
||||
:background-color="backgroundColor"
|
||||
:show-grid="showGrid"
|
||||
:show-preview="showPreview"
|
||||
:light-intensity="lightIntensity"
|
||||
:show-light-intensity-button="showLightIntensityButton"
|
||||
:fov="fov"
|
||||
:show-f-o-v-button="showFOVButton"
|
||||
:show-preview-button="showPreviewButton"
|
||||
:camera-type="cameraType"
|
||||
:has-background-image="hasBackgroundImage"
|
||||
:up-direction="upDirection"
|
||||
:material-mode="materialMode"
|
||||
:edge-threshold="edgeThreshold"
|
||||
@update-background-image="handleBackgroundImageUpdate"
|
||||
@switch-camera="switchCamera"
|
||||
@toggle-grid="toggleGrid"
|
||||
@update-background-color="handleBackgroundColorChange"
|
||||
@update-light-intensity="handleUpdateLightIntensity"
|
||||
@toggle-preview="togglePreview"
|
||||
@update-f-o-v="handleUpdateFOV"
|
||||
@update-up-direction="handleUpdateUpDirection"
|
||||
@update-material-mode="handleUpdateMaterialMode"
|
||||
@update-edge-threshold="handleUpdateEdgeThreshold"
|
||||
@export-model="handleExportModel"
|
||||
/>
|
||||
<div class="pointer-events-none absolute top-0 left-0 h-full w-full">
|
||||
<Load3DControls
|
||||
v-model:scene-config="sceneConfig"
|
||||
v-model:model-config="modelConfig"
|
||||
v-model:camera-config="cameraConfig"
|
||||
v-model:light-config="lightConfig"
|
||||
@update-background-image="handleBackgroundImageUpdate"
|
||||
@export-model="handleExportModel"
|
||||
/>
|
||||
<AnimationControls
|
||||
v-if="animations && animations.length > 0"
|
||||
v-model:animations="animations"
|
||||
v-model:playing="playing"
|
||||
v-model:selected-speed="selectedSpeed"
|
||||
v-model:selected-animation="selectedAnimation"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="enable3DViewer && node"
|
||||
v-if="enable3DViewer"
|
||||
class="pointer-events-auto absolute top-12 right-2 z-20"
|
||||
>
|
||||
<ViewerControls :node="node as LGraphNode" />
|
||||
<ViewerControls :node="node" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isPreview"
|
||||
v-if="showRecordingControls"
|
||||
class="pointer-events-auto absolute right-2 z-20"
|
||||
:class="{
|
||||
'top-12': !enable3DViewer,
|
||||
@@ -50,9 +73,10 @@
|
||||
}"
|
||||
>
|
||||
<RecordingControls
|
||||
v-model:is-recording="isRecording"
|
||||
v-model:has-recording="hasRecording"
|
||||
v-model:recording-duration="recordingDuration"
|
||||
:node="node"
|
||||
:is-recording="isRecording"
|
||||
:has-recording="hasRecording"
|
||||
:recording-duration="recordingDuration"
|
||||
@start-recording="handleStartRecording"
|
||||
@stop-recording="handleStopRecording"
|
||||
@export-recording="handleExportRecording"
|
||||
@@ -63,79 +87,250 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Load3DControls from '@/components/load3d/Load3DControls.vue'
|
||||
import Load3DScene from '@/components/load3d/Load3DScene.vue'
|
||||
import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
|
||||
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
|
||||
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
|
||||
import { useLoad3d } from '@/composables/useLoad3d'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type {
|
||||
CameraType,
|
||||
Load3DNodeType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: ComponentWidget<string[]> | SimplifiedWidget
|
||||
nodeId?: NodeId
|
||||
const { t } = useI18n()
|
||||
const { widget } = defineProps<{
|
||||
widget: ComponentWidget<string[]>
|
||||
}>()
|
||||
|
||||
function isComponentWidget(
|
||||
widget: ComponentWidget<string[]> | SimplifiedWidget
|
||||
): widget is ComponentWidget<string[]> {
|
||||
return 'node' in widget && widget.node !== undefined
|
||||
}
|
||||
const inputSpec = widget.inputSpec as CustomInputSpec
|
||||
|
||||
const node = ref<LGraphNode | null>(null)
|
||||
|
||||
if (isComponentWidget(props.widget)) {
|
||||
node.value = props.widget.node
|
||||
} else if (props.nodeId) {
|
||||
onMounted(() => {
|
||||
node.value = app.rootGraph?.getNodeById(props.nodeId!) || null
|
||||
})
|
||||
}
|
||||
const node = widget.node
|
||||
const type = inputSpec.type as Load3DNodeType
|
||||
|
||||
const backgroundColor = ref('#000000')
|
||||
const showGrid = ref(true)
|
||||
const showPreview = ref(false)
|
||||
const lightIntensity = ref(5)
|
||||
const showLightIntensityButton = ref(true)
|
||||
const fov = ref(75)
|
||||
const showFOVButton = ref(true)
|
||||
const cameraType = ref<CameraType>('perspective')
|
||||
const hasBackgroundImage = ref(false)
|
||||
const backgroundImage = ref('')
|
||||
const upDirection = ref<UpDirection>('original')
|
||||
const materialMode = ref<MaterialMode>('original')
|
||||
const edgeThreshold = ref(85)
|
||||
const load3DSceneRef = ref<InstanceType<typeof Load3DScene> | null>(null)
|
||||
|
||||
const {
|
||||
// configs
|
||||
sceneConfig,
|
||||
modelConfig,
|
||||
cameraConfig,
|
||||
lightConfig,
|
||||
|
||||
// other state
|
||||
isRecording,
|
||||
isPreview,
|
||||
hasRecording,
|
||||
recordingDuration,
|
||||
animations,
|
||||
playing,
|
||||
selectedSpeed,
|
||||
selectedAnimation,
|
||||
loading,
|
||||
loadingMessage,
|
||||
|
||||
// Methods
|
||||
initializeLoad3d,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
handleStartRecording,
|
||||
handleStopRecording,
|
||||
handleExportRecording,
|
||||
handleClearRecording,
|
||||
handleBackgroundImageUpdate,
|
||||
handleExportModel,
|
||||
handleModelDrop,
|
||||
cleanup
|
||||
} = useLoad3d(node as Ref<LGraphNode | null>)
|
||||
|
||||
const isRecording = ref(false)
|
||||
const hasRecording = ref(false)
|
||||
const recordingDuration = ref(0)
|
||||
const showRecordingControls = ref(!inputSpec.isPreview)
|
||||
const enable3DViewer = computed(() =>
|
||||
useSettingStore().get('Comfy.Load3D.3DViewerEnable')
|
||||
)
|
||||
|
||||
const showPreviewButton = computed(() => {
|
||||
return !type.includes('Preview')
|
||||
})
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (load3DSceneRef.value?.load3d) {
|
||||
load3DSceneRef.value.load3d.updateStatusMouseOnScene(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (load3DSceneRef.value?.load3d) {
|
||||
load3DSceneRef.value.load3d.updateStatusMouseOnScene(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartRecording = async () => {
|
||||
if (load3DSceneRef.value?.load3d) {
|
||||
await load3DSceneRef.value.load3d.startRecording()
|
||||
isRecording.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleStopRecording = () => {
|
||||
if (load3DSceneRef.value?.load3d) {
|
||||
load3DSceneRef.value.load3d.stopRecording()
|
||||
isRecording.value = false
|
||||
recordingDuration.value = load3DSceneRef.value.load3d.getRecordingDuration()
|
||||
hasRecording.value = recordingDuration.value > 0
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportRecording = () => {
|
||||
if (load3DSceneRef.value?.load3d) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
const filename = `${timestamp}-scene-recording.mp4`
|
||||
load3DSceneRef.value.load3d.exportRecording(filename)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearRecording = () => {
|
||||
if (load3DSceneRef.value?.load3d) {
|
||||
load3DSceneRef.value.load3d.clearRecording()
|
||||
hasRecording.value = false
|
||||
recordingDuration.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const switchCamera = () => {
|
||||
cameraType.value =
|
||||
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
|
||||
|
||||
showFOVButton.value = cameraType.value === 'perspective'
|
||||
|
||||
node.properties['Camera Type'] = cameraType.value
|
||||
}
|
||||
|
||||
const togglePreview = (value: boolean) => {
|
||||
showPreview.value = value
|
||||
|
||||
node.properties['Show Preview'] = showPreview.value
|
||||
}
|
||||
|
||||
const toggleGrid = (value: boolean) => {
|
||||
showGrid.value = value
|
||||
|
||||
node.properties['Show Grid'] = showGrid.value
|
||||
}
|
||||
|
||||
const handleUpdateLightIntensity = (value: number) => {
|
||||
lightIntensity.value = value
|
||||
|
||||
node.properties['Light Intensity'] = lightIntensity.value
|
||||
}
|
||||
|
||||
const handleBackgroundImageUpdate = async (file: File | null) => {
|
||||
if (!file) {
|
||||
hasBackgroundImage.value = false
|
||||
backgroundImage.value = ''
|
||||
node.properties['Background Image'] = ''
|
||||
return
|
||||
}
|
||||
|
||||
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
|
||||
|
||||
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
|
||||
|
||||
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
|
||||
|
||||
node.properties['Background Image'] = backgroundImage.value
|
||||
}
|
||||
|
||||
const handleUpdateFOV = (value: number) => {
|
||||
fov.value = value
|
||||
|
||||
node.properties['FOV'] = fov.value
|
||||
}
|
||||
|
||||
const handleUpdateEdgeThreshold = (value: number) => {
|
||||
edgeThreshold.value = value
|
||||
|
||||
node.properties['Edge Threshold'] = edgeThreshold.value
|
||||
}
|
||||
|
||||
const handleBackgroundColorChange = (value: string) => {
|
||||
backgroundColor.value = value
|
||||
|
||||
node.properties['Background Color'] = value
|
||||
}
|
||||
|
||||
const handleUpdateUpDirection = (value: UpDirection) => {
|
||||
upDirection.value = value
|
||||
|
||||
node.properties['Up Direction'] = value
|
||||
}
|
||||
|
||||
const handleUpdateMaterialMode = (value: MaterialMode) => {
|
||||
materialMode.value = value
|
||||
|
||||
node.properties['Material Mode'] = value
|
||||
}
|
||||
|
||||
const handleExportModel = async (format: string) => {
|
||||
if (!load3DSceneRef.value?.load3d) {
|
||||
useToastStore().addAlert(t('toastMessages.no3dSceneToExport'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await load3DSceneRef.value.load3d.exportModel(format)
|
||||
} catch (error) {
|
||||
console.error('Error exporting model:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToExportModel', {
|
||||
format: format.toUpperCase()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const listenMaterialModeChange = (mode: MaterialMode) => {
|
||||
materialMode.value = mode
|
||||
|
||||
showLightIntensityButton.value = mode === 'original'
|
||||
}
|
||||
|
||||
const listenUpDirectionChange = (value: UpDirection) => {
|
||||
upDirection.value = value
|
||||
}
|
||||
|
||||
const listenEdgeThresholdChange = (value: number) => {
|
||||
edgeThreshold.value = value
|
||||
}
|
||||
|
||||
const listenRecordingStatusChange = (value: boolean) => {
|
||||
isRecording.value = value
|
||||
|
||||
if (!value && load3DSceneRef.value?.load3d) {
|
||||
recordingDuration.value = load3DSceneRef.value.load3d.getRecordingDuration()
|
||||
hasRecording.value = recordingDuration.value > 0
|
||||
}
|
||||
}
|
||||
|
||||
const listenBackgroundColorChange = (value: string) => {
|
||||
backgroundColor.value = value
|
||||
}
|
||||
|
||||
const listenLightIntensityChange = (value: number) => {
|
||||
lightIntensity.value = value
|
||||
}
|
||||
|
||||
const listenFOVChange = (value: number) => {
|
||||
fov.value = value
|
||||
}
|
||||
|
||||
const listenCameraTypeChange = (value: CameraType) => {
|
||||
cameraType.value = value
|
||||
showFOVButton.value = cameraType.value === 'perspective'
|
||||
}
|
||||
|
||||
const listenShowGridChange = (value: boolean) => {
|
||||
showGrid.value = value
|
||||
}
|
||||
|
||||
const listenShowPreviewChange = (value: boolean) => {
|
||||
showPreview.value = value
|
||||
}
|
||||
|
||||
const listenBackgroundImageChange = (value: string) => {
|
||||
backgroundImage.value = value
|
||||
|
||||
if (backgroundImage.value && backgroundImage.value !== '') {
|
||||
hasBackgroundImage.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
336
src/components/load3d/Load3DAnimation.vue
Normal file
@@ -0,0 +1,336 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative h-full w-full"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<Load3DAnimationScene
|
||||
ref="load3DAnimationSceneRef"
|
||||
:node="node"
|
||||
:input-spec="inputSpec"
|
||||
:background-color="backgroundColor"
|
||||
:show-grid="showGrid"
|
||||
:light-intensity="lightIntensity"
|
||||
:fov="fov"
|
||||
:camera-type="cameraType"
|
||||
:show-preview="showPreview"
|
||||
:show-f-o-v-button="showFOVButton"
|
||||
:show-light-intensity-button="showLightIntensityButton"
|
||||
:playing="playing"
|
||||
:selected-speed="selectedSpeed"
|
||||
:selected-animation="selectedAnimation"
|
||||
:background-image="backgroundImage"
|
||||
:up-direction="upDirection"
|
||||
:material-mode="materialMode"
|
||||
@material-mode-change="listenMaterialModeChange"
|
||||
@background-color-change="listenBackgroundColorChange"
|
||||
@light-intensity-change="listenLightIntensityChange"
|
||||
@fov-change="listenFOVChange"
|
||||
@camera-type-change="listenCameraTypeChange"
|
||||
@show-grid-change="listenShowGridChange"
|
||||
@show-preview-change="listenShowPreviewChange"
|
||||
@background-image-change="listenBackgroundImageChange"
|
||||
@animation-list-change="animationListChange"
|
||||
@up-direction-change="listenUpDirectionChange"
|
||||
@recording-status-change="listenRecordingStatusChange"
|
||||
/>
|
||||
<div class="pointer-events-none absolute top-0 left-0 h-full w-full">
|
||||
<Load3DControls
|
||||
:input-spec="inputSpec"
|
||||
:background-color="backgroundColor"
|
||||
:show-grid="showGrid"
|
||||
:show-preview="showPreview"
|
||||
:light-intensity="lightIntensity"
|
||||
:show-light-intensity-button="showLightIntensityButton"
|
||||
:fov="fov"
|
||||
:show-f-o-v-button="showFOVButton"
|
||||
:show-preview-button="showPreviewButton"
|
||||
:camera-type="cameraType"
|
||||
:has-background-image="hasBackgroundImage"
|
||||
:up-direction="upDirection"
|
||||
:material-mode="materialMode"
|
||||
@update-background-image="handleBackgroundImageUpdate"
|
||||
@switch-camera="switchCamera"
|
||||
@toggle-grid="toggleGrid"
|
||||
@update-background-color="handleBackgroundColorChange"
|
||||
@update-light-intensity="handleUpdateLightIntensity"
|
||||
@toggle-preview="togglePreview"
|
||||
@update-f-o-v="handleUpdateFOV"
|
||||
@update-up-direction="handleUpdateUpDirection"
|
||||
@update-material-mode="handleUpdateMaterialMode"
|
||||
/>
|
||||
<Load3DAnimationControls
|
||||
:animations="animations"
|
||||
:playing="playing"
|
||||
@toggle-play="togglePlay"
|
||||
@speed-change="speedChange"
|
||||
@animation-change="animationChange"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showRecordingControls"
|
||||
class="pointer-events-auto absolute top-12 right-2 z-20"
|
||||
>
|
||||
<RecordingControls
|
||||
:node="node"
|
||||
:is-recording="isRecording"
|
||||
:has-recording="hasRecording"
|
||||
:recording-duration="recordingDuration"
|
||||
@start-recording="handleStartRecording"
|
||||
@stop-recording="handleStopRecording"
|
||||
@export-recording="handleExportRecording"
|
||||
@clear-recording="handleClearRecording"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import Load3DAnimationControls from '@/components/load3d/Load3DAnimationControls.vue'
|
||||
import Load3DAnimationScene from '@/components/load3d/Load3DAnimationScene.vue'
|
||||
import Load3DControls from '@/components/load3d/Load3DControls.vue'
|
||||
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type {
|
||||
AnimationItem,
|
||||
CameraType,
|
||||
Load3DAnimationNodeType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
|
||||
const { widget } = defineProps<{
|
||||
widget: ComponentWidget<string[]>
|
||||
}>()
|
||||
|
||||
const inputSpec = widget.inputSpec as CustomInputSpec
|
||||
|
||||
const node = widget.node
|
||||
const type = inputSpec.type as Load3DAnimationNodeType
|
||||
|
||||
const backgroundColor = ref('#000000')
|
||||
const showGrid = ref(true)
|
||||
const showPreview = ref(false)
|
||||
const lightIntensity = ref(5)
|
||||
const showLightIntensityButton = ref(true)
|
||||
const fov = ref(75)
|
||||
const showFOVButton = ref(true)
|
||||
const cameraType = ref<'perspective' | 'orthographic'>('perspective')
|
||||
const hasBackgroundImage = ref(false)
|
||||
|
||||
const animations = ref<AnimationItem[]>([])
|
||||
const playing = ref(false)
|
||||
const selectedSpeed = ref(1)
|
||||
const selectedAnimation = ref(0)
|
||||
const backgroundImage = ref('')
|
||||
|
||||
const isRecording = ref(false)
|
||||
const hasRecording = ref(false)
|
||||
const recordingDuration = ref(0)
|
||||
const showRecordingControls = ref(!inputSpec.isPreview)
|
||||
|
||||
const showPreviewButton = computed(() => {
|
||||
return !type.includes('Preview')
|
||||
})
|
||||
|
||||
const load3DAnimationSceneRef = ref<InstanceType<
|
||||
typeof Load3DAnimationScene
|
||||
> | null>(null)
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
|
||||
if (sceneRef?.load3d) {
|
||||
sceneRef.load3d.updateStatusMouseOnScene(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
|
||||
if (sceneRef?.load3d) {
|
||||
sceneRef.load3d.updateStatusMouseOnScene(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartRecording = async () => {
|
||||
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
|
||||
if (sceneRef?.load3d) {
|
||||
await sceneRef.load3d.startRecording()
|
||||
isRecording.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleStopRecording = () => {
|
||||
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
|
||||
if (sceneRef?.load3d) {
|
||||
sceneRef.load3d.stopRecording()
|
||||
isRecording.value = false
|
||||
recordingDuration.value = sceneRef.load3d.getRecordingDuration()
|
||||
hasRecording.value = recordingDuration.value > 0
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportRecording = () => {
|
||||
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
|
||||
if (sceneRef?.load3d) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
const filename = `${timestamp}-animation-recording.mp4`
|
||||
sceneRef.load3d.exportRecording(filename)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearRecording = () => {
|
||||
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
|
||||
if (sceneRef?.load3d) {
|
||||
sceneRef.load3d.clearRecording()
|
||||
hasRecording.value = false
|
||||
recordingDuration.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const listenRecordingStatusChange = (value: boolean) => {
|
||||
isRecording.value = value
|
||||
|
||||
if (!value) {
|
||||
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
|
||||
if (sceneRef?.load3d) {
|
||||
recordingDuration.value = sceneRef.load3d.getRecordingDuration()
|
||||
hasRecording.value = recordingDuration.value > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const switchCamera = () => {
|
||||
cameraType.value =
|
||||
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
|
||||
|
||||
showFOVButton.value = cameraType.value === 'perspective'
|
||||
|
||||
node.properties['Camera Type'] = cameraType.value
|
||||
}
|
||||
|
||||
const togglePreview = (value: boolean) => {
|
||||
showPreview.value = value
|
||||
|
||||
node.properties['Show Preview'] = showPreview.value
|
||||
}
|
||||
|
||||
const toggleGrid = (value: boolean) => {
|
||||
showGrid.value = value
|
||||
|
||||
node.properties['Show Grid'] = showGrid.value
|
||||
}
|
||||
|
||||
const handleUpdateLightIntensity = (value: number) => {
|
||||
lightIntensity.value = value
|
||||
|
||||
node.properties['Light Intensity'] = lightIntensity.value
|
||||
}
|
||||
|
||||
const handleBackgroundImageUpdate = async (file: File | null) => {
|
||||
if (!file) {
|
||||
hasBackgroundImage.value = false
|
||||
backgroundImage.value = ''
|
||||
node.properties['Background Image'] = ''
|
||||
return
|
||||
}
|
||||
|
||||
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
|
||||
|
||||
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
|
||||
|
||||
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
|
||||
|
||||
node.properties['Background Image'] = backgroundImage.value
|
||||
}
|
||||
|
||||
const handleUpdateFOV = (value: number) => {
|
||||
fov.value = value
|
||||
|
||||
node.properties['FOV'] = fov.value
|
||||
}
|
||||
|
||||
const materialMode = ref<MaterialMode>('original')
|
||||
const upDirection = ref<UpDirection>('original')
|
||||
|
||||
const handleUpdateUpDirection = (value: UpDirection) => {
|
||||
upDirection.value = value
|
||||
|
||||
node.properties['Up Direction'] = value
|
||||
}
|
||||
|
||||
const handleUpdateMaterialMode = (value: MaterialMode) => {
|
||||
materialMode.value = value
|
||||
|
||||
node.properties['Material Mode'] = value
|
||||
}
|
||||
|
||||
const handleBackgroundColorChange = (value: string) => {
|
||||
backgroundColor.value = value
|
||||
|
||||
node.properties['Background Color'] = value
|
||||
}
|
||||
|
||||
const togglePlay = (value: boolean) => {
|
||||
playing.value = value
|
||||
}
|
||||
|
||||
const speedChange = (value: number) => {
|
||||
selectedSpeed.value = value
|
||||
}
|
||||
|
||||
const animationChange = (value: number) => {
|
||||
selectedAnimation.value = value
|
||||
}
|
||||
|
||||
const animationListChange = (value: any) => {
|
||||
animations.value = value
|
||||
}
|
||||
|
||||
const listenMaterialModeChange = (mode: MaterialMode) => {
|
||||
materialMode.value = mode
|
||||
|
||||
showLightIntensityButton.value = mode === 'original'
|
||||
}
|
||||
|
||||
const listenUpDirectionChange = (value: UpDirection) => {
|
||||
upDirection.value = value
|
||||
}
|
||||
|
||||
const listenBackgroundColorChange = (value: string) => {
|
||||
backgroundColor.value = value
|
||||
}
|
||||
|
||||
const listenLightIntensityChange = (value: number) => {
|
||||
lightIntensity.value = value
|
||||
}
|
||||
|
||||
const listenFOVChange = (value: number) => {
|
||||
fov.value = value
|
||||
}
|
||||
|
||||
const listenCameraTypeChange = (value: CameraType) => {
|
||||
cameraType.value = value
|
||||
|
||||
showFOVButton.value = cameraType.value === 'perspective'
|
||||
}
|
||||
|
||||
const listenShowGridChange = (value: boolean) => {
|
||||
showGrid.value = value
|
||||
}
|
||||
|
||||
const listenShowPreviewChange = (value: boolean) => {
|
||||
showPreview.value = value
|
||||
}
|
||||
|
||||
const listenBackgroundImageChange = (value: string) => {
|
||||
backgroundImage.value = value
|
||||
|
||||
if (backgroundImage.value && backgroundImage.value !== '') {
|
||||
hasBackgroundImage.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -15,6 +15,7 @@
|
||||
option-label="name"
|
||||
option-value="value"
|
||||
class="w-24"
|
||||
@change="speedChange"
|
||||
/>
|
||||
|
||||
<Select
|
||||
@@ -23,6 +24,7 @@
|
||||
option-label="name"
|
||||
option-value="index"
|
||||
class="w-32"
|
||||
@change="animationChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -30,13 +32,23 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Select from 'primevue/select'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
type Animation = { name: string; index: number }
|
||||
const props = defineProps<{
|
||||
animations: Array<{ name: string; index: number }>
|
||||
playing: boolean
|
||||
}>()
|
||||
|
||||
const animations = defineModel<Animation[]>('animations')
|
||||
const playing = defineModel<boolean>('playing')
|
||||
const selectedSpeed = defineModel<number>('selectedSpeed')
|
||||
const selectedAnimation = defineModel<number>('selectedAnimation')
|
||||
const emit = defineEmits<{
|
||||
(e: 'togglePlay', value: boolean): void
|
||||
(e: 'speedChange', value: number): void
|
||||
(e: 'animationChange', value: number): void
|
||||
}>()
|
||||
|
||||
const animations = ref(props.animations)
|
||||
const playing = ref(props.playing)
|
||||
const selectedSpeed = ref(1)
|
||||
const selectedAnimation = ref(0)
|
||||
|
||||
const speedOptions = [
|
||||
{ name: '0.1x', value: 0.1 },
|
||||
@@ -46,7 +58,24 @@ const speedOptions = [
|
||||
{ name: '2x', value: 2 }
|
||||
]
|
||||
|
||||
watch(
|
||||
() => props.animations,
|
||||
(newVal) => {
|
||||
animations.value = newVal
|
||||
}
|
||||
)
|
||||
|
||||
const togglePlay = () => {
|
||||
playing.value = !playing.value
|
||||
|
||||
emit('togglePlay', playing.value)
|
||||
}
|
||||
|
||||
const speedChange = () => {
|
||||
emit('speedChange', selectedSpeed.value)
|
||||
}
|
||||
|
||||
const animationChange = () => {
|
||||
emit('animationChange', selectedAnimation.value)
|
||||
}
|
||||
</script>
|
||||
208
src/components/load3d/Load3DAnimationScene.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<Load3DScene
|
||||
ref="load3DSceneRef"
|
||||
:node="node"
|
||||
:input-spec="inputSpec"
|
||||
:background-color="backgroundColor"
|
||||
:show-grid="showGrid"
|
||||
:light-intensity="lightIntensity"
|
||||
:fov="fov"
|
||||
:camera-type="cameraType"
|
||||
:show-preview="showPreview"
|
||||
:extra-listeners="animationListeners"
|
||||
:background-image="backgroundImage"
|
||||
:up-direction="upDirection"
|
||||
:material-mode="materialMode"
|
||||
@material-mode-change="listenMaterialModeChange"
|
||||
@background-color-change="listenBackgroundColorChange"
|
||||
@light-intensity-change="listenLightIntensityChange"
|
||||
@fov-change="listenFOVChange"
|
||||
@camera-type-change="listenCameraTypeChange"
|
||||
@show-grid-change="listenShowGridChange"
|
||||
@show-preview-change="listenShowPreviewChange"
|
||||
@recording-status-change="listenRecordingStatusChange"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import Load3DScene from '@/components/load3d/Load3DScene.vue'
|
||||
import type Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import type {
|
||||
CameraType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
const props = defineProps<{
|
||||
node: any
|
||||
inputSpec: CustomInputSpec
|
||||
backgroundColor: string
|
||||
showGrid: boolean
|
||||
lightIntensity: number
|
||||
fov: number
|
||||
cameraType: CameraType
|
||||
showPreview: boolean
|
||||
materialMode: MaterialMode
|
||||
upDirection: UpDirection
|
||||
showFOVButton: boolean
|
||||
showLightIntensityButton: boolean
|
||||
playing: boolean
|
||||
selectedSpeed: number
|
||||
selectedAnimation: number
|
||||
backgroundImage: string
|
||||
}>()
|
||||
|
||||
const node = ref(props.node)
|
||||
const backgroundColor = ref(props.backgroundColor)
|
||||
const showPreview = ref(props.showPreview)
|
||||
const fov = ref(props.fov)
|
||||
const lightIntensity = ref(props.lightIntensity)
|
||||
const cameraType = ref(props.cameraType)
|
||||
const showGrid = ref(props.showGrid)
|
||||
const upDirection = ref(props.upDirection)
|
||||
const materialMode = ref(props.materialMode)
|
||||
const showFOVButton = ref(props.showFOVButton)
|
||||
const showLightIntensityButton = ref(props.showLightIntensityButton)
|
||||
const load3DSceneRef = ref<InstanceType<typeof Load3DScene> | null>(null)
|
||||
|
||||
watch(
|
||||
() => props.cameraType,
|
||||
(newValue) => {
|
||||
cameraType.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showGrid,
|
||||
(newValue) => {
|
||||
showGrid.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.backgroundColor,
|
||||
(newValue) => {
|
||||
backgroundColor.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.lightIntensity,
|
||||
(newValue) => {
|
||||
lightIntensity.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.fov,
|
||||
(newValue) => {
|
||||
fov.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.upDirection,
|
||||
(newValue) => {
|
||||
upDirection.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.materialMode,
|
||||
(newValue) => {
|
||||
materialMode.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showPreview,
|
||||
(newValue) => {
|
||||
showPreview.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.playing,
|
||||
(newValue) => {
|
||||
const load3d = load3DSceneRef.value?.load3d as Load3dAnimation | null
|
||||
load3d?.toggleAnimation(newValue)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.selectedSpeed,
|
||||
(newValue) => {
|
||||
const load3d = load3DSceneRef.value?.load3d as Load3dAnimation | null
|
||||
load3d?.setAnimationSpeed(newValue)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.selectedAnimation,
|
||||
(newValue) => {
|
||||
const load3d = load3DSceneRef.value?.load3d as Load3dAnimation | null
|
||||
load3d?.updateSelectedAnimation(newValue)
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'animationListChange', animationList: string): void
|
||||
(e: 'materialModeChange', materialMode: MaterialMode): void
|
||||
(e: 'backgroundColorChange', color: string): void
|
||||
(e: 'lightIntensityChange', lightIntensity: number): void
|
||||
(e: 'fovChange', fov: number): void
|
||||
(e: 'cameraTypeChange', cameraType: CameraType): void
|
||||
(e: 'showGridChange', showGrid: boolean): void
|
||||
(e: 'showPreviewChange', showPreview: boolean): void
|
||||
(e: 'upDirectionChange', direction: UpDirection): void
|
||||
(e: 'recording-status-change', status: boolean): void
|
||||
}>()
|
||||
|
||||
const listenMaterialModeChange = (mode: MaterialMode) => {
|
||||
materialMode.value = mode
|
||||
|
||||
showLightIntensityButton.value = mode === 'original'
|
||||
}
|
||||
|
||||
const listenBackgroundColorChange = (value: string) => {
|
||||
backgroundColor.value = value
|
||||
}
|
||||
|
||||
const listenLightIntensityChange = (value: number) => {
|
||||
lightIntensity.value = value
|
||||
}
|
||||
|
||||
const listenFOVChange = (value: number) => {
|
||||
fov.value = value
|
||||
}
|
||||
|
||||
const listenCameraTypeChange = (value: CameraType) => {
|
||||
cameraType.value = value
|
||||
|
||||
showFOVButton.value = cameraType.value === 'perspective'
|
||||
}
|
||||
|
||||
const listenShowGridChange = (value: boolean) => {
|
||||
showGrid.value = value
|
||||
}
|
||||
|
||||
const listenShowPreviewChange = (value: boolean) => {
|
||||
showPreview.value = value
|
||||
}
|
||||
|
||||
const listenRecordingStatusChange = (value: boolean) => {
|
||||
emit('recording-status-change', value)
|
||||
}
|
||||
|
||||
const animationListeners = {
|
||||
animationListChange: (newValue: any) => {
|
||||
emit('animationListChange', newValue)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
load3DSceneRef
|
||||
})
|
||||
</script>
|
||||
@@ -1,10 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="pointer-events-auto absolute top-12 left-2 z-20 flex flex-col rounded-lg bg-smoke-700/30"
|
||||
@pointerdown.stop
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
@wheel.stop
|
||||
>
|
||||
<div class="show-menu relative">
|
||||
<Button class="p-button-rounded p-button-text" @click="toggleMenu">
|
||||
@@ -24,9 +20,7 @@
|
||||
@click="selectCategory(category)"
|
||||
>
|
||||
<i :class="getCategoryIcon(category)" />
|
||||
<span class="whitespace-nowrap text-white">{{
|
||||
$t(categoryLabels[category])
|
||||
}}</span>
|
||||
<span class="text-white">{{ t(categoryLabels[category]) }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,47 +28,71 @@
|
||||
|
||||
<div v-show="activeCategory" class="rounded-lg bg-smoke-700/30">
|
||||
<SceneControls
|
||||
v-if="showSceneControls"
|
||||
v-if="activeCategory === 'scene'"
|
||||
ref="sceneControlsRef"
|
||||
v-model:show-grid="sceneConfig!.showGrid"
|
||||
v-model:background-color="sceneConfig!.backgroundColor"
|
||||
v-model:background-image="sceneConfig!.backgroundImage"
|
||||
:background-color="backgroundColor"
|
||||
:show-grid="showGrid"
|
||||
:has-background-image="hasBackgroundImage"
|
||||
@toggle-grid="handleToggleGrid"
|
||||
@update-background-color="handleBackgroundColorChange"
|
||||
@update-background-image="handleBackgroundImageUpdate"
|
||||
/>
|
||||
|
||||
<ModelControls
|
||||
v-if="showModelControls"
|
||||
v-if="activeCategory === 'model'"
|
||||
ref="modelControlsRef"
|
||||
v-model:material-mode="modelConfig!.materialMode"
|
||||
v-model:up-direction="modelConfig!.upDirection"
|
||||
:input-spec="inputSpec"
|
||||
:up-direction="upDirection"
|
||||
:material-mode="materialMode"
|
||||
:edge-threshold="edgeThreshold"
|
||||
@update-up-direction="handleUpdateUpDirection"
|
||||
@update-material-mode="handleUpdateMaterialMode"
|
||||
@update-edge-threshold="handleUpdateEdgeThreshold"
|
||||
/>
|
||||
|
||||
<CameraControls
|
||||
v-if="showCameraControls"
|
||||
v-if="activeCategory === 'camera'"
|
||||
ref="cameraControlsRef"
|
||||
v-model:camera-type="cameraConfig!.cameraType"
|
||||
v-model:fov="cameraConfig!.fov"
|
||||
:camera-type="cameraType"
|
||||
:fov="fov"
|
||||
:show-f-o-v-button="showFOVButton"
|
||||
@switch-camera="switchCamera"
|
||||
@update-f-o-v="handleUpdateFOV"
|
||||
/>
|
||||
|
||||
<LightControls
|
||||
v-if="showLightControls"
|
||||
v-if="activeCategory === 'light'"
|
||||
ref="lightControlsRef"
|
||||
v-model:light-intensity="lightConfig!.intensity"
|
||||
v-model:material-mode="modelConfig!.materialMode"
|
||||
:light-intensity="lightIntensity"
|
||||
:show-light-intensity-button="showLightIntensityButton"
|
||||
@update-light-intensity="handleUpdateLightIntensity"
|
||||
/>
|
||||
|
||||
<ExportControls
|
||||
v-if="showExportControls"
|
||||
v-if="activeCategory === 'export'"
|
||||
ref="exportControlsRef"
|
||||
@export-model="handleExportModel"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showPreviewButton">
|
||||
<Button class="p-button-rounded p-button-text" @click="togglePreview">
|
||||
<i
|
||||
v-tooltip.right="{ value: t('load3d.previewOutput'), showDelay: 300 }"
|
||||
:class="[
|
||||
'pi',
|
||||
showPreview ? 'pi-eye' : 'pi-eye-slash',
|
||||
'text-lg text-white'
|
||||
]"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import CameraControls from '@/components/load3d/controls/CameraControls.vue'
|
||||
import ExportControls from '@/components/load3d/controls/ExportControls.vue'
|
||||
@@ -82,16 +100,31 @@ import LightControls from '@/components/load3d/controls/LightControls.vue'
|
||||
import ModelControls from '@/components/load3d/controls/ModelControls.vue'
|
||||
import SceneControls from '@/components/load3d/controls/SceneControls.vue'
|
||||
import type {
|
||||
CameraConfig,
|
||||
LightConfig,
|
||||
ModelConfig,
|
||||
SceneConfig
|
||||
CameraType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
const sceneConfig = defineModel<SceneConfig>('sceneConfig')
|
||||
const modelConfig = defineModel<ModelConfig>('modelConfig')
|
||||
const cameraConfig = defineModel<CameraConfig>('cameraConfig')
|
||||
const lightConfig = defineModel<LightConfig>('lightConfig')
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const props = defineProps<{
|
||||
inputSpec: CustomInputSpec
|
||||
backgroundColor: string
|
||||
showGrid: boolean
|
||||
showPreview: boolean
|
||||
lightIntensity: number
|
||||
showLightIntensityButton: boolean
|
||||
fov: number
|
||||
showFOVButton: boolean
|
||||
showPreviewButton: boolean
|
||||
cameraType: CameraType
|
||||
hasBackgroundImage?: boolean
|
||||
upDirection: UpDirection
|
||||
materialMode: MaterialMode
|
||||
edgeThreshold?: number
|
||||
}>()
|
||||
|
||||
const isMenuOpen = ref(false)
|
||||
const activeCategory = ref<string>('scene')
|
||||
@@ -104,25 +137,14 @@ const categoryLabels: Record<string, string> = {
|
||||
}
|
||||
|
||||
const availableCategories = computed(() => {
|
||||
return ['scene', 'model', 'camera', 'light', 'export']
|
||||
})
|
||||
const baseCategories = ['scene', 'model', 'camera', 'light']
|
||||
|
||||
const showSceneControls = computed(
|
||||
() => activeCategory.value === 'scene' && !!sceneConfig.value
|
||||
)
|
||||
const showModelControls = computed(
|
||||
() => activeCategory.value === 'model' && !!modelConfig.value
|
||||
)
|
||||
const showCameraControls = computed(
|
||||
() => activeCategory.value === 'camera' && !!cameraConfig.value
|
||||
)
|
||||
const showLightControls = computed(
|
||||
() =>
|
||||
activeCategory.value === 'light' &&
|
||||
!!lightConfig.value &&
|
||||
!!modelConfig.value
|
||||
)
|
||||
const showExportControls = computed(() => activeCategory.value === 'export')
|
||||
if (!props.inputSpec.isAnimation) {
|
||||
return [...baseCategories, 'export']
|
||||
}
|
||||
|
||||
return baseCategories
|
||||
})
|
||||
|
||||
const toggleMenu = () => {
|
||||
isMenuOpen.value = !isMenuOpen.value
|
||||
@@ -146,14 +168,73 @@ const getCategoryIcon = (category: string) => {
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'switchCamera'): void
|
||||
(e: 'toggleGrid', value: boolean): void
|
||||
(e: 'updateBackgroundColor', color: string): void
|
||||
(e: 'updateLightIntensity', value: number): void
|
||||
(e: 'updateFOV', value: number): void
|
||||
(e: 'togglePreview', value: boolean): void
|
||||
(e: 'updateBackgroundImage', file: File | null): void
|
||||
(e: 'updateUpDirection', direction: UpDirection): void
|
||||
(e: 'updateMaterialMode', mode: MaterialMode): void
|
||||
(e: 'updateEdgeThreshold', value: number): void
|
||||
(e: 'exportModel', format: string): void
|
||||
}>()
|
||||
|
||||
const backgroundColor = ref(props.backgroundColor)
|
||||
const showGrid = ref(props.showGrid)
|
||||
const showPreview = ref(props.showPreview)
|
||||
const lightIntensity = ref(props.lightIntensity)
|
||||
const upDirection = ref(props.upDirection || 'original')
|
||||
const materialMode = ref(props.materialMode || 'original')
|
||||
const showLightIntensityButton = ref(props.showLightIntensityButton)
|
||||
const fov = ref(props.fov)
|
||||
const showFOVButton = ref(props.showFOVButton)
|
||||
const showPreviewButton = ref(props.showPreviewButton)
|
||||
const hasBackgroundImage = ref(props.hasBackgroundImage)
|
||||
const edgeThreshold = ref(props.edgeThreshold)
|
||||
|
||||
const switchCamera = () => {
|
||||
emit('switchCamera')
|
||||
}
|
||||
|
||||
const togglePreview = () => {
|
||||
showPreview.value = !showPreview.value
|
||||
emit('togglePreview', showPreview.value)
|
||||
}
|
||||
|
||||
const handleToggleGrid = (value: boolean) => {
|
||||
emit('toggleGrid', value)
|
||||
}
|
||||
|
||||
const handleBackgroundColorChange = (value: string) => {
|
||||
emit('updateBackgroundColor', value)
|
||||
}
|
||||
|
||||
const handleBackgroundImageUpdate = (file: File | null) => {
|
||||
emit('updateBackgroundImage', file)
|
||||
}
|
||||
|
||||
const handleUpdateUpDirection = (direction: UpDirection) => {
|
||||
emit('updateUpDirection', direction)
|
||||
}
|
||||
|
||||
const handleUpdateMaterialMode = (mode: MaterialMode) => {
|
||||
emit('updateMaterialMode', mode)
|
||||
}
|
||||
|
||||
const handleUpdateEdgeThreshold = (value: number) => {
|
||||
emit('updateEdgeThreshold', value)
|
||||
}
|
||||
|
||||
const handleUpdateLightIntensity = (value: number) => {
|
||||
emit('updateLightIntensity', value)
|
||||
}
|
||||
|
||||
const handleUpdateFOV = (value: number) => {
|
||||
emit('updateFOV', value)
|
||||
}
|
||||
|
||||
const handleExportModel = (format: string) => {
|
||||
emit('exportModel', format)
|
||||
}
|
||||
@@ -166,6 +247,101 @@ const closeSlider = (e: MouseEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.upDirection,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
upDirection.value = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.backgroundColor,
|
||||
(newValue) => {
|
||||
backgroundColor.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.fov,
|
||||
(newValue) => {
|
||||
fov.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.lightIntensity,
|
||||
(newValue) => {
|
||||
lightIntensity.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showFOVButton,
|
||||
(newValue) => {
|
||||
showFOVButton.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showLightIntensityButton,
|
||||
(newValue) => {
|
||||
showLightIntensityButton.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.upDirection,
|
||||
(newValue) => {
|
||||
upDirection.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.materialMode,
|
||||
(newValue) => {
|
||||
materialMode.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showPreviewButton,
|
||||
(newValue) => {
|
||||
showPreviewButton.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showPreview,
|
||||
(newValue) => {
|
||||
showPreview.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.hasBackgroundImage,
|
||||
(newValue) => {
|
||||
hasBackgroundImage.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.materialMode,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
materialMode.value = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.edgeThreshold,
|
||||
(newValue) => {
|
||||
edgeThreshold.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeSlider)
|
||||
})
|
||||
|
||||
@@ -1,72 +1,238 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
class="relative h-full w-full"
|
||||
data-capture-wheel="true"
|
||||
@pointerdown.stop
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
@mousedown.stop
|
||||
@mousemove.stop
|
||||
@mouseup.stop
|
||||
@contextmenu.stop.prevent
|
||||
@dragover.prevent.stop="handleDragOver"
|
||||
@dragleave.stop="handleDragLeave"
|
||||
@drop.prevent.stop="handleDrop"
|
||||
>
|
||||
<LoadingOverlay
|
||||
ref="loadingOverlayRef"
|
||||
:loading="loading"
|
||||
:loading-message="loadingMessage"
|
||||
/>
|
||||
<div
|
||||
v-if="!isPreview && isDragging"
|
||||
class="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg border-2 border-dashed border-blue-400 bg-blue-500/20 px-6 py-4 text-lg font-medium text-blue-100"
|
||||
>
|
||||
{{ dragMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<div ref="container" class="relative h-full w-full">
|
||||
<LoadingOverlay ref="loadingOverlayRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, toRaw, watch } from 'vue'
|
||||
|
||||
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
|
||||
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
|
||||
import type Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import type Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import type {
|
||||
CameraType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
|
||||
const props = defineProps<{
|
||||
initializeLoad3d: (containerRef: HTMLElement) => Promise<void>
|
||||
cleanup: () => void
|
||||
loading: boolean
|
||||
loadingMessage: string
|
||||
onModelDrop?: (file: File) => void | Promise<void>
|
||||
isPreview: boolean
|
||||
node: LGraphNode
|
||||
inputSpec: CustomInputSpec
|
||||
backgroundColor: string
|
||||
showGrid: boolean
|
||||
lightIntensity: number
|
||||
fov: number
|
||||
cameraType: CameraType
|
||||
showPreview: boolean
|
||||
backgroundImage: string
|
||||
upDirection: UpDirection
|
||||
materialMode: MaterialMode
|
||||
edgeThreshold?: number
|
||||
extraListeners?: Record<string, (value: any) => void>
|
||||
}>()
|
||||
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const node = ref(props.node)
|
||||
const load3d = ref<Load3d | Load3dAnimation | null>(null)
|
||||
const loadingOverlayRef = ref<InstanceType<typeof LoadingOverlay> | null>(null)
|
||||
|
||||
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
|
||||
useLoad3dDrag({
|
||||
onModelDrop: async (file) => {
|
||||
if (props.onModelDrop) {
|
||||
await props.onModelDrop(file)
|
||||
}
|
||||
},
|
||||
disabled: computed(() => props.isPreview)
|
||||
const eventConfig = {
|
||||
materialModeChange: (value: string) =>
|
||||
emit('materialModeChange', value as MaterialMode),
|
||||
backgroundColorChange: (value: string) =>
|
||||
emit('backgroundColorChange', value),
|
||||
lightIntensityChange: (value: number) => emit('lightIntensityChange', value),
|
||||
fovChange: (value: number) => emit('fovChange', value),
|
||||
cameraTypeChange: (value: string) =>
|
||||
emit('cameraTypeChange', value as CameraType),
|
||||
showGridChange: (value: boolean) => emit('showGridChange', value),
|
||||
showPreviewChange: (value: boolean) => emit('showPreviewChange', value),
|
||||
backgroundImageChange: (value: string) =>
|
||||
emit('backgroundImageChange', value),
|
||||
backgroundImageLoadingStart: () =>
|
||||
loadingOverlayRef.value?.startLoading(t('load3d.loadingBackgroundImage')),
|
||||
backgroundImageLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
|
||||
upDirectionChange: (value: string) =>
|
||||
emit('upDirectionChange', value as UpDirection),
|
||||
edgeThresholdChange: (value: number) => emit('edgeThresholdChange', value),
|
||||
modelLoadingStart: () =>
|
||||
loadingOverlayRef.value?.startLoading(t('load3d.loadingModel')),
|
||||
modelLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
|
||||
materialLoadingStart: () =>
|
||||
loadingOverlayRef.value?.startLoading(t('load3d.switchingMaterialMode')),
|
||||
materialLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
|
||||
exportLoadingStart: (message: string) => {
|
||||
loadingOverlayRef.value?.startLoading(message || t('load3d.exportingModel'))
|
||||
},
|
||||
exportLoadingEnd: () => {
|
||||
loadingOverlayRef.value?.endLoading()
|
||||
},
|
||||
recordingStatusChange: (value: boolean) =>
|
||||
emit('recordingStatusChange', value)
|
||||
} as const
|
||||
|
||||
watch(
|
||||
() => props.showPreview,
|
||||
(newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
rawLoad3d.togglePreview(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.cameraType,
|
||||
(newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
rawLoad3d.toggleCamera(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.fov,
|
||||
(newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
rawLoad3d.setFOV(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.lightIntensity,
|
||||
(newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
rawLoad3d.setLightIntensity(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showGrid,
|
||||
(newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
rawLoad3d.toggleGrid(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.backgroundColor,
|
||||
(newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
rawLoad3d.setBackgroundColor(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.backgroundImage,
|
||||
async (newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
await rawLoad3d.setBackgroundImage(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.upDirection,
|
||||
(newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
rawLoad3d.setUpDirection(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.materialMode,
|
||||
(newValue) => {
|
||||
if (load3d.value) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
rawLoad3d.setMaterialMode(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.edgeThreshold,
|
||||
(newValue) => {
|
||||
if (load3d.value && newValue) {
|
||||
const rawLoad3d = toRaw(load3d.value) as Load3d
|
||||
|
||||
rawLoad3d.setEdgeThreshold(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'materialModeChange', materialMode: MaterialMode): void
|
||||
(e: 'backgroundColorChange', color: string): void
|
||||
(e: 'lightIntensityChange', lightIntensity: number): void
|
||||
(e: 'fovChange', fov: number): void
|
||||
(e: 'cameraTypeChange', cameraType: CameraType): void
|
||||
(e: 'showGridChange', showGrid: boolean): void
|
||||
(e: 'showPreviewChange', showPreview: boolean): void
|
||||
(e: 'backgroundImageChange', backgroundImage: string): void
|
||||
(e: 'upDirectionChange', upDirection: UpDirection): void
|
||||
(e: 'edgeThresholdChange', threshold: number): void
|
||||
(e: 'recordingStatusChange', status: boolean): void
|
||||
}>()
|
||||
|
||||
const handleEvents = (action: 'add' | 'remove') => {
|
||||
if (!load3d.value) return
|
||||
|
||||
Object.entries(eventConfig).forEach(([event, handler]) => {
|
||||
const method = `${action}EventListener` as const
|
||||
load3d.value?.[method](event, handler)
|
||||
})
|
||||
|
||||
if (props.extraListeners) {
|
||||
Object.entries(props.extraListeners).forEach(([event, handler]) => {
|
||||
const method = `${action}EventListener` as const
|
||||
load3d.value?.[method](event, handler)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (container.value) {
|
||||
void props.initializeLoad3d(container.value)
|
||||
load3d.value = useLoad3dService().registerLoad3d(
|
||||
node.value as LGraphNode,
|
||||
container.value,
|
||||
props.inputSpec
|
||||
)
|
||||
}
|
||||
handleEvents('add')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
props.cleanup()
|
||||
handleEvents('remove')
|
||||
useLoad3dService().removeLoad3d(node.value as LGraphNode)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
load3d
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -11,20 +11,7 @@
|
||||
ref="containerRef"
|
||||
class="absolute h-full w-full"
|
||||
@resize="viewer.handleResize"
|
||||
@dragover.prevent.stop="handleDragOver"
|
||||
@dragleave.stop="handleDragLeave"
|
||||
@drop.prevent.stop="handleDrop"
|
||||
/>
|
||||
<div
|
||||
v-if="isDragging"
|
||||
class="pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg border-2 border-dashed border-blue-400 bg-blue-500/20 px-6 py-4 text-lg font-medium text-blue-100"
|
||||
>
|
||||
{{ dragMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-72 flex-col">
|
||||
@@ -88,7 +75,6 @@ import ExportControls from '@/components/load3d/controls/viewer/ViewerExportCont
|
||||
import LightControls from '@/components/load3d/controls/viewer/ViewerLightControls.vue'
|
||||
import ModelControls from '@/components/load3d/controls/viewer/ViewerModelControls.vue'
|
||||
import SceneControls from '@/components/load3d/controls/viewer/ViewerSceneControls.vue'
|
||||
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
@@ -106,14 +92,6 @@ const mutationObserver = ref<MutationObserver | null>(null)
|
||||
|
||||
const viewer = useLoad3dService().getOrCreateViewer(toRaw(props.node))
|
||||
|
||||
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
|
||||
useLoad3dDrag({
|
||||
onModelDrop: async (file) => {
|
||||
await viewer.handleModelDrop(file)
|
||||
},
|
||||
disabled: viewer.isPreview
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const source = useLoad3dService().getLoad3d(props.node)
|
||||
if (source && containerRef.value) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="bg-opacity-50 absolute inset-0 z-50 flex items-center justify-center bg-black"
|
||||
v-if="modelLoading"
|
||||
class="absolute inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="spinner" />
|
||||
@@ -15,10 +15,29 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
loading: boolean
|
||||
loadingMessage: string
|
||||
}>()
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const modelLoading = ref(false)
|
||||
const loadingMessage = ref('')
|
||||
|
||||
const startLoading = async (message?: string) => {
|
||||
loadingMessage.value = message || t('load3d.loadingModel')
|
||||
modelLoading.value = true
|
||||
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const endLoading = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
modelLoading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
startLoading,
|
||||
endLoading
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Button class="p-button-rounded p-button-text" @click="switchCamera">
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.switchCamera'),
|
||||
value: t('load3d.switchCamera'),
|
||||
showDelay: 300
|
||||
}"
|
||||
:class="['pi', getCameraIcon, 'text-lg text-white']"
|
||||
@@ -12,7 +12,7 @@
|
||||
<div v-if="showFOVButton" class="show-fov relative">
|
||||
<Button class="p-button-rounded p-button-text" @click="toggleFOV">
|
||||
<i
|
||||
v-tooltip.right="{ value: $t('load3d.fov'), showDelay: 300 }"
|
||||
v-tooltip.right="{ value: t('load3d.fov'), showDelay: 300 }"
|
||||
class="pi pi-expand text-lg text-white"
|
||||
/>
|
||||
</Button>
|
||||
@@ -21,37 +21,83 @@
|
||||
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg"
|
||||
style="width: 150px"
|
||||
>
|
||||
<Slider v-model="fov" class="w-full" :min="10" :max="150" :step="1" />
|
||||
<Slider
|
||||
v-model="fov"
|
||||
class="w-full"
|
||||
:min="10"
|
||||
:max="150"
|
||||
:step="1"
|
||||
@change="updateFOV"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import type { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const props = defineProps<{
|
||||
cameraType: CameraType
|
||||
fov: number
|
||||
showFOVButton: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'switchCamera'): void
|
||||
(e: 'updateFOV', value: number): void
|
||||
}>()
|
||||
|
||||
const cameraType = ref(props.cameraType)
|
||||
const fov = ref(props.fov)
|
||||
const showFOVButton = ref(props.showFOVButton)
|
||||
const showFOV = ref(false)
|
||||
|
||||
const cameraType = defineModel<CameraType>('cameraType')
|
||||
const fov = defineModel<number>('fov')
|
||||
const showFOVButton = computed(() => cameraType.value === 'perspective')
|
||||
const getCameraIcon = computed(() => {
|
||||
return cameraType.value === 'perspective' ? 'pi-camera' : 'pi-camera'
|
||||
})
|
||||
watch(
|
||||
() => props.fov,
|
||||
(newValue) => {
|
||||
fov.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showFOVButton,
|
||||
(newValue) => {
|
||||
showFOVButton.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.cameraType,
|
||||
(newValue) => {
|
||||
cameraType.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
const switchCamera = () => {
|
||||
emit('switchCamera')
|
||||
}
|
||||
|
||||
const toggleFOV = () => {
|
||||
showFOV.value = !showFOV.value
|
||||
}
|
||||
|
||||
const switchCamera = () => {
|
||||
cameraType.value =
|
||||
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
|
||||
const updateFOV = () => {
|
||||
emit('updateFOV', fov.value)
|
||||
}
|
||||
|
||||
const getCameraIcon = computed(() => {
|
||||
return props.cameraType === 'perspective' ? 'pi-camera' : 'pi-camera'
|
||||
})
|
||||
|
||||
const closeCameraSlider = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.exportModel'),
|
||||
value: t('load3d.exportModel'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-download text-lg text-white"
|
||||
@@ -33,9 +33,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'exportModel', format: string): void
|
||||
}>()
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.lightIntensity'),
|
||||
value: t('load3d.lightIntensity'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-sun text-lg text-white"
|
||||
@@ -24,6 +24,7 @@
|
||||
:min="lightIntensityMinimum"
|
||||
:max="lightIntensityMaximum"
|
||||
:step="lightAdjustmentIncrement"
|
||||
@change="updateLightIntensity"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,19 +32,27 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import type { MaterialMode } from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const lightIntensity = defineModel<number>('lightIntensity')
|
||||
const materialMode = defineModel<MaterialMode>('materialMode')
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const showLightIntensityButton = computed(
|
||||
() => materialMode.value === 'original'
|
||||
)
|
||||
const props = defineProps<{
|
||||
lightIntensity: number
|
||||
showLightIntensityButton: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateLightIntensity', value: number): void
|
||||
}>()
|
||||
|
||||
const lightIntensity = ref(props.lightIntensity)
|
||||
const showLightIntensityButton = ref(props.showLightIntensityButton)
|
||||
const showLightIntensity = ref(false)
|
||||
|
||||
const lightIntensityMaximum = useSettingStore().get(
|
||||
@@ -56,10 +65,28 @@ const lightAdjustmentIncrement = useSettingStore().get(
|
||||
'Comfy.Load3D.LightAdjustmentIncrement'
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.lightIntensity,
|
||||
(newValue) => {
|
||||
lightIntensity.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showLightIntensityButton,
|
||||
(newValue) => {
|
||||
showLightIntensityButton.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
const toggleLightIntensity = () => {
|
||||
showLightIntensity.value = !showLightIntensity.value
|
||||
}
|
||||
|
||||
const updateLightIntensity = () => {
|
||||
emit('updateLightIntensity', lightIntensity.value)
|
||||
}
|
||||
|
||||
const closeLightSlider = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
:class="{ 'bg-blue-500': upDirection === direction }"
|
||||
@click="selectUpDirection(direction)"
|
||||
>
|
||||
{{ direction.toUpperCase() }}
|
||||
{{ formatOption(direction) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,7 +49,7 @@
|
||||
<Button
|
||||
v-for="mode in materialModes"
|
||||
:key="mode"
|
||||
class="p-button-text whitespace-nowrap text-white"
|
||||
class="p-button-text text-white"
|
||||
:class="{ 'bg-blue-500': materialMode === mode }"
|
||||
@click="selectMaterialMode(mode)"
|
||||
>
|
||||
@@ -58,24 +58,75 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="materialMode === 'lineart'" class="show-edge-threshold relative">
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
@click="toggleEdgeThreshold"
|
||||
>
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: t('load3d.edgeThreshold'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-sliders-h text-lg text-white"
|
||||
/>
|
||||
</Button>
|
||||
<div
|
||||
v-show="showEdgeThreshold"
|
||||
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg"
|
||||
style="width: 150px"
|
||||
>
|
||||
<label class="mb-1 block text-xs text-white"
|
||||
>{{ t('load3d.edgeThreshold') }}: {{ edgeThreshold }}°</label
|
||||
>
|
||||
<Slider
|
||||
v-model="edgeThreshold"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:max="120"
|
||||
:step="1"
|
||||
@change="updateEdgeThreshold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import type {
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
const materialMode = defineModel<MaterialMode>('materialMode')
|
||||
const upDirection = defineModel<UpDirection>('upDirection')
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const props = defineProps<{
|
||||
inputSpec: CustomInputSpec
|
||||
upDirection: UpDirection
|
||||
materialMode: MaterialMode
|
||||
edgeThreshold?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateUpDirection', direction: UpDirection): void
|
||||
(e: 'updateMaterialMode', mode: MaterialMode): void
|
||||
(e: 'updateEdgeThreshold', value: number): void
|
||||
}>()
|
||||
|
||||
const upDirection = ref(props.upDirection || 'original')
|
||||
const materialMode = ref(props.materialMode || 'original')
|
||||
const edgeThreshold = ref(props.edgeThreshold || 85)
|
||||
const showUpDirection = ref(false)
|
||||
const showMaterialMode = ref(false)
|
||||
const showEdgeThreshold = ref(false)
|
||||
|
||||
const upDirections: UpDirection[] = [
|
||||
'original',
|
||||
@@ -95,26 +146,65 @@ const materialModes = computed(() => {
|
||||
//'depth' disable for now
|
||||
]
|
||||
|
||||
if (!props.inputSpec.isAnimation && !props.inputSpec.isPreview) {
|
||||
modes.push('lineart')
|
||||
}
|
||||
|
||||
return modes
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.upDirection,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
upDirection.value = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.materialMode,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
materialMode.value = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.edgeThreshold,
|
||||
(newValue) => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
edgeThreshold.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
const toggleUpDirection = () => {
|
||||
showUpDirection.value = !showUpDirection.value
|
||||
showMaterialMode.value = false
|
||||
showEdgeThreshold.value = false
|
||||
}
|
||||
|
||||
const selectUpDirection = (direction: UpDirection) => {
|
||||
upDirection.value = direction
|
||||
emit('updateUpDirection', direction)
|
||||
showUpDirection.value = false
|
||||
}
|
||||
|
||||
const formatOption = (option: string) => {
|
||||
if (option === 'original') return 'Original'
|
||||
return option.toUpperCase()
|
||||
}
|
||||
|
||||
const toggleMaterialMode = () => {
|
||||
showMaterialMode.value = !showMaterialMode.value
|
||||
showUpDirection.value = false
|
||||
showEdgeThreshold.value = false
|
||||
}
|
||||
|
||||
const selectMaterialMode = (mode: MaterialMode) => {
|
||||
materialMode.value = mode
|
||||
emit('updateMaterialMode', mode)
|
||||
showMaterialMode.value = false
|
||||
}
|
||||
|
||||
@@ -122,6 +212,16 @@ const formatMaterialMode = (mode: MaterialMode) => {
|
||||
return t(`load3d.materialModes.${mode}`)
|
||||
}
|
||||
|
||||
const toggleEdgeThreshold = () => {
|
||||
showEdgeThreshold.value = !showEdgeThreshold.value
|
||||
showUpDirection.value = false
|
||||
showMaterialMode.value = false
|
||||
}
|
||||
|
||||
const updateEdgeThreshold = () => {
|
||||
emit('updateEdgeThreshold', edgeThreshold.value)
|
||||
}
|
||||
|
||||
const closeSceneSlider = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
@@ -132,6 +232,10 @@ const closeSceneSlider = (e: MouseEvent) => {
|
||||
if (!target.closest('.show-material-mode')) {
|
||||
showMaterialMode.value = false
|
||||
}
|
||||
|
||||
if (!target.closest('.show-edge-threshold')) {
|
||||
showEdgeThreshold.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
<template>
|
||||
<div class="relative rounded-lg bg-smoke-700/30">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
@click="resizeNodeMatchOutput"
|
||||
>
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: t('load3d.resizeNodeMatchOutput'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-window-maximize text-lg text-white"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
:class="{
|
||||
@@ -12,8 +24,8 @@
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: isRecording
|
||||
? $t('load3d.stopRecording')
|
||||
: $t('load3d.startRecording'),
|
||||
? t('load3d.stopRecording')
|
||||
: t('load3d.startRecording'),
|
||||
showDelay: 300
|
||||
}"
|
||||
:class="[
|
||||
@@ -27,11 +39,11 @@
|
||||
<Button
|
||||
v-if="hasRecording && !isRecording"
|
||||
class="p-button-rounded p-button-text"
|
||||
@click="handleExportRecording"
|
||||
@click="exportRecording"
|
||||
>
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.exportRecording'),
|
||||
value: t('load3d.exportRecording'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-download text-lg text-white"
|
||||
@@ -41,11 +53,11 @@
|
||||
<Button
|
||||
v-if="hasRecording && !isRecording"
|
||||
class="p-button-rounded p-button-text"
|
||||
@click="handleClearRecording"
|
||||
@click="clearRecording"
|
||||
>
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.clearRecording'),
|
||||
value: t('load3d.clearRecording'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-trash text-lg text-white"
|
||||
@@ -53,7 +65,7 @@
|
||||
</Button>
|
||||
|
||||
<div
|
||||
v-if="recordingDuration && recordingDuration > 0 && !isRecording"
|
||||
v-if="recordingDuration > 0 && !isRecording"
|
||||
class="mt-1 text-center text-xs text-white"
|
||||
>
|
||||
{{ formatDuration(recordingDuration) }}
|
||||
@@ -63,11 +75,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const hasRecording = defineModel<boolean>('hasRecording')
|
||||
const isRecording = defineModel<boolean>('isRecording')
|
||||
const recordingDuration = defineModel<number>('recordingDuration')
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const { hasRecording, isRecording, node, recordingDuration } = defineProps<{
|
||||
hasRecording: boolean
|
||||
isRecording: boolean
|
||||
node: LGraphNode
|
||||
recordingDuration: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'startRecording'): void
|
||||
@@ -76,19 +98,49 @@ const emit = defineEmits<{
|
||||
(e: 'clearRecording'): void
|
||||
}>()
|
||||
|
||||
const resizeNodeMatchOutput = () => {
|
||||
const outputWidth = node.widgets?.find((w) => w.name === 'width')
|
||||
const outputHeight = node.widgets?.find((w) => w.name === 'height')
|
||||
|
||||
if (outputWidth && outputHeight && outputHeight.value && outputWidth.value) {
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
|
||||
const scene = node.widgets?.find((w) => w.name === 'image')
|
||||
|
||||
const sceneHeight = scene?.computedHeight
|
||||
|
||||
if (sceneHeight) {
|
||||
const sceneWidth = oldWidth - 20
|
||||
|
||||
const outputRatio = Number(outputHeight.value) / Number(outputWidth.value)
|
||||
const expectSceneHeight = sceneWidth * outputRatio
|
||||
|
||||
node.setSize([oldWidth, oldHeight + (expectSceneHeight - sceneHeight)])
|
||||
|
||||
node.graph?.setDirtyCanvas(true, true)
|
||||
|
||||
const load3d = useLoad3dService().getLoad3d(node as LGraphNode)
|
||||
|
||||
if (load3d) {
|
||||
load3d.refreshViewport()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleRecording = () => {
|
||||
if (isRecording.value) {
|
||||
if (isRecording) {
|
||||
emit('stopRecording')
|
||||
} else {
|
||||
emit('startRecording')
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportRecording = () => {
|
||||
const exportRecording = () => {
|
||||
emit('exportRecording')
|
||||
}
|
||||
|
||||
const handleClearRecording = () => {
|
||||
const clearRecording = () => {
|
||||
emit('clearRecording')
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@click="toggleGrid"
|
||||
>
|
||||
<i
|
||||
v-tooltip.right="{ value: $t('load3d.showGrid'), showDelay: 300 }"
|
||||
v-tooltip.right="{ value: t('load3d.showGrid'), showDelay: 300 }"
|
||||
class="pi pi-table text-lg text-white"
|
||||
/>
|
||||
</Button>
|
||||
@@ -15,7 +15,7 @@
|
||||
<Button class="p-button-rounded p-button-text" @click="openColorPicker">
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.backgroundColor'),
|
||||
value: t('load3d.backgroundColor'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-palette text-lg text-white"
|
||||
@@ -36,7 +36,7 @@
|
||||
<Button class="p-button-rounded p-button-text" @click="openImagePicker">
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.uploadBackgroundImage'),
|
||||
value: t('load3d.uploadBackgroundImage'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-image text-lg text-white"
|
||||
@@ -58,7 +58,7 @@
|
||||
>
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.removeBackgroundImage'),
|
||||
value: t('load3d.removeBackgroundImage'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-times text-lg text-white"
|
||||
@@ -69,29 +69,60 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const props = defineProps<{
|
||||
backgroundColor: string
|
||||
showGrid: boolean
|
||||
hasBackgroundImage?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggleGrid', value: boolean): void
|
||||
(e: 'updateBackgroundColor', color: string): void
|
||||
(e: 'updateBackgroundImage', file: File | null): void
|
||||
}>()
|
||||
|
||||
const showGrid = defineModel<boolean>('showGrid')
|
||||
const backgroundColor = defineModel<string>('backgroundColor')
|
||||
const backgroundImage = defineModel<string>('backgroundImage')
|
||||
const hasBackgroundImage = computed(
|
||||
() => backgroundImage.value && backgroundImage.value !== ''
|
||||
)
|
||||
|
||||
const backgroundColor = ref(props.backgroundColor)
|
||||
const showGrid = ref(props.showGrid)
|
||||
const hasBackgroundImage = ref(props.hasBackgroundImage)
|
||||
const colorPickerRef = ref<HTMLInputElement | null>(null)
|
||||
const imagePickerRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
watch(
|
||||
() => props.backgroundColor,
|
||||
(newValue) => {
|
||||
backgroundColor.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.showGrid,
|
||||
(newValue) => {
|
||||
showGrid.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.hasBackgroundImage,
|
||||
(newValue) => {
|
||||
hasBackgroundImage.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
const toggleGrid = () => {
|
||||
showGrid.value = !showGrid.value
|
||||
emit('toggleGrid', showGrid.value)
|
||||
}
|
||||
|
||||
const updateBackgroundColor = (color: string) => {
|
||||
backgroundColor.value = color
|
||||
emit('updateBackgroundColor', color)
|
||||
}
|
||||
|
||||
const openColorPicker = () => {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
@@ -23,6 +24,8 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const { node } = defineProps<{
|
||||
node: LGraphNode
|
||||
}>()
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</Select>
|
||||
|
||||
<Button severity="secondary" text rounded @click="exportModel(exportFormat)">
|
||||
{{ $t('load3d.export') }}
|
||||
{{ t('load3d.export') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,8 @@ import Button from 'primevue/button'
|
||||
import Select from 'primevue/select'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'exportModel', format: string): void
|
||||
}>()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<label>{{ $t('load3d.lightIntensity') }}</label>
|
||||
<label>{{ t('load3d.lightIntensity') }}</label>
|
||||
|
||||
<Slider
|
||||
v-model="lightIntensity"
|
||||
@@ -13,6 +13,7 @@
|
||||
<script setup lang="ts">
|
||||
import Slider from 'primevue/slider'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const lightIntensity = defineModel<number>('lightIntensity')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label>{{ $t('load3d.upDirection') }}</label>
|
||||
<label>{{ t('load3d.upDirection') }}</label>
|
||||
<Select
|
||||
v-model="upDirection"
|
||||
:options="upDirectionOptions"
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>{{ $t('load3d.materialMode') }}</label>
|
||||
<label>{{ t('load3d.materialMode') }}</label>
|
||||
<Select
|
||||
v-model="materialMode"
|
||||
:options="materialModeOptions"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="space-y-4">
|
||||
<div v-if="!hasBackgroundImage">
|
||||
<label>
|
||||
{{ $t('load3d.backgroundColor') }}
|
||||
{{ t('load3d.backgroundColor') }}
|
||||
</label>
|
||||
<input v-model="backgroundColor" type="color" class="w-full" />
|
||||
</div>
|
||||
@@ -10,14 +10,14 @@
|
||||
<div>
|
||||
<Checkbox v-model="showGrid" input-id="showGrid" binary name="showGrid" />
|
||||
<label for="showGrid" class="pl-2">
|
||||
{{ $t('load3d.showGrid') }}
|
||||
{{ t('load3d.showGrid') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasBackgroundImage">
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="$t('load3d.uploadBackgroundImage')"
|
||||
:label="t('load3d.uploadBackgroundImage')"
|
||||
icon="pi pi-image"
|
||||
class="w-full"
|
||||
@click="openImagePicker"
|
||||
@@ -34,7 +34,7 @@
|
||||
<div v-if="hasBackgroundImage" class="space-y-2">
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="$t('load3d.removeBackgroundImage')"
|
||||
:label="t('load3d.removeBackgroundImage')"
|
||||
icon="pi pi-times"
|
||||
class="w-full"
|
||||
@click="removeBackgroundImage"
|
||||
@@ -48,6 +48,8 @@ import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const backgroundColor = defineModel<string>('backgroundColor')
|
||||
const showGrid = defineModel<boolean>('showGrid')
|
||||
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
<template>
|
||||
<div
|
||||
v-tooltip="{
|
||||
value: t('sideToolbar.labels.menu'),
|
||||
showDelay: 300,
|
||||
hideDelay: 300
|
||||
}"
|
||||
class="comfy-menu-button-wrapper flex shrink-0 cursor-pointer flex-col items-center justify-center p-2 transition-colors"
|
||||
class="comfy-menu-button-wrapper flex shrink-0 cursor-pointer flex-col items-center justify-center rounded-t-md p-2 transition-colors"
|
||||
:class="{
|
||||
'comfy-menu-button-active': menuRef?.visible
|
||||
}"
|
||||
@click="onLogoMenuClick($event)"
|
||||
@click="menuRef?.toggle($event)"
|
||||
>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-black">
|
||||
<ComfyLogo
|
||||
alt="ComfyUI Logo"
|
||||
class="comfyui-logo h-[18px] w-[18px] text-white"
|
||||
mode="fill"
|
||||
/>
|
||||
</div>
|
||||
<ComfyLogoTransparent
|
||||
alt="ComfyUI Logo"
|
||||
class="comfyui-logo h-[18px] w-[18px]"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-if="!isSmall"
|
||||
class="side-bar-button-label mt-1 text-center text-[10px]"
|
||||
>{{ t('sideToolbar.labels.menu') }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<TieredMenu
|
||||
@@ -77,10 +75,8 @@ import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import ComfyLogo from '@/components/icons/ComfyLogo.vue'
|
||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||
import ComfyLogoTransparent from '@/components/icons/ComfyLogoTransparent.vue'
|
||||
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
@@ -99,19 +95,14 @@ const colorPaletteService = useColorPaletteService()
|
||||
const dialogStore = useDialogStore()
|
||||
const managerState = useManagerState()
|
||||
|
||||
const { isSmall = false } = defineProps<{
|
||||
isSmall?: boolean
|
||||
}>()
|
||||
|
||||
const menuRef = ref<
|
||||
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
|
||||
>(null)
|
||||
|
||||
const telemetry = useTelemetry()
|
||||
|
||||
function onLogoMenuClick(event: MouseEvent) {
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_comfy_menu_opened'
|
||||
})
|
||||
menuRef.value?.toggle(event)
|
||||
}
|
||||
|
||||
const translateMenuItem = (item: MenuItem): MenuItem => {
|
||||
const label = typeof item.label === 'function' ? item.label() : item.label
|
||||
const translatedLabel = label
|
||||
@@ -169,18 +160,13 @@ const extraMenuItems = computed(() => [
|
||||
key: 'browse-templates',
|
||||
label: t('menuLabels.Browse Templates'),
|
||||
icon: 'icon-[comfy--template]',
|
||||
command: () => useWorkflowTemplateSelectorDialog().show('menu')
|
||||
command: () => commandStore.execute('Comfy.BrowseTemplates')
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: t('g.settings'),
|
||||
icon: 'mdi mdi-cog-outline',
|
||||
command: () => {
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_settings_menu_opened'
|
||||
})
|
||||
showSettings()
|
||||
}
|
||||
command: () => showSettings()
|
||||
},
|
||||
{
|
||||
key: 'manage-extensions',
|
||||
@@ -290,12 +276,12 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
}
|
||||
|
||||
.comfy-menu-button-wrapper:hover {
|
||||
background: var(--interface-panel-hover-surface);
|
||||
background: var(--p-button-text-secondary-hover-background);
|
||||
}
|
||||
|
||||
.comfy-menu-button-active,
|
||||
.comfy-menu-button-active:hover {
|
||||
background: var(--interface-panel-selected-surface);
|
||||
background-color: var(--content-hover-bg);
|
||||
}
|
||||
|
||||
.keybinding-tag {
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
'small-sidebar': isSmall,
|
||||
'connected-sidebar': isConnected,
|
||||
'floating-sidebar': !isConnected,
|
||||
'overflowing-sidebar': isOverflowing,
|
||||
'border-r border-[var(--interface-stroke)] shadow-interface': isConnected
|
||||
'overflowing-sidebar': isOverflowing
|
||||
}"
|
||||
>
|
||||
<div
|
||||
@@ -19,7 +18,7 @@
|
||||
"
|
||||
>
|
||||
<div ref="topToolbarRef" :class="groupClasses">
|
||||
<ComfyMenuButton />
|
||||
<ComfyMenuButton :is-small="isSmall" />
|
||||
<SidebarIcon
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@@ -58,7 +57,6 @@ import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
|
||||
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
|
||||
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
@@ -99,40 +97,10 @@ const isConnected = computed(
|
||||
const tabs = computed(() => workspaceStore.getSidebarTabs())
|
||||
const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
|
||||
|
||||
/**
|
||||
* Handle sidebar tab icon click.
|
||||
* - Emits UI button telemetry for known tabs
|
||||
* - Delegates to the corresponding toggle command
|
||||
*/
|
||||
const onTabClick = async (item: SidebarTabExtension) => {
|
||||
const telemetry = useTelemetry()
|
||||
|
||||
const isNodeLibraryTab = item.id === 'node-library'
|
||||
const isModelLibraryTab = item.id === 'model-library'
|
||||
const isWorkflowsTab = item.id === 'workflows'
|
||||
const isAssetsTab = item.id === 'assets'
|
||||
|
||||
if (isNodeLibraryTab)
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_tab_node_library_selected'
|
||||
})
|
||||
else if (isModelLibraryTab)
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_tab_model_library_selected'
|
||||
})
|
||||
else if (isWorkflowsTab)
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_tab_workflows_selected'
|
||||
})
|
||||
else if (isAssetsTab)
|
||||
telemetry?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_tab_assets_media_selected'
|
||||
})
|
||||
|
||||
const onTabClick = async (item: SidebarTabExtension) =>
|
||||
await commandStore.commands
|
||||
.find((cmd) => cmd.id === `Workspace.ToggleSidebarTab.${item.id}`)
|
||||
?.function?.()
|
||||
}
|
||||
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
@@ -146,7 +114,7 @@ const isOverflowing = ref(false)
|
||||
const groupClasses = computed(() =>
|
||||
cn(
|
||||
'sidebar-item-group pointer-events-auto flex flex-col items-center overflow-hidden flex-shrink-0' +
|
||||
(isConnected.value ? '' : ' rounded-lg shadow-interface')
|
||||
(isConnected.value ? '' : ' rounded-lg shadow-md')
|
||||
)
|
||||
)
|
||||
|
||||
@@ -215,7 +183,7 @@ onMounted(() => {
|
||||
--sidebar-padding: 4px;
|
||||
--sidebar-icon-size: 1rem;
|
||||
|
||||
--sidebar-default-floating-width: 48px;
|
||||
--sidebar-default-floating-width: 56px;
|
||||
--sidebar-default-connected-width: calc(
|
||||
var(--sidebar-default-floating-width) + var(--sidebar-padding) * 2
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
:label="$t('sideToolbar.labels.console')"
|
||||
:tooltip="$t('menu.toggleBottomPanel')"
|
||||
:selected="bottomPanelStore.activePanel == 'terminal'"
|
||||
@click="toggleConsole"
|
||||
@click="bottomPanelStore.toggleBottomPanel"
|
||||
>
|
||||
<template #icon>
|
||||
<i-ph:terminal-bold />
|
||||
@@ -12,20 +12,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
|
||||
/**
|
||||
* Toggle console bottom panel and track UI button click.
|
||||
*/
|
||||
const toggleConsole = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_bottom_panel_console_toggled'
|
||||
})
|
||||
bottomPanelStore.toggleBottomPanel()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -65,7 +65,6 @@ import { computed, onMounted, toRefs } from 'vue'
|
||||
|
||||
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||
import ReleaseNotificationToast from '@/platform/updates/components/ReleaseNotificationToast.vue'
|
||||
import WhatsNewPopup from '@/platform/updates/components/WhatsNewPopup.vue'
|
||||
@@ -105,13 +104,7 @@ const sidebarLocation = computed(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
)
|
||||
|
||||
/**
|
||||
* Toggle Help Center and track UI button click.
|
||||
*/
|
||||
const toggleHelpCenter = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_help_center_toggled'
|
||||
})
|
||||
helpCenterStore.toggle()
|
||||
}
|
||||
|
||||
|
||||
@@ -86,11 +86,7 @@ const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
|
||||
}
|
||||
|
||||
.side-bar-button-selected {
|
||||
background-color: var(--interface-panel-selected-surface);
|
||||
color: var(--content-hover-fg);
|
||||
}
|
||||
.side-bar-button:hover {
|
||||
background-color: var(--interface-panel-hover-surface);
|
||||
background-color: var(--content-hover-bg);
|
||||
color: var(--content-hover-fg);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
|
||||
@@ -35,13 +34,7 @@ const tooltipText = computed(
|
||||
() => `${t('shortcuts.keyboardShortcuts')} (${formatKeySequence(command)})`
|
||||
)
|
||||
|
||||
/**
|
||||
* Toggle keyboard shortcuts panel and track UI button click.
|
||||
*/
|
||||
const toggleShortcutsPanel = () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'sidebar_shortcuts_panel_toggled'
|
||||
})
|
||||
bottomPanelStore.togglePanel('shortcuts')
|
||||
}
|
||||
</script>
|
||||
|
||||