Compare commits

...

14 Commits

Author SHA1 Message Date
Yourz
a97f38d980 update: code quality 2026-04-23 23:13:09 +08:00
Yourz
7a873e95ca update: local EcoSystemSection visual 2026-04-23 22:59:32 +08:00
Yourz
f0eac7e019 update: content 2026-04-23 22:14:12 +08:00
github-actions
c6551430ce [automated] Update website screenshot expectations 2026-04-23 08:12:05 +00:00
DrJKL
40a448ea31 feat: refactor UseCaseSection scroll system and add keyboard navigation
- Reduce scroll distance from 500vh to 100vh (20vh per item)
- Kill proxy tween pattern, derive index from ScrollTrigger progress
- Use gsap.quickSetter for content Y performance
- Add scroll snap so categories settle on stop
- Simplify useParallax to always use gsap.fromTo
- Extract VH_PER_ITEM constant, rename isActive to isEnabled
- Add arrow key navigation between categories
- Remove focus outline (color change indicates state)

Amp-Thread-ID: https://ampcode.com/threads/T-019db918-310f-755d-9d8c-f7c7ffae6f32
Co-authored-by: Amp <amp@ampcode.com>
2026-04-23 00:43:22 -07:00
DrJKL
151bf78f9f fix: move GitHubStarBadge into desktop CTA container
Amp-Thread-ID: https://ampcode.com/threads/T-019db90c-a666-706b-80d3-6d9e6be73fd2
Co-authored-by: Amp <amp@ampcode.com>
2026-04-22 23:35:34 -07:00
DrJKL
26a5d3cf7d fix: restore paginate in upsert-comment-section
per_page:100 misses the bot comment on PRs with >100 comments,
causing duplicates. Restore github.paginate to search all pages.

Amp-Thread-ID: https://ampcode.com/threads/T-019db842-342c-7307-ae60-14e6f3a24b8e
Co-authored-by: Amp <amp@ampcode.com>
2026-04-22 23:32:52 -07:00
Alexander Brown
27cfd0c6db Merge branch 'main' into feat/more-website-fun 2026-04-22 22:51:50 -07:00
DrJKL
50cb16fb07 fix: harden website CI workflows from PR review
- Add explicit permissions (contents:write, pull-requests:read) to
  update-screenshots job
- Move all expression interpolations in github-script blocks to env
  vars to prevent expression injection
- Move permissions from workflow-level to job-level in preview workflow
- Rename post-status-comment to post-result-comment for consistency
- Rename generic 'Build section content' steps to be workflow-specific
- Replace paginate with listComments + per_page:100
- Guard against null comment body with ?? ''
- Document comment-marker, section names, escapeRegex, and is-pr output

Amp-Thread-ID: https://ampcode.com/threads/T-019db842-342c-7307-ae60-14e6f3a24b8e
Co-authored-by: Amp <amp@ampcode.com>
2026-04-22 22:49:44 -07:00
DrJKL
344ae10d77 chore: document race conditions, fork-PR safety, and duplicated conditions
Amp-Thread-ID: https://ampcode.com/threads/T-019db83a-5aa6-7314-9791-c1fab5c7cd25
Co-authored-by: Amp <amp@ampcode.com>
2026-04-22 19:53:12 -07:00
DrJKL
f17dd9de32 feat: consolidate website PR comments into single comment with sections
Add upsert-comment-section composite action that manages a single PR
comment with independently-updatable sections via HTML comment markers.

Refactor ci-website-e2e, pr-vercel-website-preview, and
pr-update-website-screenshots to all write to one shared comment
(<!-- WEBSITE_CI_REPORT -->) with dedicated sections for preview,
e2e results, and screenshot updates.

Comment-posting jobs use non-canceling concurrency groups to serialize
writes and avoid race conditions.

Amp-Thread-ID: https://ampcode.com/threads/T-019db82c-211a-7060-98f7-ddcf135efe8d
Co-authored-by: Amp <amp@ampcode.com>
2026-04-22 19:44:38 -07:00
DrJKL
d8b416f7a8 fix: resolve horizontal overflow on /cloud and /download pages
- Add overflow-x-clip to html element so root scroll container clips
  hero SVG bleed without hiding the visuals
- Bump ReasonSection two-column layout from md to lg breakpoint to
  prevent overflow at 768px
- Remove OVERFLOW_SKIP blacklist from e2e overflow guard tests

Amp-Thread-ID: https://ampcode.com/threads/T-019db817-69ae-777e-9a2c-7351ca612343
Co-authored-by: Amp <amp@ampcode.com>
2026-04-22 19:26:37 -07:00
DrJKL
664f407d6e fix: remove placeholder doc links from custom nodes feature description
Amp-Thread-ID: https://ampcode.com/threads/T-019db806-658d-7698-9b6d-61cc3bd216e5
Co-authored-by: Amp <amp@ampcode.com>
2026-04-22 18:51:38 -07:00
DrJKL
1f69c5ce86 chore: open draft PR
Amp-Thread-ID: https://ampcode.com/threads/T-019db806-658d-7698-9b6d-61cc3bd216e5
Co-authored-by: Amp <amp@ampcode.com>
2026-04-22 18:49:29 -07:00
27 changed files with 628 additions and 256 deletions

View 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
})

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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>

View File

@@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -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"

View 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>

View File

@@ -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>

View File

@@ -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'

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"
>

View File

@@ -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 })
})
})
})

View File

@@ -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 }
}

View File

@@ -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': '游戏与视频制作'

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>