Compare commits
14 Commits
fix/settin
...
feat/more-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a97f38d980 | ||
|
|
7a873e95ca | ||
|
|
f0eac7e019 | ||
|
|
c6551430ce | ||
|
|
40a448ea31 | ||
|
|
151bf78f9f | ||
|
|
26a5d3cf7d | ||
|
|
27cfd0c6db | ||
|
|
50cb16fb07 | ||
|
|
344ae10d77 | ||
|
|
f17dd9de32 | ||
|
|
d8b416f7a8 | ||
|
|
664f407d6e | ||
|
|
1f69c5ce86 |
79
.github/actions/upsert-comment-section/action.yaml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Upsert Comment Section
|
||||
description: >
|
||||
Manage a consolidated PR comment with independently-updatable sections.
|
||||
All website CI workflows share the marker <!-- WEBSITE_CI_REPORT -->.
|
||||
Valid section names: "e2e", "preview", "screenshot-update".
|
||||
|
||||
inputs:
|
||||
pr-number:
|
||||
description: PR number to comment on
|
||||
required: true
|
||||
section-name:
|
||||
description: 'Section identifier: "e2e", "preview", or "screenshot-update"'
|
||||
required: true
|
||||
section-content:
|
||||
description: Markdown content for this section
|
||||
required: true
|
||||
comment-marker:
|
||||
description: Top-level HTML comment marker (must be <!-- WEBSITE_CI_REPORT --> for all callers)
|
||||
required: true
|
||||
token:
|
||||
description: GitHub token with pull-requests write permission
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
env:
|
||||
INPUT_PR_NUMBER: ${{ inputs.pr-number }}
|
||||
INPUT_SECTION_NAME: ${{ inputs.section-name }}
|
||||
INPUT_SECTION_CONTENT: ${{ inputs.section-content }}
|
||||
INPUT_COMMENT_MARKER: ${{ inputs.comment-marker }}
|
||||
with:
|
||||
github-token: ${{ inputs.token }}
|
||||
script: |
|
||||
const prNumber = Number(process.env.INPUT_PR_NUMBER)
|
||||
const sectionName = process.env.INPUT_SECTION_NAME
|
||||
const sectionContent = process.env.INPUT_SECTION_CONTENT
|
||||
const commentMarker = process.env.INPUT_COMMENT_MARKER
|
||||
|
||||
const sectionStart = `<!-- section:${sectionName}:start -->`
|
||||
const sectionEnd = `<!-- section:${sectionName}:end -->`
|
||||
const sectionBlock = `${sectionStart}\n${sectionContent}\n${sectionEnd}`
|
||||
|
||||
// Escape special regex characters in delimiter strings
|
||||
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{ ...context.repo, issue_number: prNumber }
|
||||
)
|
||||
|
||||
const existing = comments.find(
|
||||
(c) =>
|
||||
c.user?.login === 'github-actions[bot]' &&
|
||||
c.body?.includes(commentMarker)
|
||||
)
|
||||
|
||||
if (!existing) {
|
||||
return github.rest.issues.createComment({
|
||||
...context.repo,
|
||||
issue_number: prNumber,
|
||||
body: `${commentMarker}\n${sectionBlock}`
|
||||
})
|
||||
}
|
||||
|
||||
const body = existing.body ?? ''
|
||||
const sectionRegex = new RegExp(
|
||||
`${escapeRegex(sectionStart)}[\\s\\S]*?${escapeRegex(sectionEnd)}`
|
||||
)
|
||||
const updated = sectionRegex.test(body)
|
||||
? body.replace(sectionRegex, sectionBlock)
|
||||
: body.trimEnd() + '\n\n' + sectionBlock
|
||||
|
||||
return github.rest.issues.updateComment({
|
||||
...context.repo,
|
||||
comment_id: existing.id,
|
||||
body: updated
|
||||
})
|
||||
153
.github/workflows/ci-website-e2e.yaml
vendored
@@ -27,35 +27,17 @@ jobs:
|
||||
image: mcr.microsoft.com/playwright:v1.58.1-noble
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
outputs:
|
||||
test-outcome: ${{ steps.tests.outcome }}
|
||||
report-url: ${{ steps.deploy.outputs.url }}
|
||||
screenshot-failures: ${{ steps.failures.outputs.screenshot }}
|
||||
other-failures: ${{ steps.failures.outputs.other }}
|
||||
# Evaluated at job level (not from a step) — static expression.
|
||||
is-pr: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Post starting comment
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const marker = '<!-- WEBSITE_E2E_STATUS -->'
|
||||
const body = [
|
||||
marker,
|
||||
'## 🌐 Website E2E',
|
||||
'',
|
||||
'> [!NOTE]',
|
||||
`> Tests are running… [View workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
|
||||
].join('\n')
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
...context.repo,
|
||||
issue_number: context.issue.number
|
||||
})
|
||||
const existing = comments.find(c => c.body.includes(marker))
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body })
|
||||
} else {
|
||||
await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body })
|
||||
}
|
||||
|
||||
- name: Install pnpm
|
||||
run: corepack enable && corepack prepare
|
||||
|
||||
@@ -134,15 +116,20 @@ jobs:
|
||||
core.setOutput('screenshot', screenshotFailures)
|
||||
core.setOutput('other', failed.length - screenshotFailures)
|
||||
|
||||
- name: Post result comment and summary
|
||||
- name: Write job summary
|
||||
if: always() && !cancelled()
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
TEST_OUTCOME: ${{ steps.tests.outcome }}
|
||||
REPORT_URL: ${{ steps.deploy.outputs.url }}
|
||||
SCREENSHOT_FAILURES: ${{ steps.failures.outputs.screenshot }}
|
||||
OTHER_FAILURES: ${{ steps.failures.outputs.other }}
|
||||
with:
|
||||
script: |
|
||||
const passed = '${{ steps.tests.outcome }}' === 'success'
|
||||
const reportUrl = '${{ steps.deploy.outputs.url }}'
|
||||
const screenshotFailures = parseInt('${{ steps.failures.outputs.screenshot }}') || 0
|
||||
const otherFailures = parseInt('${{ steps.failures.outputs.other }}') || 0
|
||||
const passed = process.env.TEST_OUTCOME === 'success'
|
||||
const reportUrl = process.env.REPORT_URL
|
||||
const screenshotFailures = parseInt(process.env.SCREENSHOT_FAILURES) || 0
|
||||
const otherFailures = parseInt(process.env.OTHER_FAILURES) || 0
|
||||
|
||||
const lines = ['## 🌐 Website E2E', '']
|
||||
|
||||
@@ -152,7 +139,87 @@ jobs:
|
||||
lines.push('> [!CAUTION]', '> Some tests failed.')
|
||||
}
|
||||
|
||||
// Results table
|
||||
const rows = [
|
||||
['Status', passed ? '✅ Passed' : '❌ Failed'],
|
||||
['Report', reportUrl ? `[View Report](${reportUrl})` : '_unavailable_']
|
||||
]
|
||||
if (!passed) {
|
||||
rows.push(
|
||||
['Screenshot diffs', String(screenshotFailures)],
|
||||
['Other failures', String(otherFailures)]
|
||||
)
|
||||
}
|
||||
lines.push(
|
||||
'',
|
||||
'| | |',
|
||||
'|---|---|',
|
||||
...rows.map(([k, v]) => `| **${k}** | ${v} |`)
|
||||
)
|
||||
|
||||
await core.summary.addRaw(lines.join('\n')).write()
|
||||
|
||||
post-starting-comment:
|
||||
# Safe to comment from pull_request trigger: fork PRs are excluded by the guard below.
|
||||
# This avoids a ci-*/pr-* workflow_run split for a comment that must appear immediately.
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
concurrency:
|
||||
group: website-pr-comment-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/upsert-comment-section
|
||||
with:
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
section-name: e2e
|
||||
comment-marker: '<!-- WEBSITE_CI_REPORT -->'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
section-content: |-
|
||||
## 🌐 Website E2E
|
||||
<!-- WEBSITE_E2E_STATUS -->
|
||||
|
||||
> [!NOTE]
|
||||
> Tests are running… [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
post-result-comment:
|
||||
needs: website-e2e
|
||||
if: always() && !cancelled() && needs.website-e2e.outputs.is-pr == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
concurrency:
|
||||
group: website-pr-comment-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build e2e section content
|
||||
id: content
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
TEST_OUTCOME: ${{ needs.website-e2e.outputs.test-outcome }}
|
||||
REPORT_URL: ${{ needs.website-e2e.outputs.report-url }}
|
||||
SCREENSHOT_FAILURES: ${{ needs.website-e2e.outputs.screenshot-failures }}
|
||||
OTHER_FAILURES: ${{ needs.website-e2e.outputs.other-failures }}
|
||||
with:
|
||||
script: |
|
||||
const passed = process.env.TEST_OUTCOME === 'success'
|
||||
const reportUrl = process.env.REPORT_URL
|
||||
const screenshotFailures = parseInt(process.env.SCREENSHOT_FAILURES) || 0
|
||||
const otherFailures = parseInt(process.env.OTHER_FAILURES) || 0
|
||||
|
||||
const lines = ['## 🌐 Website E2E', '<!-- WEBSITE_E2E_STATUS -->', '']
|
||||
|
||||
if (passed) {
|
||||
lines.push('> [!TIP]', '> All tests passed.')
|
||||
} else {
|
||||
lines.push('> [!CAUTION]', '> Some tests failed.')
|
||||
}
|
||||
|
||||
const rows = [
|
||||
['Status', passed ? '✅ Passed' : '❌ Failed'],
|
||||
['Report', reportUrl ? `[View Report](${reportUrl})` : '_unavailable_']
|
||||
@@ -182,20 +249,12 @@ jobs:
|
||||
)
|
||||
}
|
||||
|
||||
const summary = lines.join('\n')
|
||||
await core.summary.addRaw(summary).write()
|
||||
core.setOutput('section-content', lines.join('\n'))
|
||||
|
||||
if (context.eventName === 'pull_request' && !context.payload.pull_request.head.repo.fork) {
|
||||
const marker = '<!-- WEBSITE_E2E_STATUS -->'
|
||||
const body = `${marker}\n${summary}`
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
...context.repo,
|
||||
issue_number: context.issue.number
|
||||
})
|
||||
const existing = comments.find(c => c.body.includes(marker))
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body })
|
||||
} else {
|
||||
await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body })
|
||||
}
|
||||
}
|
||||
- uses: ./.github/actions/upsert-comment-section
|
||||
with:
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
section-name: e2e
|
||||
comment-marker: '<!-- WEBSITE_CI_REPORT -->'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
section-content: ${{ steps.content.outputs.section-content }}
|
||||
|
||||
120
.github/workflows/pr-update-website-screenshots.yaml
vendored
@@ -16,7 +16,11 @@ jobs:
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.58.1-noble
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
# Trigger: (1) label, (2) /slash-command, or (3) checkbox in E2E status comment
|
||||
# ⚠️ This condition is duplicated on `post-starting-comment` — keep them in sync.
|
||||
if: >
|
||||
( github.event_name == 'pull_request' &&
|
||||
github.event.label.name == 'Update Website Screenshots' ) ||
|
||||
@@ -34,6 +38,11 @@ jobs:
|
||||
github.actor != 'github-actions[bot]' &&
|
||||
contains(github.event.comment.body, '<!-- WEBSITE_E2E_STATUS -->') &&
|
||||
contains(github.event.comment.body, '- [x] Update website screenshots') )
|
||||
outputs:
|
||||
pr-number: ${{ steps.pr-info.outputs.pr-number }}
|
||||
update-outcome: ${{ steps.update-screenshots.outcome }}
|
||||
has-changes: ${{ steps.commit.outputs.has-changes }}
|
||||
changed-count: ${{ steps.commit.outputs.changed-count }}
|
||||
steps:
|
||||
- name: Verify sender permissions
|
||||
if: >
|
||||
@@ -53,9 +62,11 @@ jobs:
|
||||
- name: Get PR info
|
||||
id: pr-info
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.number || github.event.issue.number }}
|
||||
with:
|
||||
script: |
|
||||
const prNumber = ${{ github.event.number || github.event.issue.number }}
|
||||
const prNumber = Number(process.env.PR_NUMBER)
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number: prNumber
|
||||
@@ -90,7 +101,10 @@ jobs:
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
|
||||
if [ -z "$(git status --porcelain=v1 --untracked-files=all -- apps/website/e2e/)" ]; then
|
||||
CHANGED=$(git status --porcelain=v1 --untracked-files=all -- apps/website/e2e/ | wc -l)
|
||||
echo "changed-count=${CHANGED}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "$CHANGED" -eq 0 ]; then
|
||||
echo "No screenshot changes to commit"
|
||||
echo "has-changes=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
@@ -112,14 +126,114 @@ jobs:
|
||||
- name: Remove label
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr-info.outputs.pr-number }}
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
...context.repo,
|
||||
issue_number: ${{ steps.pr-info.outputs.pr-number }},
|
||||
issue_number: Number(process.env.PR_NUMBER),
|
||||
name: 'Update Website Screenshots'
|
||||
})
|
||||
} catch (e) {
|
||||
// Label may already be removed
|
||||
}
|
||||
|
||||
post-starting-comment:
|
||||
# Runs in parallel with update-screenshots to show "in progress" immediately.
|
||||
# ⚠️ This condition is duplicated from `update-screenshots` — keep them in sync.
|
||||
if: >
|
||||
( github.event_name == 'pull_request' &&
|
||||
github.event.label.name == 'Update Website Screenshots' ) ||
|
||||
( github.event.issue.pull_request &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'
|
||||
) &&
|
||||
startsWith(github.event.comment.body, '/update-website-screenshots') ) ||
|
||||
( github.event.issue.pull_request &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.comment.user.login == 'github-actions[bot]' &&
|
||||
github.actor != 'github-actions[bot]' &&
|
||||
contains(github.event.comment.body, '<!-- WEBSITE_E2E_STATUS -->') &&
|
||||
contains(github.event.comment.body, '- [x] Update website screenshots') )
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
concurrency:
|
||||
group: website-pr-comment-${{ github.event.number || github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/upsert-comment-section
|
||||
with:
|
||||
pr-number: ${{ github.event.number || github.event.issue.number }}
|
||||
section-name: screenshot-update
|
||||
comment-marker: '<!-- WEBSITE_CI_REPORT -->'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
section-content: |-
|
||||
## 📸 Screenshot Update
|
||||
|
||||
> [!NOTE]
|
||||
> Updating screenshots… [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
post-result-comment:
|
||||
needs: update-screenshots
|
||||
if: always() && !cancelled() && needs.update-screenshots.result != 'skipped'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
concurrency:
|
||||
group: website-pr-comment-${{ needs.update-screenshots.outputs.pr-number }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build screenshot-update section content
|
||||
id: content
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
UPDATE_OUTCOME: ${{ needs.update-screenshots.outputs.update-outcome }}
|
||||
HAS_CHANGES: ${{ needs.update-screenshots.outputs.has-changes }}
|
||||
CHANGED_COUNT: ${{ needs.update-screenshots.outputs.changed-count }}
|
||||
with:
|
||||
script: |
|
||||
const outcome = process.env.UPDATE_OUTCOME
|
||||
const hasChanges = process.env.HAS_CHANGES === 'true'
|
||||
const changedCount = parseInt(process.env.CHANGED_COUNT) || 0
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
|
||||
|
||||
const lines = ['## 📸 Screenshot Update', '']
|
||||
|
||||
if (outcome !== 'success') {
|
||||
lines.push(
|
||||
'> [!CAUTION]',
|
||||
`> Screenshot update failed. [View workflow run](${runUrl})`
|
||||
)
|
||||
} else if (!hasChanges) {
|
||||
lines.push(
|
||||
'> [!TIP]',
|
||||
'> All screenshots are already up to date.'
|
||||
)
|
||||
} else {
|
||||
const s = changedCount === 1 ? '' : 's'
|
||||
lines.push(
|
||||
'> [!TIP]',
|
||||
`> Updated ${changedCount} screenshot${s} and pushed to the branch.`
|
||||
)
|
||||
}
|
||||
|
||||
core.setOutput('section-content', lines.join('\n'))
|
||||
|
||||
- uses: ./.github/actions/upsert-comment-section
|
||||
with:
|
||||
pr-number: ${{ needs.update-screenshots.outputs.pr-number }}
|
||||
section-name: screenshot-update
|
||||
comment-marker: '<!-- WEBSITE_CI_REPORT -->'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
section-content: ${{ steps.content.outputs.section-content }}
|
||||
|
||||
52
.github/workflows/pr-vercel-website-preview.yaml
vendored
@@ -7,11 +7,6 @@ on:
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
@@ -19,6 +14,16 @@ concurrency:
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
actions: read
|
||||
# Uses head_branch as proxy for PR number (unavailable at job-level in workflow_run).
|
||||
# Preview and E2E comment writes are NOT mutually serialized — the race window is
|
||||
# small and self-healing on next push.
|
||||
concurrency:
|
||||
group: website-pr-comment-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: false
|
||||
if: >
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
@@ -37,28 +42,29 @@ jobs:
|
||||
id: pr-meta
|
||||
uses: ./.github/actions/resolve-pr-from-workflow-run
|
||||
|
||||
- name: Write report
|
||||
- name: Read preview URLs
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
env:
|
||||
DEPLOYED_AT: ${{ github.event.workflow_run.updated_at }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
id: urls
|
||||
run: |
|
||||
STABLE_URL=$(cat temp/vercel-preview/stable-url.txt)
|
||||
UNIQUE_URL=$(cat temp/vercel-preview/url.txt)
|
||||
SHORT_SHA="${HEAD_SHA:0:7}"
|
||||
cat > preview-report.md <<EOF
|
||||
**Website Preview:** $STABLE_URL
|
||||
echo "stable-url=$(cat temp/vercel-preview/stable-url.txt)" >> "$GITHUB_OUTPUT"
|
||||
echo "unique-url=$(cat temp/vercel-preview/url.txt)" >> "$GITHUB_OUTPUT"
|
||||
echo "short-sha=${HEAD_SHA:0:7}" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
<sub>This commit: $UNIQUE_URL</sub>
|
||||
|
||||
<sub>Last updated: $DEPLOYED_AT for \`$SHORT_SHA\`</sub>
|
||||
EOF
|
||||
|
||||
- name: Post PR comment
|
||||
- name: Post preview comment
|
||||
if: steps.pr-meta.outputs.skip != 'true'
|
||||
uses: ./.github/actions/post-pr-report-comment
|
||||
uses: ./.github/actions/upsert-comment-section
|
||||
with:
|
||||
pr-number: ${{ steps.pr-meta.outputs.number }}
|
||||
report-file: ./preview-report.md
|
||||
comment-marker: '<!-- VERCEL_WEBSITE_PREVIEW -->'
|
||||
section-name: preview
|
||||
comment-marker: '<!-- WEBSITE_CI_REPORT -->'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
section-content: |-
|
||||
## 🔗 Website Preview
|
||||
|
||||
**Website Preview:** ${{ steps.urls.outputs.stable-url }}
|
||||
|
||||
<sub>This commit: ${{ steps.urls.outputs.unique-url }}</sub>
|
||||
|
||||
<sub>Last updated: ${{ github.event.workflow_run.updated_at }} for `${{ steps.urls.outputs.short-sha }}`</sub>
|
||||
|
||||
@@ -122,17 +122,6 @@ test.describe('About', { tag: '@visual' }, () => {
|
||||
}
|
||||
})
|
||||
|
||||
const OVERFLOW_SKIP = new Set([
|
||||
'/ 3-lg',
|
||||
'/ 4-xl',
|
||||
'/cloud 2-md',
|
||||
'/cloud 3-lg',
|
||||
'/cloud 4-xl',
|
||||
'/download 2-md',
|
||||
'/download 3-lg',
|
||||
'/download 4-xl'
|
||||
])
|
||||
|
||||
test.describe('Overflow guards', { tag: '@visual' }, () => {
|
||||
const pages = [
|
||||
'/',
|
||||
@@ -146,10 +135,7 @@ test.describe('Overflow guards', { tag: '@visual' }, () => {
|
||||
]
|
||||
for (const url of pages) {
|
||||
for (const vp of VIEWPORTS) {
|
||||
const key = `${url} ${vp.name}`
|
||||
|
||||
test(`${url} ${vp.name} no overflow`, async ({ page }) => {
|
||||
test.skip(OVERFLOW_SKIP.has(key), 'Known overflow bug at this viewport')
|
||||
await page.setViewportSize({ width: vp.width, height: vp.height })
|
||||
await page.goto(url)
|
||||
await assertNoOverflow(page)
|
||||
|
||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
@@ -204,13 +204,12 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<GitHubStarBadge v-if="githubStars" :stars="githubStars" />
|
||||
|
||||
<!-- Desktop CTA buttons -->
|
||||
<div
|
||||
data-testid="desktop-nav-cta"
|
||||
class="hidden shrink-0 items-center gap-2 lg:flex"
|
||||
>
|
||||
<GitHubStarBadge v-if="githubStars" :stars="githubStars" />
|
||||
<BrandButton
|
||||
v-for="cta in ctaButtons"
|
||||
:key="cta.href"
|
||||
|
||||
22
apps/website/src/components/customers/ContactSection.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="flex flex-col items-center px-4 pt-16 pb-24 text-center lg:px-20 lg:pt-10 lg:pb-40"
|
||||
>
|
||||
<span
|
||||
class="text-primary-comfy-yellow text-sm font-bold tracking-widest uppercase"
|
||||
>
|
||||
{{ t('customers.contact.label', locale) }}
|
||||
</span>
|
||||
<h2
|
||||
class="text-primary-comfy-canvas mt-4 max-w-2xl text-3xl font-light whitespace-pre-line"
|
||||
v-html="t('customers.contact.heading', locale)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
@@ -16,12 +15,8 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
{{ t('gallery.contact.label', locale) }}
|
||||
</span>
|
||||
<h2
|
||||
class="text-primary-comfy-canvas mt-4 text-3xl font-light whitespace-pre-line"
|
||||
>
|
||||
{{ t('gallery.contact.heading', locale) }}
|
||||
</h2>
|
||||
<BrandButton href="/contact" variant="outline-dark" size="sm" class="mt-8">
|
||||
{{ t('gallery.contact.cta', locale) }}
|
||||
</BrandButton>
|
||||
class="text-primary-comfy-canvas mt-4 max-w-2xl text-2xl font-light whitespace-pre-line"
|
||||
v-html="t('gallery.contact.heading', locale)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { t } from '../../i18n/translations'
|
||||
|
||||
import { externalLinks } from '../../config/routes'
|
||||
import { useParallax } from '../../composables/useParallax'
|
||||
import { usePinScrub } from '../../composables/usePinScrub'
|
||||
import { usePinScrub, VH_PER_ITEM } from '../../composables/usePinScrub'
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
@@ -52,7 +52,7 @@ const rightImgRef = ref<HTMLElement>()
|
||||
|
||||
const {
|
||||
activeIndex: activeCategory,
|
||||
isActive: isPinned,
|
||||
isEnabled,
|
||||
scrollToIndex
|
||||
} = usePinScrub(
|
||||
{ section: sectionRef, content: contentRef, nav: navRef },
|
||||
@@ -69,15 +69,34 @@ const uid = useId()
|
||||
const leftBlobId = `left-blob-${uid}`
|
||||
const rightBlobId = `right-blob-${uid}`
|
||||
|
||||
const pinScrubEnd = `+=${categories.length * 100}%`
|
||||
function onNavKeydown(event: KeyboardEvent) {
|
||||
const delta = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0
|
||||
if (!delta) return
|
||||
|
||||
event.preventDefault()
|
||||
const next = Math.min(
|
||||
categories.length - 1,
|
||||
Math.max(0, activeCategory.value + delta)
|
||||
)
|
||||
scrollToIndex(next)
|
||||
|
||||
const buttons =
|
||||
navRef.value?.querySelectorAll<HTMLButtonElement>(':scope > button')
|
||||
buttons?.[next]?.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
const pinScrubEnd = `+=${categories.length * VH_PER_ITEM}%`
|
||||
useParallax([rightImgRef], {
|
||||
trigger: sectionRef,
|
||||
fromY: -150,
|
||||
y: 150,
|
||||
start: 'top top',
|
||||
end: pinScrubEnd
|
||||
})
|
||||
useParallax([leftImgRef], {
|
||||
trigger: sectionRef,
|
||||
y: -60,
|
||||
fromY: 150,
|
||||
y: -150,
|
||||
start: 'top top',
|
||||
end: pinScrubEnd
|
||||
})
|
||||
@@ -89,7 +108,7 @@ useParallax([leftImgRef], {
|
||||
:class="
|
||||
cn(
|
||||
'bg-primary-comfy-ink relative isolate px-8 py-20 lg:px-0 lg:py-24',
|
||||
isPinned && 'overflow-x-clip lg:h-[calc(100vh+60px)]'
|
||||
isEnabled && 'overflow-x-clip lg:h-[calc(100vh+60px)]'
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -167,7 +186,8 @@ useParallax([leftImgRef], {
|
||||
<nav
|
||||
ref="navRef"
|
||||
class="mt-16 flex w-full max-w-5/6 flex-col items-center justify-center gap-12 lg:mt-20 lg:max-w-none lg:gap-8"
|
||||
aria-label="Industry categories"
|
||||
:aria-label="t('useCase.navLabel', locale)"
|
||||
@keydown="onNavKeydown"
|
||||
>
|
||||
<button
|
||||
v-for="(category, index) in categories"
|
||||
@@ -175,7 +195,7 @@ useParallax([leftImgRef], {
|
||||
type="button"
|
||||
:class="
|
||||
cn(
|
||||
'lg:text-4.5xl cursor-pointer text-center text-4xl font-light whitespace-pre-line transition-colors',
|
||||
'lg:text-4.5xl cursor-pointer text-center text-4xl font-light whitespace-pre-line transition-colors outline-none',
|
||||
index === activeCategory
|
||||
? 'text-primary-comfy-canvas'
|
||||
: 'text-primary-comfy-canvas/30 hover:text-primary-comfy-canvas/50'
|
||||
|
||||
@@ -1,67 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import type { Locale, TranslationKey } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
interface IncludedFeature {
|
||||
title: string
|
||||
description: string
|
||||
titleKey: TranslationKey
|
||||
descriptionKey: TranslationKey
|
||||
isComingSoon?: boolean
|
||||
}
|
||||
|
||||
const features: IncludedFeature[] = [
|
||||
{
|
||||
title: 'Machine Setup',
|
||||
description: 'Comfy Cloud runs on Blackwell RTX 6000 Pro – 96GB VRAM'
|
||||
titleKey: 'pricing.included.feature1.title',
|
||||
descriptionKey: 'pricing.included.feature1.description'
|
||||
},
|
||||
{
|
||||
title: 'Time limit per job',
|
||||
description:
|
||||
'Each workflow run has a maximum duration of 30 minutes. On the Pro plan, the time limit is increased to 1 hour. Jobs exceeding that limit are automatically cancelled to ensure fair usage and system stability.'
|
||||
titleKey: 'pricing.included.feature2.title',
|
||||
descriptionKey: 'pricing.included.feature2.description'
|
||||
},
|
||||
{
|
||||
title: 'Usage',
|
||||
description:
|
||||
"You're only charged for <strong>active GPU</strong> time while a workflow is running. Idle time (e.g. time spent building workflows) does not consume GPU hours."
|
||||
titleKey: 'pricing.included.feature3.title',
|
||||
descriptionKey: 'pricing.included.feature3.description'
|
||||
},
|
||||
{
|
||||
title: 'Credit balance',
|
||||
description:
|
||||
'All plans will include a monthly pool of credits that are spent on active workflow runtime and <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">Partner Nodes</a> like Nano Banana Pro.'
|
||||
titleKey: 'pricing.included.feature4.title',
|
||||
descriptionKey: 'pricing.included.feature4.description'
|
||||
},
|
||||
{
|
||||
title: 'Add more credits anytime',
|
||||
description:
|
||||
'Purchase additional credits at any time. Unused top-ups roll over to the next month automatically for up to 1 year.'
|
||||
titleKey: 'pricing.included.feature5.title',
|
||||
descriptionKey: 'pricing.included.feature5.description'
|
||||
},
|
||||
{
|
||||
title: 'Pre-installed models',
|
||||
description: 'Access a library of 900+ pre-installed models.'
|
||||
titleKey: 'pricing.included.feature6.title',
|
||||
descriptionKey: 'pricing.included.feature6.description'
|
||||
},
|
||||
{
|
||||
title: 'Custom nodes support',
|
||||
description:
|
||||
'Comfy Cloud currently supports a variety of the most-used custom nodes from the ComfyUI community. <a href="https://docs.comfy.org" class="text-primary-comfy-yellow underline">Check out which nodes we support currently.</a> We\'re expanding support regularly based on demand and compatibility. <a href="https://docs.comfy.org" class="text-primary-comfy-yellow underline">See which nodes we\'re working on adding.</a>'
|
||||
titleKey: 'pricing.included.feature7.title',
|
||||
descriptionKey: 'pricing.included.feature7.description'
|
||||
},
|
||||
{
|
||||
title: 'Partner Nodes',
|
||||
description:
|
||||
'Run <strong>proprietary models</strong> through Comfy\'s <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">Partner Nodes</a>, such as Nano Banana. The amount of credits each node uses depends on the model and parameters you set in the node, but these credits are the same ones that your monthly subscription comes with. These credits can also be used across Comfy Cloud and local ComfyUI. Read more about Partner nodes <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">here</a>.'
|
||||
titleKey: 'pricing.included.feature8.title',
|
||||
descriptionKey: 'pricing.included.feature8.description'
|
||||
},
|
||||
{
|
||||
title: 'Job queue',
|
||||
description: 'Queue up to 100 workflows at once.'
|
||||
titleKey: 'pricing.included.feature9.title',
|
||||
descriptionKey: 'pricing.included.feature9.description'
|
||||
},
|
||||
{
|
||||
title: 'Custom LoRA importing',
|
||||
description:
|
||||
'For those on the Creator or Pro plans, you can bring in your own models & LoRAs from CivitAI or Huggingface to perfect your own style.'
|
||||
titleKey: 'pricing.included.feature10.title',
|
||||
descriptionKey: 'pricing.included.feature10.description'
|
||||
},
|
||||
{
|
||||
title: 'Parallel job execution',
|
||||
description:
|
||||
'Run multiple workflows in parallel to speed up your pipeline.',
|
||||
titleKey: 'pricing.included.feature11.title',
|
||||
descriptionKey: 'pricing.included.feature11.description',
|
||||
isComingSoon: true
|
||||
}
|
||||
]
|
||||
@@ -85,7 +77,7 @@ const features: IncludedFeature[] = [
|
||||
<div>
|
||||
<div
|
||||
v-for="(feature, index) in features"
|
||||
:key="feature.title"
|
||||
:key="feature.titleKey"
|
||||
:class="
|
||||
index < features.length - 1
|
||||
? 'border-primary-comfy-canvas/15 border-b border-solid'
|
||||
@@ -111,13 +103,13 @@ const features: IncludedFeature[] = [
|
||||
/>
|
||||
<div>
|
||||
<p class="text-primary-comfy-canvas text-sm font-medium">
|
||||
{{ feature.title }}
|
||||
{{ t(feature.titleKey, locale) }}
|
||||
</p>
|
||||
<span
|
||||
v-if="feature.isComingSoon"
|
||||
class="text-primary-comfy-yellow mt-1 inline-block text-xs"
|
||||
>
|
||||
coming soon
|
||||
{{ t('pricing.included.comingSoon', locale) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,7 +117,7 @@ const features: IncludedFeature[] = [
|
||||
<!-- Description -->
|
||||
<p
|
||||
class="text-primary-comfy-canvas/55 mt-3 text-sm/relaxed lg:mt-0"
|
||||
v-html="feature.description"
|
||||
v-html="t(feature.descriptionKey, locale)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,17 +21,12 @@ const features = computed(() => [
|
||||
{
|
||||
title: t('enterprise.team.feature2.title', locale),
|
||||
description: t('enterprise.team.feature2.description', locale),
|
||||
image: 'https://media.comfy.org/website/enterprise/dark-fluid-texture.webp',
|
||||
ctaText: t('enterprise.team.feature2.cta', locale),
|
||||
ctaHref: routes.value.cloud
|
||||
image: 'https://media.comfy.org/website/enterprise/dark-fluid-texture.webp'
|
||||
},
|
||||
{
|
||||
title: t('enterprise.team.feature3.title', locale),
|
||||
description: t('enterprise.team.feature3.description', locale),
|
||||
image:
|
||||
'https://media.comfy.org/website/enterprise/isometric-interface.webp',
|
||||
ctaText: t('enterprise.hero.contactSales', locale),
|
||||
ctaHref: routes.value.contact
|
||||
image: 'https://media.comfy.org/website/enterprise/isometric-interface.webp'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -40,17 +40,16 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Replace with final ecosystem illustration -->
|
||||
<div
|
||||
class="rounded-4.5xl aspect-4/3 w-full flex-1 overflow-hidden bg-linear-to-b from-emerald-300 to-amber-200"
|
||||
>
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<span
|
||||
class="bg-primary-comfy-ink text-primary-comfy-yellow rounded-full px-4 py-2 text-sm font-bold"
|
||||
>
|
||||
4x
|
||||
</span>
|
||||
</div>
|
||||
<div class="rounded-4.5xl w-full flex-1 overflow-hidden">
|
||||
<video
|
||||
src="https://media.comfy.org/website/local/racer.webm"
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
aria-hidden="true"
|
||||
class="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -29,14 +29,14 @@ const {
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="flex flex-col gap-4 px-4 py-24 md:flex-row md:gap-16 md:px-20 md:py-40"
|
||||
class="flex flex-col gap-4 px-4 py-24 lg:flex-row lg:gap-16 lg:px-20 lg:py-40"
|
||||
>
|
||||
<!-- Left heading -->
|
||||
<div
|
||||
class="bg-primary-comfy-ink sticky top-20 z-10 w-full shrink-0 self-start py-4 md:top-28 md:w-115 md:py-0"
|
||||
class="bg-primary-comfy-ink sticky top-20 z-10 w-full shrink-0 self-start py-4 lg:top-28 lg:w-115 lg:py-0"
|
||||
>
|
||||
<h2
|
||||
class="text-primary-comfy-canvas text-4xl/16 font-light whitespace-pre-line md:text-5xl/16"
|
||||
class="text-primary-comfy-canvas text-4xl/16 font-light whitespace-pre-line lg:text-5xl/16"
|
||||
>
|
||||
{{ t(headingKey, locale)
|
||||
}}<span v-if="headingHighlightKey" :class="highlightClass">{{
|
||||
@@ -56,9 +56,9 @@ const {
|
||||
<div
|
||||
v-for="reason in reasons"
|
||||
:key="reason.titleKey"
|
||||
class="border-primary-comfy-canvas/20 flex flex-col gap-4 border-b py-10 first:pt-0 md:flex-row md:gap-12"
|
||||
class="border-primary-comfy-canvas/20 flex flex-col gap-4 border-b py-10 first:pt-0 lg:flex-row lg:gap-12"
|
||||
>
|
||||
<div class="shrink-0 md:w-52">
|
||||
<div class="shrink-0 lg:w-52">
|
||||
<h3
|
||||
class="text-primary-comfy-canvas text-2xl font-light whitespace-pre-line"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,9 @@ import { gsap } from '../scripts/gsapSetup'
|
||||
import { prefersReducedMotion } from './useReducedMotion'
|
||||
|
||||
interface ParallaxOptions {
|
||||
/** Vertical offset in pixels (default: 200) */
|
||||
/** Starting vertical offset in pixels (default: 0) */
|
||||
fromY?: number
|
||||
/** Ending vertical offset in pixels (default: 200) */
|
||||
y?: number
|
||||
trigger?: Ref<HTMLElement | undefined>
|
||||
/** ScrollTrigger start value (default: 'top bottom') */
|
||||
@@ -17,7 +19,7 @@ export function useParallax(
|
||||
elements: Ref<HTMLElement | undefined>[],
|
||||
options: ParallaxOptions = {}
|
||||
) {
|
||||
const { y = 200 } = options
|
||||
const { fromY = 0, y = 200 } = options
|
||||
let ctx: gsap.Context | undefined
|
||||
|
||||
onMounted(() => {
|
||||
@@ -27,19 +29,16 @@ export function useParallax(
|
||||
.filter((el): el is HTMLElement => !!el && el.offsetParent !== null)
|
||||
if (!els.length || prefersReducedMotion()) return
|
||||
|
||||
const scrollTrigger = {
|
||||
trigger: trigger ?? els[0],
|
||||
start: options.start ?? 'top bottom',
|
||||
end: options.end ?? 'bottom top',
|
||||
scrub: 1
|
||||
}
|
||||
|
||||
ctx = gsap.context(() => {
|
||||
els.forEach((el) => {
|
||||
gsap.to(el, {
|
||||
y,
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: trigger ?? el,
|
||||
start: options.start ?? 'top bottom',
|
||||
end: options.end ?? 'bottom top',
|
||||
scrub: 1,
|
||||
invalidateOnRefresh: true
|
||||
}
|
||||
})
|
||||
gsap.fromTo(el, { y: fromY }, { y, ease: 'none', scrollTrigger })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import type { ScrollTrigger } from '../scripts/gsapSetup'
|
||||
import { gsap } from '../scripts/gsapSetup'
|
||||
import { scrollTo } from '../scripts/smoothScroll'
|
||||
import { prefersReducedMotion } from './useReducedMotion'
|
||||
@@ -12,10 +13,13 @@ interface PinScrubRefs {
|
||||
|
||||
interface PinScrubOptions {
|
||||
itemCount: number
|
||||
/** Viewport-height percentage per item (default: 100) */
|
||||
/** Viewport-height percentage per item (default: 20) */
|
||||
vhPerItem?: number
|
||||
}
|
||||
|
||||
/** Viewport-height percentage each category occupies in the scroll distance. */
|
||||
export const VH_PER_ITEM = 20
|
||||
|
||||
function interpolateY(
|
||||
index: number,
|
||||
buttonCenters: number[],
|
||||
@@ -38,11 +42,11 @@ function interpolateY(
|
||||
|
||||
export function usePinScrub(refs: PinScrubRefs, options: PinScrubOptions) {
|
||||
const activeIndex = ref(0)
|
||||
const isActive = ref(false)
|
||||
const isEnabled = ref(false)
|
||||
let ctx: gsap.Context | undefined
|
||||
let scrollTriggerInstance: ScrollTrigger | undefined
|
||||
|
||||
const vhPerItem = options.vhPerItem ?? 100
|
||||
const vhPerItem = options.vhPerItem ?? VH_PER_ITEM
|
||||
|
||||
function scrollToIndex(index: number) {
|
||||
if (!scrollTriggerInstance) {
|
||||
@@ -86,45 +90,53 @@ export function usePinScrub(refs: PinScrubRefs, options: PinScrubOptions) {
|
||||
}
|
||||
|
||||
cacheLayout()
|
||||
isActive.value = true
|
||||
isEnabled.value = true
|
||||
const setContentY = gsap.quickSetter(content, 'y', 'px')
|
||||
|
||||
const proxy = { index: 0 }
|
||||
ctx = gsap.context(() => {
|
||||
gsap.to(proxy, {
|
||||
index: options.itemCount - 1,
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: section,
|
||||
start: 'top top',
|
||||
end: `+=${options.itemCount * vhPerItem}%`,
|
||||
pin: true,
|
||||
scrub: true,
|
||||
refreshPriority: 1,
|
||||
onRefresh: cacheLayout,
|
||||
onUpdate(self) {
|
||||
scrollTriggerInstance = self
|
||||
}
|
||||
},
|
||||
onUpdate() {
|
||||
activeIndex.value = Math.round(proxy.index)
|
||||
const tween = gsap.to(
|
||||
{},
|
||||
{
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: section,
|
||||
start: 'top top',
|
||||
end: `+=${options.itemCount * vhPerItem}%`,
|
||||
pin: true,
|
||||
scrub: true,
|
||||
snap: {
|
||||
snapTo: 1 / (options.itemCount - 1),
|
||||
duration: { min: 0.2, max: 0.6 },
|
||||
delay: 0.1,
|
||||
ease: 'power1.inOut'
|
||||
},
|
||||
onRefresh: cacheLayout,
|
||||
onUpdate(self) {
|
||||
const index = self.progress * (options.itemCount - 1)
|
||||
const nextActive = Math.round(index)
|
||||
|
||||
if (contentH <= vpH) {
|
||||
gsap.set(content, { y: 0 })
|
||||
return
|
||||
}
|
||||
if (nextActive !== activeIndex.value) {
|
||||
activeIndex.value = nextActive
|
||||
}
|
||||
|
||||
gsap.set(content, {
|
||||
y: interpolateY(proxy.index, buttonCenters, contentH, vpH)
|
||||
})
|
||||
if (contentH <= vpH) {
|
||||
setContentY(0)
|
||||
return
|
||||
}
|
||||
|
||||
setContentY(interpolateY(index, buttonCenters, contentH, vpH))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
scrollTriggerInstance = tween.scrollTrigger as ScrollTrigger
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
isActive.value = false
|
||||
isEnabled.value = false
|
||||
ctx?.revert()
|
||||
})
|
||||
|
||||
return { activeIndex, isActive, scrollToIndex }
|
||||
return { activeIndex, isEnabled, scrollToIndex }
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ const translations = {
|
||||
en: 'Industries that create with ComfyUI',
|
||||
'zh-CN': '使用 ComfyUI 创作的行业'
|
||||
},
|
||||
'useCase.navLabel': {
|
||||
en: 'Industry categories',
|
||||
'zh-CN': '行业分类'
|
||||
},
|
||||
'useCase.vfx': {
|
||||
en: 'VFX &\nAnimation',
|
||||
'zh-CN': '视觉特效 &\n动画'
|
||||
@@ -66,7 +70,7 @@ const translations = {
|
||||
'zh-CN': '广告 &\n创意工作室'
|
||||
},
|
||||
'useCase.gaming': {
|
||||
en: 'Video Games',
|
||||
en: 'Gaming Industry',
|
||||
'zh-CN': '电子游戏'
|
||||
},
|
||||
'useCase.ecommerce': {
|
||||
@@ -951,9 +955,9 @@ const translations = {
|
||||
'zh-CN': '我可以运行长时间或多个工作流吗?'
|
||||
},
|
||||
'cloud.faq.9.a': {
|
||||
en: "Each workflow can run for up to 30 minutes, with one active job at a time. We're adding higher tiers and parallel runs soon for even more flexibility.",
|
||||
en: "Each workflow can run for up to 60 minutes, with one active job at a time. We're adding higher tiers and parallel runs soon for even more flexibility.",
|
||||
'zh-CN':
|
||||
'每个工作流最长可运行 30 分钟,同时运行一个活跃任务。我们即将推出更高层级和并行运行,提供更大灵活性。'
|
||||
'每个工作流最长可运行 60 分钟,同时运行一个活跃任务。我们即将推出更高层级和并行运行,提供更大灵活性。'
|
||||
},
|
||||
'cloud.faq.10.q': {
|
||||
en: 'How is my user data stored and secured in Comfy Cloud?',
|
||||
@@ -1070,8 +1074,8 @@ const translations = {
|
||||
'zh-CN': '包含免费版全部能力,另加:'
|
||||
},
|
||||
'pricing.plan.standard.feature1': {
|
||||
en: '30-minute max runtime per workflow',
|
||||
'zh-CN': '单个工作流最长运行 30 分钟'
|
||||
en: '60-minute max runtime per workflow',
|
||||
'zh-CN': '单个工作流最长运行 60 分钟'
|
||||
},
|
||||
'pricing.plan.standard.feature2': {
|
||||
en: 'Add more credits anytime',
|
||||
@@ -1081,7 +1085,7 @@ const translations = {
|
||||
'pricing.plan.creator.label': { en: 'CREATOR', 'zh-CN': '创作者版' },
|
||||
'pricing.plan.creator.summary': {
|
||||
en: 'Small teams building fine-tuned, repeatable workflows',
|
||||
'zh-CN': '面向专业人士与小团队,构建可复用、精细调优的工作流'
|
||||
'zh-CN': '小团队构建精细调优、可复用的工作流'
|
||||
},
|
||||
'pricing.plan.creator.price': { en: '$35', 'zh-CN': '$35' },
|
||||
'pricing.plan.creator.credits': {
|
||||
@@ -1146,7 +1150,7 @@ const translations = {
|
||||
en: 'For teams running Comfy in production, and at scale.',
|
||||
'zh-CN': '面向在生产环境和规模化场景中运行 Comfy 的团队。'
|
||||
},
|
||||
'pricing.enterprise.cta': { en: 'LEARN MORE', 'zh-CN': '联系我们' },
|
||||
'pricing.enterprise.cta': { en: 'LEARN MORE', 'zh-CN': '了解更多' },
|
||||
'pricing.enterprise.featureIntro': {
|
||||
en: 'Everything in Pro, plus:',
|
||||
'zh-CN': '包含专业版全部能力,另加:'
|
||||
@@ -1178,6 +1182,105 @@ const translations = {
|
||||
en: "What's included\nin the Comfy plan",
|
||||
'zh-CN': 'Comfy 计划\n包含哪些内容'
|
||||
},
|
||||
'pricing.included.feature1.title': {
|
||||
en: 'Machine Setup',
|
||||
'zh-CN': '机器配置'
|
||||
},
|
||||
'pricing.included.feature1.description': {
|
||||
en: 'Comfy Cloud runs on Blackwell RTX 6000 Pro – 96GB VRAM',
|
||||
'zh-CN': 'Comfy Cloud 运行在 Blackwell RTX 6000 Pro 上,配备 96GB 显存'
|
||||
},
|
||||
'pricing.included.feature2.title': {
|
||||
en: 'Time limit per job',
|
||||
'zh-CN': '单个任务时限'
|
||||
},
|
||||
'pricing.included.feature2.description': {
|
||||
en: 'Each workflow run has a maximum duration of 60 minutes. On the Pro plan, the time limit is increased to 1 hour. Jobs exceeding that limit are automatically cancelled to ensure fair usage and system stability.',
|
||||
'zh-CN':
|
||||
'每个工作流运行最长为 60 分钟。Pro 计划的时限可延长至 1 小时。超时任务将自动取消,以确保公平使用和系统稳定。'
|
||||
},
|
||||
'pricing.included.feature3.title': {
|
||||
en: 'Usage',
|
||||
'zh-CN': '用量计费'
|
||||
},
|
||||
'pricing.included.feature3.description': {
|
||||
en: "You're only charged for <strong>active GPU</strong> time while a workflow is running. Idle time (e.g. time spent building workflows) does not consume GPU hours.",
|
||||
'zh-CN':
|
||||
'仅在工作流运行期间按<strong>实际 GPU</strong> 使用时长计费。空闲时间(如构建工作流)不消耗 GPU 时长。'
|
||||
},
|
||||
'pricing.included.feature4.title': {
|
||||
en: 'Credit balance',
|
||||
'zh-CN': '积分余额'
|
||||
},
|
||||
'pricing.included.feature4.description': {
|
||||
en: 'All plans will include a monthly pool of credits that are spent on active workflow runtime and <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">Partner Nodes</a> like Nano Banana Pro.',
|
||||
'zh-CN':
|
||||
'所有计划均包含每月积分池,可用于工作流运行和<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">合作节点</a>(如 Nano Banana Pro)。'
|
||||
},
|
||||
'pricing.included.feature5.title': {
|
||||
en: 'Add more credits anytime',
|
||||
'zh-CN': '随时加购积分'
|
||||
},
|
||||
'pricing.included.feature5.description': {
|
||||
en: 'Purchase additional credits at any time. Unused top-ups roll over to the next month automatically for up to 1 year.',
|
||||
'zh-CN':
|
||||
'可随时购买额外积分。未使用的充值积分自动结转至下月,最长保留 1 年。'
|
||||
},
|
||||
'pricing.included.feature6.title': {
|
||||
en: 'Pre-installed models',
|
||||
'zh-CN': '预装模型'
|
||||
},
|
||||
'pricing.included.feature6.description': {
|
||||
en: 'Access a library of 900+ pre-installed models.',
|
||||
'zh-CN': '可访问 900+ 预装模型库。'
|
||||
},
|
||||
'pricing.included.feature7.title': {
|
||||
en: 'Custom nodes support',
|
||||
'zh-CN': '自定义节点支持'
|
||||
},
|
||||
'pricing.included.feature7.description': {
|
||||
en: "Comfy Cloud currently supports a variety of the most-used custom nodes from the ComfyUI community. We're expanding support regularly based on demand and compatibility.",
|
||||
'zh-CN':
|
||||
'Comfy Cloud 目前支持 ComfyUI 社区中最常用的多种自定义节点,并根据需求和兼容性持续扩展支持范围。'
|
||||
},
|
||||
'pricing.included.feature8.title': {
|
||||
en: 'Partner Nodes',
|
||||
'zh-CN': '合作节点'
|
||||
},
|
||||
'pricing.included.feature8.description': {
|
||||
en: 'Run <strong>proprietary models</strong> through Comfy\'s <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">Partner Nodes</a>, such as Nano Banana. The amount of credits each node uses depends on the model and parameters you set in the node, but these credits are the same ones that your monthly subscription comes with. These credits can also be used across Comfy Cloud and local ComfyUI. Read more about Partner nodes <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">here</a>.',
|
||||
'zh-CN':
|
||||
'通过 Comfy 的<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">合作节点</a>运行<strong>专有模型</strong>,如 Nano Banana。每个节点消耗的积分取决于所用模型和参数设置,且与月度订阅积分通用。积分可在 Comfy Cloud 和本地 ComfyUI 间通用。了解更多关于合作节点的信息请点击<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">此处</a>。'
|
||||
},
|
||||
'pricing.included.feature9.title': {
|
||||
en: 'Job queue',
|
||||
'zh-CN': '任务队列'
|
||||
},
|
||||
'pricing.included.feature9.description': {
|
||||
en: 'Queue up to 100 workflows at once.',
|
||||
'zh-CN': '可同时排队最多 100 个工作流。'
|
||||
},
|
||||
'pricing.included.feature10.title': {
|
||||
en: 'Custom LoRA importing',
|
||||
'zh-CN': '自定义 LoRA 导入'
|
||||
},
|
||||
'pricing.included.feature10.description': {
|
||||
en: 'For those on the Creator or Pro plans, you can bring in your own models & LoRAs from CivitAI or Huggingface to perfect your own style.',
|
||||
'zh-CN':
|
||||
'Creator 或 Pro 计划用户可从 CivitAI 或 Huggingface 导入自己的模型和 LoRA,打造专属风格。'
|
||||
},
|
||||
'pricing.included.feature11.title': {
|
||||
en: 'Parallel job execution',
|
||||
'zh-CN': '并行任务执行'
|
||||
},
|
||||
'pricing.included.feature11.description': {
|
||||
en: 'Run multiple workflows in parallel to speed up your pipeline.',
|
||||
'zh-CN': '并行运行多个工作流,加速你的流程。'
|
||||
},
|
||||
'pricing.included.comingSoon': {
|
||||
en: 'coming soon',
|
||||
'zh-CN': '即将推出'
|
||||
},
|
||||
|
||||
// VideoPlayer
|
||||
'player.play': { en: 'Play', 'zh-CN': '播放' },
|
||||
@@ -1212,10 +1315,10 @@ const translations = {
|
||||
// ContactSection
|
||||
'gallery.contact.label': { en: 'CONTACT', 'zh-CN': '联系' },
|
||||
'gallery.contact.heading': {
|
||||
en: 'Questions? Reach out!',
|
||||
'zh-CN': '有精彩作品想要分享?\n联系我们。'
|
||||
en: 'Built something cool with ComfyUI? <a href="https://docs.google.com/forms/d/1B6_RPQfhTyKvqHk9OO2bUn8z1Qgh6QIZsF3GNMiCXDw/preview" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">Submit</a> your work to be featured on our website and socials and get seen by the global ComfyUI community.',
|
||||
'zh-CN':
|
||||
'用 ComfyUI 创作了很酷的作品?<a href="https://docs.google.com/forms/d/1B6_RPQfhTyKvqHk9OO2bUn8z1Qgh6QIZsF3GNMiCXDw/preview" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">提交</a>你的作品,展示在我们的网站和社交媒体上,让全球 ComfyUI 社区看到。'
|
||||
},
|
||||
'gallery.contact.cta': { en: 'Contact us', 'zh-CN': '联系我们' },
|
||||
|
||||
// AboutHeroSection
|
||||
'about.hero.label': { en: 'ABOUT', 'zh-CN': '关于' },
|
||||
@@ -1353,7 +1456,7 @@ const translations = {
|
||||
},
|
||||
|
||||
// CareersHeroSection
|
||||
'careers.hero.label': { en: 'CAREERS', 'zh-CN': 'CAREERS' },
|
||||
'careers.hero.label': { en: 'CAREERS', 'zh-CN': '招聘' },
|
||||
'careers.hero.heading': {
|
||||
en: 'Building an operating\nsystem for Gen AI',
|
||||
'zh-CN': '构建生成式 AI 的\n\u201C操作系统\u201D'
|
||||
@@ -1361,7 +1464,7 @@ const translations = {
|
||||
'careers.hero.body1': {
|
||||
en: "We're building the world's leading visual AI platform \u2014 open, modular, and designed for those who want control, quality and scale in their creative process.",
|
||||
'zh-CN':
|
||||
'我们是全球领先的视觉 AI 平台\u2014\u2014一个开放、模块化的系统,任何人都可以精确地构建、定制和自动化 AI 工作流,并拥有完全的控制权。'
|
||||
'我们正在构建全球领先的视觉 AI 平台——开放、模块化,专为追求创作过程中的控制力、品质和规模化的人而设计。'
|
||||
},
|
||||
'careers.hero.body2': {
|
||||
en: 'From solo creators to enterprise teams, millions of people rely on ComfyUI to push the boundaries of what creative AI can do.',
|
||||
@@ -2078,6 +2181,13 @@ const translations = {
|
||||
'zh-CN':
|
||||
'从独立艺术家到全球工作室——构建视觉媒体未来的团队都在使用 ComfyUI。'
|
||||
},
|
||||
'customers.contact.label': { en: 'CONTACT', 'zh-CN': '联系' },
|
||||
'customers.contact.heading': {
|
||||
en: 'Interested in a case study with ComfyUI? Reach out <a href="https://docs.google.com/forms/d/e/1FAIpQLSd-Keeq1VIePeanQIsdHq9eYeDE82MHJTdvwdgpxCoEzo_CUg/viewform" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">here</a>',
|
||||
'zh-CN':
|
||||
'有兴趣与 ComfyUI 合作案例研究?点击<a href="https://docs.google.com/forms/d/e/1FAIpQLSd-Keeq1VIePeanQIsdHq9eYeDE82MHJTdvwdgpxCoEzo_CUg/viewform" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">此处</a>联系我们'
|
||||
},
|
||||
|
||||
'customers.story.series-entertainment.category': {
|
||||
en: 'GAME & VIDEO PRODUCTION',
|
||||
'zh-CN': '游戏与视频制作'
|
||||
|
||||
@@ -57,7 +57,7 @@ const websiteJsonLd = {
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={locale}>
|
||||
<html lang={locale} class="overflow-x-clip">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import HeroSection from '../components/customers/HeroSection.vue'
|
||||
import StorySection from '../components/customers/StorySection.vue'
|
||||
import FeedbackSection from '../components/customers/FeedbackSection.vue'
|
||||
import VideoSection from '../components/customers/VideoSection.vue'
|
||||
import ContactSection from '../components/customers/ContactSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="Customer Stories — Comfy">
|
||||
@@ -11,4 +12,5 @@ import VideoSection from '../components/customers/VideoSection.vue'
|
||||
<StorySection />
|
||||
<FeedbackSection client:load />
|
||||
<VideoSection client:load />
|
||||
<ContactSection />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -5,12 +5,20 @@ import HeroSection from '../../../components/product/enterprise/HeroSection.vue'
|
||||
import SocialProofBarSection from '../../../components/common/SocialProofBarSection.vue'
|
||||
import ReasonSection from '../../../components/product/enterprise/ReasonSection.vue'
|
||||
import TeamSection from '../../../components/product/enterprise/TeamSection.vue'
|
||||
import DataOwnershipSection from '../../../components/product/enterprise/DataOwnershipSection.vue'
|
||||
import BYOKeySection from '../../../components/product/enterprise/BYOKeySection.vue'
|
||||
import OrchestrationSection from '../../../components/product/enterprise/OrchestrationSection.vue'
|
||||
import ProductCardsSection from '../../../components/common/ProductCardsSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="Comfy Enterprise">
|
||||
<BaseLayout title="Comfy 企业版">
|
||||
<CloudBannerSection locale="zh-CN" />
|
||||
<HeroSection locale="zh-CN" client:load />
|
||||
<SocialProofBarSection />
|
||||
<ReasonSection locale="zh-CN" />
|
||||
<TeamSection locale="zh-CN" />
|
||||
<DataOwnershipSection locale="zh-CN" client:visible />
|
||||
<BYOKeySection locale="zh-CN" />
|
||||
<OrchestrationSection locale="zh-CN" client:visible />
|
||||
<ProductCardsSection locale="zh-CN" exclude-product="enterprise" label-key="products.labelProducts" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -4,6 +4,7 @@ import HeroSection from '../../components/customers/HeroSection.vue'
|
||||
import StorySection from '../../components/customers/StorySection.vue'
|
||||
import FeedbackSection from '../../components/customers/FeedbackSection.vue'
|
||||
import VideoSection from '../../components/customers/VideoSection.vue'
|
||||
import ContactSection from '../../components/customers/ContactSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="客户故事 — Comfy">
|
||||
@@ -11,4 +12,5 @@ import VideoSection from '../../components/customers/VideoSection.vue'
|
||||
<StorySection locale="zh-CN" />
|
||||
<FeedbackSection locale="zh-CN" client:load />
|
||||
<VideoSection locale="zh-CN" client:load />
|
||||
<ContactSection locale="zh-CN" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,39 +1,12 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import HeroSection from '../../components/gallery/HeroSection.vue'
|
||||
import GallerySection from '../../components/gallery/GallerySection.vue'
|
||||
import ContactSection from '../../components/gallery/ContactSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="作品集 — Comfy">
|
||||
<div class="bg-black text-white">
|
||||
<!-- Hero -->
|
||||
<section class="mx-auto max-w-5xl px-6 pb-16 pt-32 text-center">
|
||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
在 ComfyUI 中<span class="text-brand-yellow">构建、调整与梦想</span>
|
||||
</h1>
|
||||
<p class="mx-auto mt-4 max-w-2xl text-lg text-smoke-700">
|
||||
社区使用 ComfyUI 创作的精彩作品一瞥。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Placeholder Grid -->
|
||||
<section class="mx-auto max-w-6xl px-6 pb-24">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map(() => (
|
||||
<div class="flex aspect-video items-center justify-center rounded-xl border border-white/10 bg-charcoal-600">
|
||||
<p class="text-sm text-smoke-700">社区展示即将上线</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="mx-auto max-w-3xl px-6 pb-32 text-center">
|
||||
<h2 class="text-2xl font-semibold">有很酷的作品想分享?</h2>
|
||||
<a
|
||||
href="https://support.comfy.org/"
|
||||
class="mt-6 inline-block rounded-full bg-brand-yellow px-8 py-3 font-medium text-black transition hover:opacity-90"
|
||||
>
|
||||
联系我们
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
<HeroSection locale="zh-CN" />
|
||||
<GallerySection locale="zh-CN" client:load />
|
||||
<ContactSection locale="zh-CN" />
|
||||
</BaseLayout>
|
||||
|
||||