mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Compare commits
11 Commits
v1.26.5
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec282d4eab | ||
|
|
ae7f6b8143 | ||
|
|
90c09db919 | ||
|
|
8d0a523ffd | ||
|
|
97e5291b05 | ||
|
|
80097007c9 | ||
|
|
deba8df8ce | ||
|
|
694817297d | ||
|
|
28d74be363 | ||
|
|
2240645a7d | ||
|
|
cf9847a11e |
96
.github/ISSUE_TEMPLATE/storybook-improvement.yaml
vendored
Normal file
96
.github/ISSUE_TEMPLATE/storybook-improvement.yaml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: Storybook Improvement
|
||||
description: 'Report Storybook-related improvements, new stories, or configuration issues'
|
||||
title: '[Storybook]: '
|
||||
labels: ['area:storybook', 'enhancement']
|
||||
type: Storybook
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label: I have checked the existing Storybook documentation in `.storybook/`
|
||||
required: true
|
||||
- label: I have searched existing issues to make sure this isn't a duplicate
|
||||
required: true
|
||||
- label: I have checked if this relates to any existing Storybook PRs
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: Category
|
||||
description: What type of Storybook improvement is this?
|
||||
options:
|
||||
- Component Stories - Add new stories for existing components
|
||||
- Story Enhancement - Improve existing stories
|
||||
- Configuration - Storybook configuration improvements
|
||||
- Visual Testing - Chromatic/visual regression testing
|
||||
- Documentation - Storybook documentation improvements
|
||||
- Build/Performance - Build optimizations or performance improvements
|
||||
- Theme/Styling - Theme support or styling improvements
|
||||
- Tools/Addons - Storybook addons or tool integration
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What improvement are you suggesting?
|
||||
description: Describe the Storybook improvement you'd like to see
|
||||
placeholder: |
|
||||
Example: "Add comprehensive stories for the NodeWidget component covering all widget types (text, number, combo, etc.) to improve component development and testing."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Use Case & Context
|
||||
description: Why is this improvement needed? What problem does it solve?
|
||||
placeholder: |
|
||||
- Current state of the component/story
|
||||
- What's missing or could be improved
|
||||
- How this would benefit development workflow
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: Priority
|
||||
description: How important is this improvement?
|
||||
options:
|
||||
- Low - Nice to have enhancement
|
||||
- Medium - Would improve development workflow
|
||||
- High - Important for component quality/testing
|
||||
- Critical - Needed for proper Storybook functionality
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: components
|
||||
attributes:
|
||||
label: Components Affected
|
||||
description: Which components or areas would be affected by this improvement?
|
||||
placeholder: |
|
||||
- NodeWidget
|
||||
- Settings components
|
||||
- All button components
|
||||
- etc.
|
||||
|
||||
- type: textarea
|
||||
id: implementation
|
||||
attributes:
|
||||
label: Implementation Ideas (Optional)
|
||||
description: Any ideas on how this could be implemented?
|
||||
placeholder: |
|
||||
- Specific stories to create
|
||||
- Configuration changes needed
|
||||
- Dependencies or tools required
|
||||
|
||||
- type: textarea
|
||||
id: examples
|
||||
attributes:
|
||||
label: Examples or References
|
||||
description: Any examples from other projects, screenshots, or links that help illustrate the improvement
|
||||
154
.github/workflows/pr-checks.yml
vendored
154
.github/workflows/pr-checks.yml
vendored
@@ -1,154 +0,0 @@
|
||||
name: PR Checks
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.check-changes.outputs.should_run }}
|
||||
has_browser_tests: ${{ steps.check-coverage.outputs.has_browser_tests }}
|
||||
has_screen_recording: ${{ steps.check-recording.outputs.has_recording }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Ensure base branch is available
|
||||
run: |
|
||||
# Fetch the specific base commit to ensure it's available for git diff
|
||||
git fetch origin ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Check if significant changes exist
|
||||
id: check-changes
|
||||
run: |
|
||||
# Get list of changed files
|
||||
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }})
|
||||
|
||||
# Filter for src/ files
|
||||
SRC_FILES=$(echo "$CHANGED_FILES" | grep '^src/' || true)
|
||||
|
||||
if [ -z "$SRC_FILES" ]; then
|
||||
echo "No src/ files changed"
|
||||
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count lines changed in src files
|
||||
TOTAL_LINES=0
|
||||
for file in $SRC_FILES; do
|
||||
if [ -f "$file" ]; then
|
||||
# Count added lines (non-empty)
|
||||
ADDED=$(git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- "$file" | grep '^+' | grep -v '^+++' | grep -v '^+$' | wc -l)
|
||||
# Count removed lines (non-empty)
|
||||
REMOVED=$(git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- "$file" | grep '^-' | grep -v '^---' | grep -v '^-$' | wc -l)
|
||||
TOTAL_LINES=$((TOTAL_LINES + ADDED + REMOVED))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Total lines changed in src/: $TOTAL_LINES"
|
||||
|
||||
if [ $TOTAL_LINES -gt 3 ]; then
|
||||
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check browser test coverage
|
||||
id: check-coverage
|
||||
if: steps.check-changes.outputs.should_run == 'true'
|
||||
run: |
|
||||
# Check if browser tests were updated
|
||||
BROWSER_TEST_CHANGES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} | grep '^browser_tests/.*\.ts$' || true)
|
||||
|
||||
if [ -n "$BROWSER_TEST_CHANGES" ]; then
|
||||
echo "has_browser_tests=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_browser_tests=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check for screen recording
|
||||
id: check-recording
|
||||
if: steps.check-changes.outputs.should_run == 'true'
|
||||
env:
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
run: |
|
||||
# Check PR body for screen recording
|
||||
# Check for GitHub user attachments or YouTube links
|
||||
if echo "$PR_BODY" | grep -qiE 'github\.com/user-attachments/assets/[a-f0-9-]+|youtube\.com/watch|youtu\.be/'; then
|
||||
echo "has_recording=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_recording=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Final check and create results
|
||||
id: final-check
|
||||
if: always()
|
||||
run: |
|
||||
# Initialize results
|
||||
WARNINGS_JSON=""
|
||||
|
||||
# Only run checks if should_run is true
|
||||
if [ "${{ steps.check-changes.outputs.should_run }}" == "true" ]; then
|
||||
# Check browser test coverage
|
||||
if [ "${{ steps.check-coverage.outputs.has_browser_tests }}" != "true" ]; then
|
||||
if [ -n "$WARNINGS_JSON" ]; then
|
||||
WARNINGS_JSON="${WARNINGS_JSON},"
|
||||
fi
|
||||
WARNINGS_JSON="${WARNINGS_JSON}{\"message\":\"⚠️ **Warning: E2E Test Coverage Missing**\\n\\nIf this PR modifies behavior that can be covered by browser-based E2E tests, those tests are required. PRs lacking applicable test coverage may not be reviewed until added. Please add or update browser tests to ensure code quality and prevent regressions.\"}"
|
||||
fi
|
||||
|
||||
# Check screen recording
|
||||
if [ "${{ steps.check-recording.outputs.has_recording }}" != "true" ]; then
|
||||
if [ -n "$WARNINGS_JSON" ]; then
|
||||
WARNINGS_JSON="${WARNINGS_JSON},"
|
||||
fi
|
||||
WARNINGS_JSON="${WARNINGS_JSON}{\"message\":\"⚠️ **Warning: Visual Documentation Missing**\\n\\nIf this PR changes user-facing behavior, visual proof (screen recording or screenshot) is required. PRs without applicable visual documentation may not be reviewed until provided.\\nYou can add it by:\\n\\n- GitHub: Drag & drop media directly into the PR description\\n\\n- YouTube: Include a link to a short demo\"}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create results JSON
|
||||
if [ -n "$WARNINGS_JSON" ]; then
|
||||
# Create JSON with warnings
|
||||
cat > pr-check-results.json << EOF
|
||||
{
|
||||
"fails": [],
|
||||
"warnings": [$WARNINGS_JSON],
|
||||
"messages": [],
|
||||
"markdowns": []
|
||||
}
|
||||
EOF
|
||||
echo "failed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create JSON with success
|
||||
cat > pr-check-results.json << 'EOF'
|
||||
{
|
||||
"fails": [],
|
||||
"warnings": [],
|
||||
"messages": [],
|
||||
"markdowns": []
|
||||
}
|
||||
EOF
|
||||
echo "failed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Write PR metadata
|
||||
echo "${{ github.event.pull_request.number }}" > pr-number.txt
|
||||
echo "${{ github.event.pull_request.head.sha }}" > pr-sha.txt
|
||||
|
||||
- name: Upload results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: pr-check-results-${{ github.run_id }}
|
||||
path: |
|
||||
pr-check-results.json
|
||||
pr-number.txt
|
||||
pr-sha.txt
|
||||
retention-days: 1
|
||||
149
.github/workflows/pr-comment.yml
vendored
149
.github/workflows/pr-comment.yml
vendored
@@ -1,149 +0,0 @@
|
||||
name: PR Comment
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["PR Checks"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pr-check-results-${{ github.event.workflow_run.id }}
|
||||
path: /tmp/pr-artifacts
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Post results
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Helper function to safely read files
|
||||
function safeReadFile(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
return fs.readFileSync(filePath, 'utf8').trim();
|
||||
} catch (e) {
|
||||
console.error(`Error reading ${filePath}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Read artifact files
|
||||
const artifactDir = '/tmp/pr-artifacts';
|
||||
const prNumber = safeReadFile(path.join(artifactDir, 'pr-number.txt'));
|
||||
const prSha = safeReadFile(path.join(artifactDir, 'pr-sha.txt'));
|
||||
const resultsJson = safeReadFile(path.join(artifactDir, 'pr-check-results.json'));
|
||||
|
||||
// Validate PR number
|
||||
if (!prNumber || isNaN(parseInt(prNumber))) {
|
||||
throw new Error('Invalid or missing PR number');
|
||||
}
|
||||
|
||||
// Parse and validate results
|
||||
let results;
|
||||
try {
|
||||
results = JSON.parse(resultsJson || '{}');
|
||||
} catch (e) {
|
||||
console.error('Failed to parse check results:', e);
|
||||
|
||||
// Post error comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(prNumber),
|
||||
body: `⚠️ PR checks failed to complete properly. Error parsing results: ${e.message}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Format check messages
|
||||
const messages = [];
|
||||
|
||||
if (results.fails && results.fails.length > 0) {
|
||||
messages.push('### ❌ Failures\n' + results.fails.map(f => f.message).join('\n\n'));
|
||||
}
|
||||
|
||||
if (results.warnings && results.warnings.length > 0) {
|
||||
messages.push('### ⚠️ Warnings\n' + results.warnings.map(w => w.message).join('\n\n'));
|
||||
}
|
||||
|
||||
if (results.messages && results.messages.length > 0) {
|
||||
messages.push('### 💬 Messages\n' + results.messages.map(m => m.message).join('\n\n'));
|
||||
}
|
||||
|
||||
if (results.markdowns && results.markdowns.length > 0) {
|
||||
messages.push(...results.markdowns.map(m => m.message));
|
||||
}
|
||||
|
||||
// Find existing bot comment
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(prNumber)
|
||||
});
|
||||
|
||||
const botComment = comments.data.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('<!-- pr-checks-comment -->')
|
||||
);
|
||||
|
||||
// Post comment if there are any messages
|
||||
if (messages.length > 0) {
|
||||
const body = messages.join('\n\n');
|
||||
const commentBody = `<!-- pr-checks-comment -->\n${body}`;
|
||||
|
||||
if (botComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(prNumber),
|
||||
body: commentBody
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No messages - delete existing comment if present
|
||||
if (botComment) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Set commit status based on failures
|
||||
if (prSha) {
|
||||
const hasFailures = results.fails && results.fails.length > 0;
|
||||
const hasWarnings = results.warnings && results.warnings.length > 0;
|
||||
await github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: prSha,
|
||||
state: hasFailures ? 'failure' : 'success',
|
||||
context: 'pr-checks',
|
||||
description: hasFailures
|
||||
? `${results.fails.length} check(s) failed`
|
||||
: hasWarnings
|
||||
? `${results.warnings.length} warning(s)`
|
||||
: 'All checks passed'
|
||||
});
|
||||
}
|
||||
225
.github/workflows/test-ui.yaml
vendored
225
.github/workflows/test-ui.yaml
vendored
@@ -4,13 +4,18 @@ on:
|
||||
push:
|
||||
branches: [main, master, core/*, desktop/*]
|
||||
pull_request:
|
||||
branches-ignore: [wip/*, draft/*, temp/*, vue-nodes-migration]
|
||||
branches-ignore:
|
||||
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
|
||||
|
||||
env:
|
||||
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
sanitized-branch: ${{ steps.branch-info.outputs.sanitized }}
|
||||
steps:
|
||||
- name: Checkout ComfyUI
|
||||
uses: actions/checkout@v4
|
||||
@@ -36,6 +41,29 @@ jobs:
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Get current time
|
||||
id: current-time
|
||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Comment PR - Tests Started
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: append
|
||||
body: |
|
||||
<!-- PLAYWRIGHT_TEST_STATUS -->
|
||||
|
||||
---
|
||||
|
||||
<img alt='claude-loading-gif' src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
|
||||
<bold>[${{ steps.current-time.outputs.time }} UTC] Preparing browser tests across multiple browsers...</bold>
|
||||
|
||||
---
|
||||
*This comment will be updated when tests complete*
|
||||
|
||||
- name: Build ComfyUI_frontend
|
||||
run: |
|
||||
npm ci
|
||||
@@ -46,6 +74,14 @@ jobs:
|
||||
id: cache-key
|
||||
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate sanitized branch name
|
||||
id: branch-info
|
||||
run: |
|
||||
# Get branch name and sanitize it for Cloudflare branch names
|
||||
BRANCH_NAME="${{ github.head_ref || github.ref_name }}"
|
||||
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
|
||||
echo "sanitized=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Save cache
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
with:
|
||||
@@ -57,6 +93,10 @@ jobs:
|
||||
playwright-tests:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -78,6 +118,32 @@ jobs:
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Get current time
|
||||
id: current-time
|
||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set project name
|
||||
id: project-name
|
||||
run: |
|
||||
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
|
||||
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "branch=${{ needs.setup.outputs.sanitized-branch }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Comment PR - Browser Test Started
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: append
|
||||
body: |
|
||||
<img alt='claude-loading-gif' src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
|
||||
<bold>${{ matrix.browser }}</bold>: Running tests...
|
||||
|
||||
- name: Install requirements
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -97,12 +163,165 @@ jobs:
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Run Playwright tests (${{ matrix.browser }})
|
||||
run: npx playwright test --project=${{ matrix.browser }}
|
||||
id: playwright
|
||||
run: npx playwright test --project=${{ matrix.browser }} --reporter=html
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
if: always() # note: use always() to allow results to be upload/report even tests failed.
|
||||
with:
|
||||
name: playwright-report-${{ matrix.browser }}
|
||||
path: ComfyUI_frontend/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
|
||||
id: cloudflare-deploy
|
||||
if: always()
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy ComfyUI_frontend/playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}
|
||||
|
||||
- name: Save deployment info for summary
|
||||
if: always()
|
||||
run: |
|
||||
mkdir -p deployment-info
|
||||
# Use step conclusion to determine test result
|
||||
if [ "${{ steps.playwright.conclusion }}" = "success" ]; then
|
||||
EXIT_CODE="0"
|
||||
else
|
||||
EXIT_CODE="1"
|
||||
fi
|
||||
DEPLOYMENT_URL="${{ steps.cloudflare-deploy.outputs.deployment-url || steps.cloudflare-deploy.outputs.url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }}"
|
||||
echo "${{ matrix.browser }}|${EXIT_CODE}|${DEPLOYMENT_URL}" > deployment-info/${{ matrix.browser }}.txt
|
||||
|
||||
- name: Upload deployment info
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deployment-info-${{ matrix.browser }}
|
||||
path: deployment-info/
|
||||
retention-days: 1
|
||||
|
||||
- name: Get completion time
|
||||
id: completion-time
|
||||
if: always()
|
||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Comment PR - Browser Test Complete
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: append
|
||||
body: |
|
||||
${{ steps.playwright.conclusion == 'success' && '✅' || '❌' }} **${{ matrix.browser }}**: ${{ steps.playwright.conclusion == 'success' && 'Tests passed!' || 'Tests failed!' }} [View Report](${{ steps.cloudflare-deploy.outputs.deployment-url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }})
|
||||
|
||||
comment-summary:
|
||||
needs: playwright-tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Download all deployment info
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: deployment-info-*
|
||||
merge-multiple: true
|
||||
path: deployment-info
|
||||
|
||||
- name: Get completion time
|
||||
id: completion-time
|
||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate comment body
|
||||
id: comment-body
|
||||
run: |
|
||||
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
|
||||
echo "## 🎭 Playwright Test Results" >> comment.md
|
||||
echo "" >> comment.md
|
||||
|
||||
# Check if all tests passed
|
||||
ALL_PASSED=true
|
||||
for file in deployment-info/*.txt; do
|
||||
if [ -f "$file" ]; then
|
||||
browser=$(basename "$file" .txt)
|
||||
info=$(cat "$file")
|
||||
exit_code=$(echo "$info" | cut -d'|' -f2)
|
||||
if [ "$exit_code" != "0" ]; then
|
||||
ALL_PASSED=false
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ALL_PASSED" = "true" ]; then
|
||||
echo "✅ **All tests passed across all browsers!**" >> comment.md
|
||||
else
|
||||
echo "❌ **Some tests failed!**" >> comment.md
|
||||
fi
|
||||
|
||||
echo "" >> comment.md
|
||||
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "### 📊 Test Reports by Browser" >> comment.md
|
||||
|
||||
for file in deployment-info/*.txt; do
|
||||
if [ -f "$file" ]; then
|
||||
browser=$(basename "$file" .txt)
|
||||
info=$(cat "$file")
|
||||
exit_code=$(echo "$info" | cut -d'|' -f2)
|
||||
url=$(echo "$info" | cut -d'|' -f3)
|
||||
|
||||
if [ "$exit_code" = "0" ]; then
|
||||
status="✅"
|
||||
else
|
||||
status="❌"
|
||||
fi
|
||||
|
||||
echo "- $status **$browser**: [View Report]($url)" >> comment.md
|
||||
fi
|
||||
done
|
||||
|
||||
echo "" >> comment.md
|
||||
echo "---" >> comment.md
|
||||
if [ "$ALL_PASSED" = "true" ]; then
|
||||
echo "🎉 Your tests are passing across all browsers!" >> comment.md
|
||||
else
|
||||
echo "⚠️ Please check the test reports for details on failures." >> comment.md
|
||||
fi
|
||||
|
||||
- name: Comment PR - Tests Complete
|
||||
uses: edumserrano/find-create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: replace
|
||||
body-path: comment.md
|
||||
|
||||
- name: Check test results and fail if needed
|
||||
run: |
|
||||
# Check if all tests passed and fail the job if not
|
||||
ALL_PASSED=true
|
||||
for file in deployment-info/*.txt; do
|
||||
if [ -f "$file" ]; then
|
||||
info=$(cat "$file")
|
||||
exit_code=$(echo "$info" | cut -d'|' -f2)
|
||||
if [ "$exit_code" != "0" ]; then
|
||||
ALL_PASSED=false
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ALL_PASSED" = "false" ]; then
|
||||
echo "❌ Tests failed in one or more browsers. Failing the CI job."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All tests passed across all browsers!"
|
||||
fi
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,8 +13,9 @@ bun.lockb
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# ESLint cache
|
||||
# Cache files
|
||||
.eslintcache
|
||||
.prettiercache
|
||||
|
||||
node_modules
|
||||
dist
|
||||
@@ -22,6 +23,7 @@ dist-ssr
|
||||
*.local
|
||||
# Claude configuration
|
||||
.claude/*.local.json
|
||||
.claude/settings.json
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
src/types/comfyRegistryTypes.ts
|
||||
src/types/generatedManagerTypes.ts
|
||||
62
STORYBOOK_ISSUE_SYSTEM.md
Normal file
62
STORYBOOK_ISSUE_SYSTEM.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Storybook Issue Tracking System
|
||||
|
||||
This directory contains resources for tracking and organizing all Storybook-related work in the ComfyUI Frontend repository.
|
||||
|
||||
## 📋 Components
|
||||
|
||||
### 1. Issue Template (`.github/ISSUE_TEMPLATE/storybook-improvement.yaml`)
|
||||
A structured GitHub issue template specifically for Storybook-related improvements and requests.
|
||||
|
||||
**Features:**
|
||||
- Categorizes improvements (Component Stories, Configuration, Visual Testing, etc.)
|
||||
- Priority levels (Low → Critical)
|
||||
- Component impact tracking
|
||||
- Implementation ideas and examples
|
||||
|
||||
**Usage:** When creating new issues related to Storybook, use this template to ensure consistent formatting and complete information.
|
||||
|
||||
### 2. Tracking Documentation (`STORYBOOK_TRACKING_ISSUE.md`)
|
||||
Comprehensive documentation listing all 27+ Storybook-related PRs, organized by category.
|
||||
|
||||
**Contains:**
|
||||
- Current status overview
|
||||
- PRs organized by category (Setup, Stories, Themes, Config, etc.)
|
||||
- Upcoming priorities roadmap
|
||||
- Contribution guidelines
|
||||
- Resource links
|
||||
|
||||
## 🔧 How to Use
|
||||
|
||||
### For New Storybook Issues
|
||||
1. Go to [GitHub Issues → New Issue](https://github.com/Comfy-Org/ComfyUI_frontend/issues/new/choose)
|
||||
2. Select "Storybook Improvement" template
|
||||
3. Fill out the structured form
|
||||
4. Add `area:storybook` label if not automatically applied
|
||||
|
||||
### For Tracking Progress
|
||||
1. Reference the tracking documentation in `STORYBOOK_TRACKING_ISSUE.md`
|
||||
2. Create a GitHub issue using this content as the body
|
||||
3. Use labels: `area:storybook`, `tracking`
|
||||
4. Pin the issue for easy access
|
||||
|
||||
### For Contributors
|
||||
1. Check existing tracking issue for current priorities
|
||||
2. Follow guidelines in `.storybook/README.md` and `.storybook/CLAUDE.md`
|
||||
3. Reference the tracking issue number in related PRs
|
||||
4. Update tracking documentation when completing work
|
||||
|
||||
## 📚 Related Resources
|
||||
|
||||
- **Storybook Documentation**: `.storybook/README.md`
|
||||
- **Developer Guidelines**: `.storybook/CLAUDE.md`
|
||||
- **Component Examples**: `src/components/*/\*.stories.ts`
|
||||
- **Visual Testing**: Chromatic integration in CI/CD workflows
|
||||
|
||||
## 🎯 Purpose
|
||||
|
||||
This system helps:
|
||||
- **Organize** all Storybook-related work in one place
|
||||
- **Track** progress across multiple PRs and initiatives
|
||||
- **Prioritize** improvements based on impact and urgency
|
||||
- **Facilitate** collaboration between contributors
|
||||
- **Maintain** comprehensive documentation of Storybook evolution
|
||||
121
STORYBOOK_TRACKING_ISSUE.md
Normal file
121
STORYBOOK_TRACKING_ISSUE.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 📚 Storybook Development Tracking
|
||||
|
||||
This issue serves as a central hub for tracking all Storybook-related PRs and improvements in the ComfyUI Frontend repository.
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
Storybook is a crucial part of our component development workflow, enabling:
|
||||
- Component isolation and development
|
||||
- Visual documentation and testing
|
||||
- Automated visual regression testing with Chromatic
|
||||
- Design system development and maintenance
|
||||
|
||||
## 📈 Current Status
|
||||
|
||||
**Storybook Setup**: ✅ Complete
|
||||
**Component Coverage**: 🔄 In Progress
|
||||
**Visual Testing**: ✅ Integrated with Chromatic
|
||||
**Documentation**: ✅ Comprehensive guides available
|
||||
|
||||
## 📋 Storybook PRs by Category
|
||||
|
||||
### 🏗️ Initial Setup & Infrastructure
|
||||
- **#4861** - [feat] Add Storybook setup and NodePreview story *(merged)*
|
||||
- Complete Storybook v8 setup with Vue 3 + Vite
|
||||
- Chromatic integration for visual testing
|
||||
- Comprehensive documentation and guidelines
|
||||
|
||||
### 📖 Component Stories & Documentation
|
||||
- **#4999** - [feat] 100+ more Stories for Common Components *(open)*
|
||||
- 76 story variants across 11 components
|
||||
- Covers STATIC → SIMPLE_PROPS → INTERACTIVE → COMPLEX tiers
|
||||
- **#5034** - [feat] Add Storybook configuration and settings panel stories *(open)*
|
||||
- Settings panel components with all input types
|
||||
- Responsive design and accessibility features
|
||||
- **#5098** - [feat] Add comprehensive Storybook stories for custom UI components *(open)*
|
||||
- 12 custom UI components with interactive testing
|
||||
- Button, input, and layout component stories
|
||||
- **#5122** - [docs] Add Storybook documentation *(open)*
|
||||
- Enhanced `.storybook/README.md` with comprehensive guidelines
|
||||
|
||||
### 🎨 Theme & Visual Improvements
|
||||
- **#5088** - [feat] Add dark theme support for Storybook *(merged)*
|
||||
- Dark theme toggle with persistence
|
||||
- Smooth transitions and proper styling
|
||||
|
||||
### 🔧 Configuration & Build Optimizations
|
||||
- **#5117** - [ci] Enhance CI/CD caching across all workflows *(open)*
|
||||
- Improved caching for Storybook builds
|
||||
- **#5118** - [ci] Add retry logic to wrangler page deploy step *(open)*
|
||||
- Stability improvements for Storybook deployment
|
||||
|
||||
### 🚀 Features & Enhancements
|
||||
- **#5119** - [feat] Add enhanced filter UI components *(open)*
|
||||
- SearchBox integration and improved MultiSelect
|
||||
- **#5096** - [fix] Resolve breadcrumb and workflow tabs layout conflict *(open)*
|
||||
- Layout improvements affecting Storybook stories
|
||||
- **#5113** - [fix] Reposition TaskItem info *(open)*
|
||||
- Component fixes that impact Storybook examples
|
||||
|
||||
### 🔨 Technical Improvements & Fixes
|
||||
- **#5106** - Fix/widget ordering consistency *(open)*
|
||||
- Node widget improvements affecting stories
|
||||
- **#5109** - Fix CopyToClipboard Issue *(open)*
|
||||
- Component fixes relevant to Storybook examples
|
||||
- **#5092** - Add support for high-resolution wheel events *(open)*
|
||||
- Input handling improvements
|
||||
- **#5115** - Fix: Shift+Click+Drag from outputs with Subgraph outputs *(open)*
|
||||
- Node interaction improvements
|
||||
- **#5114** - Remove duplicate semantic labeling from issue templates *(open)*
|
||||
- Issue template improvements
|
||||
- **#5102** - [fix] Invoke onRemove callback in LGraphNode.removeWidget method *(merged)*
|
||||
- Widget system improvements
|
||||
- **#5099** - Remove PR checks workflows *(merged)*
|
||||
- CI/CD cleanup
|
||||
- **#5103** - Update to latest version of workflow icon *(merged)*
|
||||
- Icon updates affecting stories
|
||||
- **#5107** - [ci] Add caching support to format and knip commands *(merged)*
|
||||
- Build optimization improvements
|
||||
- **#5108** - [refactor] Remove obsolete Kontext Edit Button *(merged)*
|
||||
- Component cleanup
|
||||
- **#5110** - [chore] Ignore ./claude/settings.json *(merged)*
|
||||
- Development environment improvements
|
||||
- **#5112** - [docs] Update browser tests README *(merged)*
|
||||
- Testing documentation improvements
|
||||
- **#4908** - Modal Component & Custom UI Components *(merged)*
|
||||
- Foundation UI components used in stories
|
||||
|
||||
## 🎯 Upcoming Priorities
|
||||
|
||||
### High Priority
|
||||
- [ ] Complete component story coverage for all major UI components
|
||||
- [ ] Implement comprehensive visual regression testing
|
||||
- [ ] Improve Storybook build performance and caching
|
||||
|
||||
### Medium Priority
|
||||
- [ ] Add interactive component documentation
|
||||
- [ ] Enhance theme switching and customization
|
||||
- [ ] Improve mobile responsiveness of stories
|
||||
|
||||
### Low Priority
|
||||
- [ ] Add more sophisticated mock data patterns
|
||||
- [ ] Implement component testing automation
|
||||
- [ ] Explore advanced Storybook addons
|
||||
|
||||
## 🔄 How to Contribute
|
||||
|
||||
1. **Creating New Stories**: Follow guidelines in `.storybook/README.md` and `.storybook/CLAUDE.md`
|
||||
2. **Improving Existing Stories**: Use the Storybook Improvement issue template
|
||||
3. **Documentation**: Update relevant documentation when adding features
|
||||
4. **Testing**: Ensure all stories build and render correctly
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
- **Storybook Documentation**: `.storybook/README.md`
|
||||
- **Developer Guidelines**: `.storybook/CLAUDE.md`
|
||||
- **Component Examples**: `src/components/*/\*.stories.ts`
|
||||
- **Visual Testing**: Chromatic integration in CI/CD
|
||||
|
||||
---
|
||||
|
||||
*This issue is automatically maintained. Please reference this issue number when working on Storybook-related improvements.*
|
||||
@@ -392,16 +392,6 @@ Option 2 - Generate local baselines for comparison:
|
||||
npx playwright test --update-snapshots
|
||||
```
|
||||
|
||||
### Getting Test Artifacts from GitHub Actions
|
||||
|
||||
When tests fail in CI, you can download screenshots and traces:
|
||||
|
||||
1. Go to the failed workflow run in GitHub Actions
|
||||
2. Scroll to "Artifacts" section at the bottom
|
||||
3. Download `playwright-report` or `test-results`
|
||||
4. Extract and open the HTML report locally
|
||||
5. View actual vs expected screenshots and execution traces
|
||||
|
||||
### Creating New Screenshot Baselines
|
||||
|
||||
For PRs from `Comfy-Org/ComfyUI_frontend` branches:
|
||||
@@ -412,6 +402,33 @@ For PRs from `Comfy-Org/ComfyUI_frontend` branches:
|
||||
|
||||
> **Note:** Fork PRs cannot auto-commit screenshots. A maintainer will need to commit the screenshots manually for you (don't worry, they'll do it).
|
||||
|
||||
## Viewing Test Reports
|
||||
|
||||
### Automated Test Deployment
|
||||
|
||||
The project automatically deploys Playwright test reports to Cloudflare Pages for every PR and push to main branches.
|
||||
|
||||
### Accessing Test Reports
|
||||
|
||||
- **From PR comments**: Click the "View Report" links for each browser
|
||||
- **Direct URLs**: Reports are available at `https://[branch].comfyui-playwright-[browser].pages.dev` (branch-specific deployments)
|
||||
- **From GitHub Actions**: Download artifacts from failed runs
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Test execution**: All browser tests run in parallel across multiple browsers
|
||||
2. **Report generation**: HTML reports are generated for each browser configuration
|
||||
3. **Cloudflare deployment**: Each browser's report deploys to its own Cloudflare Pages project with branch isolation:
|
||||
- `comfyui-playwright-chromium` (with branch-specific URLs)
|
||||
- `comfyui-playwright-mobile-chrome` (with branch-specific URLs)
|
||||
- `comfyui-playwright-chromium-2x` (2x scale, with branch-specific URLs)
|
||||
- `comfyui-playwright-chromium-0-5x` (0.5x scale, with branch-specific URLs)
|
||||
|
||||
4. **PR comments**: GitHub automatically updates PR comments with:
|
||||
- ✅/❌ Test status for each browser
|
||||
- Direct links to interactive test reports
|
||||
- Real-time progress updates as tests complete
|
||||
|
||||
## Resources
|
||||
|
||||
- [Playwright UI Mode](https://playwright.dev/docs/test-ui-mode) - Interactive test debugging
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
"build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache",
|
||||
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache",
|
||||
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"test:browser": "npx playwright test",
|
||||
"test:unit": "vitest run tests-ui/tests",
|
||||
"test:component": "vitest run src/components/",
|
||||
@@ -25,7 +27,8 @@
|
||||
"lint:fix": "eslint src --cache --fix",
|
||||
"lint:no-cache": "eslint src",
|
||||
"lint:fix:no-cache": "eslint src --fix",
|
||||
"knip": "knip",
|
||||
"knip": "knip --cache",
|
||||
"knip:no-cache": "knip",
|
||||
"locale": "lobe-i18n locale",
|
||||
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 5V3C14 2.44772 13.5523 2 13 2H11C10.4477 2 10 2.44772 10 3V5C10 5.55228 10.4477 6 11 6H13C13.5523 6 14 5.55228 14 5Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M6 5V3C6 2.44772 5.55228 2 5 2H3C2.44772 2 2 2.44772 2 3V5C2 5.55228 2.44772 6 3 6H5C5.55228 6 6 5.55228 6 5Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M14 13V11C14 10.4477 13.5523 10 13 10H11C10.4477 10 10 10.4477 10 11V13C10 13.5523 10.4477 14 11 14H13C13.5523 14 14 13.5523 14 13Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M10 4H6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M10 12H8C5.79086 12 4 10.2091 4 8V6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.99999 4H6.99999M8.99999 12H7.6231C5.02081 12 3.11138 9.55445 3.74252 7.02986L3.99999 6M13.6894 3.24254L13.1894 5.24254C13.0781 5.6877 12.6781 6 12.2192 6H10.2808C9.63019 6 9.15284 5.38861 9.31062 4.75746L9.81062 2.75746C9.92192 2.3123 10.3219 2 10.7808 2H12.7192C13.3698 2 13.8471 2.61139 13.6894 3.24254ZM6.68936 3.24254L6.18936 5.24254C6.07806 5.6877 5.67808 6 5.21921 6H3.28077C2.63019 6 2.15284 5.38861 2.31062 4.75746L2.81062 2.75746C2.92191 2.3123 3.3219 2 3.78077 2H5.71921C6.36978 2 6.84714 2.61139 6.68936 3.24254ZM13.6894 11.2425L13.1894 13.2425C13.0781 13.6877 12.6781 14 12.2192 14H10.2808C9.63019 14 9.15284 13.3886 9.31062 12.7575L9.81062 10.7575C9.92192 10.3123 10.3219 10 10.7808 10H12.7192C13.3698 10 13.8471 10.6114 13.6894 11.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 890 B After Width: | Height: | Size: 910 B |
@@ -12,7 +12,6 @@
|
||||
<ColorPickerButton />
|
||||
<BypassButton />
|
||||
<PinButton />
|
||||
<EditModelButton />
|
||||
<Load3DViewerButton />
|
||||
<MaskEditorButton />
|
||||
<ConvertToSubgraphButton />
|
||||
@@ -35,7 +34,6 @@ import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||
import ConvertToSubgraphButton from '@/components/graph/selectionToolbox/ConvertToSubgraphButton.vue'
|
||||
import DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue'
|
||||
import EditModelButton from '@/components/graph/selectionToolbox/EditModelButton.vue'
|
||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
|
||||
import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="isImageOutputSelected"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Canvas_AddEditModelStep.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
icon="pi pi-pen-to-square"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.AddEditModelStep')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isImageNode, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const isImageOutputOrEditModelNode = (node: unknown) =>
|
||||
isLGraphNode(node) &&
|
||||
(isImageNode(node) || node.type === 'workflow>FLUX.1 Kontext Image Edit')
|
||||
|
||||
const isImageOutputSelected = computed(
|
||||
() =>
|
||||
canvasStore.selectedItems.length === 1 &&
|
||||
isImageOutputOrEditModelNode(canvasStore.selectedItems[0])
|
||||
)
|
||||
</script>
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { Point } from '@/lib/litegraph/src/litegraph'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { addFluxKontextGroupNode } from '@/scripts/fluxKontextEditNode'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
@@ -775,17 +774,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
versionAdded: moveSelectedNodesVersionAdded,
|
||||
function: () => moveSelectedNodes(([x, y], gridSize) => [x + gridSize, y])
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.AddEditModelStep',
|
||||
icon: 'pi pi-pen-to-square',
|
||||
label: 'Add Edit Model Step',
|
||||
versionAdded: '1.23.3',
|
||||
function: async () => {
|
||||
const node = app.canvas.selectedItems.values().next().value
|
||||
if (!(node instanceof LGraphNode)) return
|
||||
await addFluxKontextGroupNode(node)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.ConvertToSubgraph',
|
||||
icon: 'pi pi-sitemap',
|
||||
|
||||
@@ -1925,6 +1925,7 @@ export class LGraphNode
|
||||
}
|
||||
}
|
||||
|
||||
widget.onRemove?.()
|
||||
this.widgets.splice(widgetIndex, 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "تصفح القوالب"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "إضافة خطوة تحرير النموذج"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "حذف العناصر المحددة"
|
||||
},
|
||||
|
||||
@@ -762,7 +762,6 @@
|
||||
},
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "حول ComfyUI",
|
||||
"Add Edit Model Step": "إضافة خطوة تعديل النموذج",
|
||||
"Bottom Panel": "لوحة سفلية",
|
||||
"Browse Templates": "تصفح القوالب",
|
||||
"Bypass/Unbypass Selected Nodes": "تجاوز/إلغاء تجاوز العقد المحددة",
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Browse Templates"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "Add Edit Model Step"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "Delete Selected Items"
|
||||
},
|
||||
|
||||
@@ -958,7 +958,6 @@
|
||||
"Restart": "Restart",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node",
|
||||
"Browse Templates": "Browse Templates",
|
||||
"Add Edit Model Step": "Add Edit Model Step",
|
||||
"Delete Selected Items": "Delete Selected Items",
|
||||
"Zoom to fit": "Zoom to fit",
|
||||
"Move Selected Nodes Down": "Move Selected Nodes Down",
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Explorar plantillas"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "Agregar paso de edición de modelo"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "Eliminar elementos seleccionados"
|
||||
},
|
||||
|
||||
@@ -762,7 +762,6 @@
|
||||
},
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "Acerca de ComfyUI",
|
||||
"Add Edit Model Step": "Agregar paso de edición de modelo",
|
||||
"Bottom Panel": "Panel inferior",
|
||||
"Browse Templates": "Explorar plantillas",
|
||||
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Parcourir les modèles"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "Ajouter/Modifier une étape de modèle"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "Supprimer les éléments sélectionnés"
|
||||
},
|
||||
|
||||
@@ -762,7 +762,6 @@
|
||||
},
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "À propos de ComfyUI",
|
||||
"Add Edit Model Step": "Ajouter une étape d’édition de modèle",
|
||||
"Bottom Panel": "Panneau inférieur",
|
||||
"Browse Templates": "Parcourir les modèles",
|
||||
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "テンプレートを参照"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "編集モデルステップを追加"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "選択したアイテムを削除"
|
||||
},
|
||||
|
||||
@@ -762,7 +762,6 @@
|
||||
},
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "ComfyUIについて",
|
||||
"Add Edit Model Step": "モデル編集ステップを追加",
|
||||
"Bottom Panel": "下部パネル",
|
||||
"Browse Templates": "テンプレートを参照",
|
||||
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "템플릿 탐색"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "모델 편집 단계 추가"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "선택한 항목 삭제"
|
||||
},
|
||||
|
||||
@@ -762,7 +762,6 @@
|
||||
},
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "ComfyUI에 대하여",
|
||||
"Add Edit Model Step": "모델 편집 단계 추가",
|
||||
"Bottom Panel": "하단 패널",
|
||||
"Browse Templates": "템플릿 탐색",
|
||||
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Просмотр шаблонов"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "Добавить или изменить шаг модели"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "Удалить выбранные элементы"
|
||||
},
|
||||
|
||||
@@ -762,7 +762,6 @@
|
||||
},
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "О ComfyUI",
|
||||
"Add Edit Model Step": "Добавить или изменить шаг модели",
|
||||
"Bottom Panel": "Нижняя панель",
|
||||
"Browse Templates": "Просмотреть шаблоны",
|
||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "瀏覽範本"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "新增編輯模型步驟"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "刪除選取項目"
|
||||
},
|
||||
|
||||
@@ -762,7 +762,6 @@
|
||||
},
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "關於 ComfyUI",
|
||||
"Add Edit Model Step": "新增編輯模型步驟",
|
||||
"Bottom Panel": "底部面板",
|
||||
"Browse Templates": "瀏覽範本",
|
||||
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "浏览模板"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "添加编辑模型步骤"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "删除选定的项目"
|
||||
},
|
||||
|
||||
@@ -762,7 +762,6 @@
|
||||
},
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "关于ComfyUI",
|
||||
"Add Edit Model Step": "添加编辑模型步骤",
|
||||
"Bottom Panel": "底部面板",
|
||||
"Browse Templates": "浏览模板",
|
||||
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
|
||||
|
||||
@@ -1,693 +0,0 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import {
|
||||
type INodeOutputSlot,
|
||||
type LGraph,
|
||||
type LGraphNode,
|
||||
LLink,
|
||||
LiteGraph,
|
||||
type Point
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { parseFilePath } from '@/utils/formatUtil'
|
||||
|
||||
import { app } from './app'
|
||||
|
||||
const fluxKontextGroupNode = {
|
||||
nodes: [
|
||||
{
|
||||
id: -1,
|
||||
type: 'Reroute',
|
||||
pos: [2354.87890625, -127.23468780517578],
|
||||
size: [75, 26],
|
||||
flags: {},
|
||||
order: 20,
|
||||
mode: 0,
|
||||
inputs: [{ name: '', type: '*', link: null }],
|
||||
outputs: [{ name: '', type: '*', links: null }],
|
||||
properties: { showOutputText: false, horizontal: false },
|
||||
index: 0
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
type: 'ReferenceLatent',
|
||||
pos: [2730, -220],
|
||||
size: [197.712890625, 46],
|
||||
flags: {},
|
||||
order: 22,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
localized_name: 'conditioning',
|
||||
name: 'conditioning',
|
||||
type: 'CONDITIONING',
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'latent',
|
||||
name: 'latent',
|
||||
shape: 7,
|
||||
type: 'LATENT',
|
||||
link: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
localized_name: 'CONDITIONING',
|
||||
name: 'CONDITIONING',
|
||||
type: 'CONDITIONING',
|
||||
links: []
|
||||
}
|
||||
],
|
||||
properties: {
|
||||
'Node name for S&R': 'ReferenceLatent',
|
||||
cnr_id: 'comfy-core',
|
||||
ver: '0.3.38'
|
||||
},
|
||||
index: 1
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
type: 'VAEDecode',
|
||||
pos: [3270, -110],
|
||||
size: [210, 46],
|
||||
flags: {},
|
||||
order: 25,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
localized_name: 'samples',
|
||||
name: 'samples',
|
||||
type: 'LATENT',
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'vae',
|
||||
name: 'vae',
|
||||
type: 'VAE',
|
||||
link: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
localized_name: 'IMAGE',
|
||||
name: 'IMAGE',
|
||||
type: 'IMAGE',
|
||||
slot_index: 0,
|
||||
links: []
|
||||
}
|
||||
],
|
||||
properties: {
|
||||
'Node name for S&R': 'VAEDecode',
|
||||
cnr_id: 'comfy-core',
|
||||
ver: '0.3.38'
|
||||
},
|
||||
index: 2
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
type: 'KSampler',
|
||||
pos: [2930, -110],
|
||||
size: [315, 262],
|
||||
flags: {},
|
||||
order: 24,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
localized_name: 'model',
|
||||
name: 'model',
|
||||
type: 'MODEL',
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'positive',
|
||||
name: 'positive',
|
||||
type: 'CONDITIONING',
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'negative',
|
||||
name: 'negative',
|
||||
type: 'CONDITIONING',
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'latent_image',
|
||||
name: 'latent_image',
|
||||
type: 'LATENT',
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'seed',
|
||||
name: 'seed',
|
||||
type: 'INT',
|
||||
widget: { name: 'seed' },
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'steps',
|
||||
name: 'steps',
|
||||
type: 'INT',
|
||||
widget: { name: 'steps' },
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'cfg',
|
||||
name: 'cfg',
|
||||
type: 'FLOAT',
|
||||
widget: { name: 'cfg' },
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'sampler_name',
|
||||
name: 'sampler_name',
|
||||
type: 'COMBO',
|
||||
widget: { name: 'sampler_name' },
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'scheduler',
|
||||
name: 'scheduler',
|
||||
type: 'COMBO',
|
||||
widget: { name: 'scheduler' },
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'denoise',
|
||||
name: 'denoise',
|
||||
type: 'FLOAT',
|
||||
widget: { name: 'denoise' },
|
||||
link: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
localized_name: 'LATENT',
|
||||
name: 'LATENT',
|
||||
type: 'LATENT',
|
||||
slot_index: 0,
|
||||
links: []
|
||||
}
|
||||
],
|
||||
properties: {
|
||||
'Node name for S&R': 'KSampler',
|
||||
cnr_id: 'comfy-core',
|
||||
ver: '0.3.38'
|
||||
},
|
||||
widgets_values: [972054013131369, 'fixed', 20, 1, 'euler', 'simple', 1],
|
||||
index: 3
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
type: 'FluxGuidance',
|
||||
pos: [2940, -220],
|
||||
size: [211.60000610351562, 58],
|
||||
flags: {},
|
||||
order: 23,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
localized_name: 'conditioning',
|
||||
name: 'conditioning',
|
||||
type: 'CONDITIONING',
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'guidance',
|
||||
name: 'guidance',
|
||||
type: 'FLOAT',
|
||||
widget: { name: 'guidance' },
|
||||
link: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
localized_name: 'CONDITIONING',
|
||||
name: 'CONDITIONING',
|
||||
type: 'CONDITIONING',
|
||||
slot_index: 0,
|
||||
links: []
|
||||
}
|
||||
],
|
||||
properties: {
|
||||
'Node name for S&R': 'FluxGuidance',
|
||||
cnr_id: 'comfy-core',
|
||||
ver: '0.3.38'
|
||||
},
|
||||
widgets_values: [2.5],
|
||||
index: 4
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
type: 'SaveImage',
|
||||
pos: [3490, -110],
|
||||
size: [985.3012084960938, 1060.3828125],
|
||||
flags: {},
|
||||
order: 26,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
localized_name: 'images',
|
||||
name: 'images',
|
||||
type: 'IMAGE',
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'filename_prefix',
|
||||
name: 'filename_prefix',
|
||||
type: 'STRING',
|
||||
widget: { name: 'filename_prefix' },
|
||||
link: null
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
properties: { cnr_id: 'comfy-core', ver: '0.3.38' },
|
||||
widgets_values: ['ComfyUI'],
|
||||
index: 5
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
type: 'CLIPTextEncode',
|
||||
pos: [2500, -110],
|
||||
size: [422.84503173828125, 164.31304931640625],
|
||||
flags: {},
|
||||
order: 12,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
localized_name: 'clip',
|
||||
name: 'clip',
|
||||
type: 'CLIP',
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'text',
|
||||
name: 'text',
|
||||
type: 'STRING',
|
||||
widget: { name: 'text' },
|
||||
link: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
localized_name: 'CONDITIONING',
|
||||
name: 'CONDITIONING',
|
||||
type: 'CONDITIONING',
|
||||
slot_index: 0,
|
||||
links: []
|
||||
}
|
||||
],
|
||||
title: 'CLIP Text Encode (Positive Prompt)',
|
||||
properties: {
|
||||
'Node name for S&R': 'CLIPTextEncode',
|
||||
cnr_id: 'comfy-core',
|
||||
ver: '0.3.38'
|
||||
},
|
||||
widgets_values: ['there is a bright light'],
|
||||
color: '#232',
|
||||
bgcolor: '#353',
|
||||
index: 6
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
type: 'CLIPTextEncode',
|
||||
pos: [2504.1435546875, 97.9598617553711],
|
||||
size: [422.84503173828125, 164.31304931640625],
|
||||
flags: { collapsed: true },
|
||||
order: 13,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
localized_name: 'clip',
|
||||
name: 'clip',
|
||||
type: 'CLIP',
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'text',
|
||||
name: 'text',
|
||||
type: 'STRING',
|
||||
widget: { name: 'text' },
|
||||
link: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
localized_name: 'CONDITIONING',
|
||||
name: 'CONDITIONING',
|
||||
type: 'CONDITIONING',
|
||||
slot_index: 0,
|
||||
links: []
|
||||
}
|
||||
],
|
||||
title: 'CLIP Text Encode (Negative Prompt)',
|
||||
properties: {
|
||||
'Node name for S&R': 'CLIPTextEncode',
|
||||
cnr_id: 'comfy-core',
|
||||
ver: '0.3.38'
|
||||
},
|
||||
widgets_values: [''],
|
||||
color: '#322',
|
||||
bgcolor: '#533',
|
||||
index: 7
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
type: 'UNETLoader',
|
||||
pos: [2630, -370],
|
||||
size: [270, 82],
|
||||
flags: {},
|
||||
order: 6,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
localized_name: 'unet_name',
|
||||
name: 'unet_name',
|
||||
type: 'COMBO',
|
||||
widget: { name: 'unet_name' },
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'weight_dtype',
|
||||
name: 'weight_dtype',
|
||||
type: 'COMBO',
|
||||
widget: { name: 'weight_dtype' },
|
||||
link: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
localized_name: 'MODEL',
|
||||
name: 'MODEL',
|
||||
type: 'MODEL',
|
||||
links: []
|
||||
}
|
||||
],
|
||||
properties: {
|
||||
'Node name for S&R': 'UNETLoader',
|
||||
cnr_id: 'comfy-core',
|
||||
ver: '0.3.38'
|
||||
},
|
||||
widgets_values: ['flux1-kontext-dev.safetensors', 'default'],
|
||||
color: '#223',
|
||||
bgcolor: '#335',
|
||||
index: 8
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
type: 'DualCLIPLoader',
|
||||
pos: [2100, -290],
|
||||
size: [337.76861572265625, 130],
|
||||
flags: {},
|
||||
order: 8,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
localized_name: 'clip_name1',
|
||||
name: 'clip_name1',
|
||||
type: 'COMBO',
|
||||
widget: { name: 'clip_name1' },
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'clip_name2',
|
||||
name: 'clip_name2',
|
||||
type: 'COMBO',
|
||||
widget: { name: 'clip_name2' },
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'type',
|
||||
name: 'type',
|
||||
type: 'COMBO',
|
||||
widget: { name: 'type' },
|
||||
link: null
|
||||
},
|
||||
{
|
||||
localized_name: 'device',
|
||||
name: 'device',
|
||||
shape: 7,
|
||||
type: 'COMBO',
|
||||
widget: { name: 'device' },
|
||||
link: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
localized_name: 'CLIP',
|
||||
name: 'CLIP',
|
||||
type: 'CLIP',
|
||||
links: []
|
||||
}
|
||||
],
|
||||
properties: {
|
||||
'Node name for S&R': 'DualCLIPLoader',
|
||||
cnr_id: 'comfy-core',
|
||||
ver: '0.3.38'
|
||||
},
|
||||
widgets_values: [
|
||||
'clip_l.safetensors',
|
||||
't5xxl_fp8_e4m3fn_scaled.safetensors',
|
||||
'flux',
|
||||
'default'
|
||||
],
|
||||
color: '#223',
|
||||
bgcolor: '#335',
|
||||
index: 9
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
type: 'VAELoader',
|
||||
pos: [2960, -370],
|
||||
size: [270, 58],
|
||||
flags: {},
|
||||
order: 7,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{
|
||||
localized_name: 'vae_name',
|
||||
name: 'vae_name',
|
||||
type: 'COMBO',
|
||||
widget: { name: 'vae_name' },
|
||||
link: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
localized_name: 'VAE',
|
||||
name: 'VAE',
|
||||
type: 'VAE',
|
||||
links: []
|
||||
}
|
||||
],
|
||||
properties: {
|
||||
'Node name for S&R': 'VAELoader',
|
||||
cnr_id: 'comfy-core',
|
||||
ver: '0.3.38'
|
||||
},
|
||||
widgets_values: ['ae.safetensors'],
|
||||
color: '#223',
|
||||
bgcolor: '#335',
|
||||
index: 10
|
||||
}
|
||||
],
|
||||
links: [
|
||||
[6, 0, 1, 0, 72, 'CONDITIONING'],
|
||||
[0, 0, 1, 1, 66, '*'],
|
||||
[3, 0, 2, 0, 69, 'LATENT'],
|
||||
[10, 0, 2, 1, 76, 'VAE'],
|
||||
[8, 0, 3, 0, 74, 'MODEL'],
|
||||
[4, 0, 3, 1, 70, 'CONDITIONING'],
|
||||
[7, 0, 3, 2, 73, 'CONDITIONING'],
|
||||
[0, 0, 3, 3, 66, '*'],
|
||||
[1, 0, 4, 0, 67, 'CONDITIONING'],
|
||||
[2, 0, 5, 0, 68, 'IMAGE'],
|
||||
[9, 0, 6, 0, 75, 'CLIP'],
|
||||
[9, 0, 7, 0, 75, 'CLIP']
|
||||
],
|
||||
external: [],
|
||||
config: {
|
||||
'0': {},
|
||||
'1': {},
|
||||
'2': { output: { '0': { visible: true } } },
|
||||
'3': {
|
||||
output: { '0': { visible: true } },
|
||||
input: {
|
||||
denoise: { visible: false },
|
||||
cfg: { visible: false }
|
||||
}
|
||||
},
|
||||
'4': {},
|
||||
'5': {},
|
||||
'6': {},
|
||||
'7': { input: { text: { visible: false } } },
|
||||
'8': { input: { weight_dtype: { visible: false } } },
|
||||
'9': { input: { type: { visible: false }, device: { visible: false } } },
|
||||
'10': {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureGraphHasFluxKontextGroupNode(
|
||||
graph: LGraph & { extra: { groupNodes?: Record<string, any> } }
|
||||
) {
|
||||
graph.extra ??= {}
|
||||
graph.extra.groupNodes ??= {}
|
||||
if (graph.extra.groupNodes['FLUX.1 Kontext Image Edit']) return
|
||||
|
||||
graph.extra.groupNodes['FLUX.1 Kontext Image Edit'] =
|
||||
structuredClone(fluxKontextGroupNode)
|
||||
|
||||
// Lazy import to avoid circular dependency issues
|
||||
const { GroupNodeConfig } = await import('@/extensions/core/groupNode')
|
||||
await GroupNodeConfig.registerFromWorkflow(
|
||||
{
|
||||
'FLUX.1 Kontext Image Edit':
|
||||
graph.extra.groupNodes['FLUX.1 Kontext Image Edit']
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
export async function addFluxKontextGroupNode(fromNode: LGraphNode) {
|
||||
const { canvas } = app
|
||||
const { graph } = canvas
|
||||
if (!graph) throw new TypeError('Graph is not initialized')
|
||||
await ensureGraphHasFluxKontextGroupNode(graph)
|
||||
|
||||
const node = LiteGraph.createNode('workflow>FLUX.1 Kontext Image Edit')
|
||||
if (!node) throw new TypeError('Failed to create node')
|
||||
|
||||
const pos = getPosToRightOfNode(fromNode)
|
||||
|
||||
graph.add(node)
|
||||
node.pos = pos
|
||||
app.canvas.processSelect(node, undefined)
|
||||
|
||||
connectPreviousLatent(fromNode, node)
|
||||
|
||||
const symb = Object.getOwnPropertySymbols(node)[0]
|
||||
// @ts-expect-error It's there -- promise.
|
||||
node[symb].populateWidgets()
|
||||
|
||||
setWidgetValues(node)
|
||||
}
|
||||
|
||||
function setWidgetValues(node: LGraphNode) {
|
||||
const seedInput = node.widgets?.find((x) => x.name === 'seed')
|
||||
if (!seedInput) throw new TypeError('Seed input not found')
|
||||
seedInput.value = Math.floor(Math.random() * 1_125_899_906_842_624)
|
||||
|
||||
const firstClip = node.widgets?.find((x) => x.name === 'clip_name1')
|
||||
setPreferredValue('t5xxl_fp8_e4m3fn_scaled.safetensors', 't5xxl', firstClip)
|
||||
|
||||
const secondClip = node.widgets?.find((x) => x.name === 'clip_name2')
|
||||
setPreferredValue('clip_l.safetensors', 'clip_l', secondClip)
|
||||
|
||||
const unet = node.widgets?.find((x) => x.name === 'unet_name')
|
||||
setPreferredValue('flux1-dev-kontext_fp8_scaled.safetensors', 'kontext', unet)
|
||||
|
||||
const vae = node.widgets?.find((x) => x.name === 'vae_name')
|
||||
setPreferredValue('ae.safetensors', 'ae.s', vae)
|
||||
}
|
||||
|
||||
function setPreferredValue(
|
||||
preferred: string,
|
||||
match: string,
|
||||
widget: IBaseWidget | undefined
|
||||
): void {
|
||||
if (!widget) throw new TypeError('Widget not found')
|
||||
|
||||
const { values } = widget.options
|
||||
if (!Array.isArray(values)) return
|
||||
|
||||
// Match against filename portion only
|
||||
const mapped = values.map((x) => parseFilePath(x).filename)
|
||||
const value =
|
||||
mapped.find((x) => x === preferred) ??
|
||||
mapped.find((x) => x.includes?.(match))
|
||||
widget.value = value ?? preferred
|
||||
}
|
||||
|
||||
function getPosToRightOfNode(fromNode: LGraphNode) {
|
||||
const nodes = app.canvas.graph?.nodes
|
||||
if (!nodes) throw new TypeError('Could not get graph nodes')
|
||||
|
||||
const pos = [
|
||||
fromNode.pos[0] + fromNode.size[0] + 100,
|
||||
fromNode.pos[1]
|
||||
] satisfies Point
|
||||
|
||||
while (nodes.find((x) => isPointTooClose(x.pos, pos))) {
|
||||
pos[0] += 20
|
||||
pos[1] += 20
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
function connectPreviousLatent(fromNode: LGraphNode, toEditNode: LGraphNode) {
|
||||
const { canvas } = app
|
||||
const { graph } = canvas
|
||||
if (!graph) throw new TypeError('Graph is not initialized')
|
||||
|
||||
const l = findNearestOutputOfType([fromNode], 'LATENT')
|
||||
if (!l) {
|
||||
const imageOutput = findNearestOutputOfType([fromNode], 'IMAGE')
|
||||
if (!imageOutput) throw new TypeError('No image output found')
|
||||
|
||||
const vaeEncode = LiteGraph.createNode('VAEEncode')
|
||||
if (!vaeEncode) throw new TypeError('Failed to create node')
|
||||
|
||||
const { node: imageNode, index: imageIndex } = imageOutput
|
||||
graph.add(vaeEncode)
|
||||
vaeEncode.pos = getPosToRightOfNode(fromNode)
|
||||
vaeEncode.pos[1] -= 200
|
||||
|
||||
vaeEncode.connect(0, toEditNode, 0)
|
||||
imageNode.connect(imageIndex, vaeEncode, 0)
|
||||
return
|
||||
}
|
||||
|
||||
const { node, index } = l
|
||||
|
||||
node.connect(index, toEditNode, 0)
|
||||
}
|
||||
|
||||
function getInputNodes(node: LGraphNode): LGraphNode[] {
|
||||
return node.inputs
|
||||
.map((x) => LLink.resolve(x.link, app.graph)?.outputNode)
|
||||
.filter((x) => !!x)
|
||||
}
|
||||
|
||||
function getOutputOfType(
|
||||
node: LGraphNode,
|
||||
type: string
|
||||
): {
|
||||
output: INodeOutputSlot
|
||||
index: number
|
||||
} {
|
||||
const index = node.outputs.findIndex((x) => x.type === type)
|
||||
const output = node.outputs[index]
|
||||
return { output, index }
|
||||
}
|
||||
|
||||
function findNearestOutputOfType(
|
||||
nodes: Iterable<LGraphNode>,
|
||||
type: string = 'LATENT',
|
||||
depth: number = 0
|
||||
): { node: LGraphNode; index: number } | undefined {
|
||||
for (const node of nodes) {
|
||||
const { output, index } = getOutputOfType(node, type)
|
||||
if (output) return { node, index }
|
||||
}
|
||||
|
||||
if (depth < 3) {
|
||||
const closestNodes = new Set([...nodes].flatMap((x) => getInputNodes(x)))
|
||||
const res = findNearestOutputOfType(closestNodes, type, depth + 1)
|
||||
if (res) return res
|
||||
}
|
||||
}
|
||||
|
||||
function isPointTooClose(a: Point, b: Point, precision: number = 5) {
|
||||
return Math.abs(a[0] - b[0]) < precision && Math.abs(a[1] - b[1]) < precision
|
||||
}
|
||||
Reference in New Issue
Block a user