mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 17:28:58 +00:00
Compare commits
1 Commits
codex/part
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27859cb41c |
@@ -1,9 +1,20 @@
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
const MCP_ENDPOINT = 'https://cloud.comfy.org/mcp'
|
||||
|
||||
// The setup island hydrates on visibility; clicks before hydration are
|
||||
// no-ops, so retry until the tab actually activates.
|
||||
async function selectClientTab(setup: Locator, name: string) {
|
||||
const tab = setup.getByRole('tab', { name })
|
||||
await expect(async () => {
|
||||
await tab.click()
|
||||
await expect(tab).toHaveAttribute('data-state', 'active', { timeout: 500 })
|
||||
}).toPass()
|
||||
}
|
||||
|
||||
test.describe('MCP page @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/mcp')
|
||||
@@ -19,46 +30,72 @@ test.describe('MCP page @smoke', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('setup section shows both install options', async ({ page }) => {
|
||||
test('Claude Desktop is the default tab and shows only the connector card', async ({
|
||||
page
|
||||
}) => {
|
||||
const setup = page.locator('#setup')
|
||||
await setup.scrollIntoViewIfNeeded()
|
||||
await expect(
|
||||
setup.getByRole('tab', { name: 'Claude Desktop' })
|
||||
).toHaveAttribute('data-state', 'active')
|
||||
await expect(
|
||||
setup.getByRole('heading', { name: 'Add Custom Connector' })
|
||||
).toBeVisible()
|
||||
await expect(setup.getByText(MCP_ENDPOINT, { exact: true })).toBeVisible()
|
||||
await expect(
|
||||
setup.getByRole('heading', {
|
||||
name: 'Ask your agent to install Comfy MCP'
|
||||
})
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
setup.getByRole('heading', { name: 'Install manually' })
|
||||
).toBeVisible()
|
||||
await expect(setup.getByText(MCP_ENDPOINT, { exact: true })).toBeVisible()
|
||||
).toHaveCount(0)
|
||||
await expect(setup.locator('video')).toBeVisible()
|
||||
})
|
||||
|
||||
test('client tabs swap install instructions', async ({ page }) => {
|
||||
test('client tabs swap install instructions and agent-card visibility', async ({
|
||||
page
|
||||
}) => {
|
||||
const setup = page.locator('#setup')
|
||||
await setup.scrollIntoViewIfNeeded()
|
||||
const activePanel = setup.locator('[role="tabpanel"][data-state="active"]')
|
||||
const agentHeading = setup.getByRole('heading', {
|
||||
name: 'Ask your agent to install Comfy MCP'
|
||||
})
|
||||
|
||||
// Claude Code is the default tab and carries the CLI command
|
||||
await expect(
|
||||
setup.getByRole('tab', { name: 'Claude Code' })
|
||||
).toHaveAttribute('data-state', 'active')
|
||||
await expect(activePanel).toContainText('Add custom connector')
|
||||
|
||||
// First interaction retries until the island hydrates; later switches
|
||||
// assert synchronously so steady-state click regressions fail.
|
||||
await selectClientTab(setup, 'Claude Code Terminal')
|
||||
await expect(activePanel).toContainText(
|
||||
`claude mcp add --transport http comfy-cloud ${MCP_ENDPOINT}`
|
||||
)
|
||||
await expect(
|
||||
setup.getByRole('heading', { name: 'Install manually' })
|
||||
).toBeVisible()
|
||||
await expect(agentHeading).toBeVisible()
|
||||
|
||||
await setup.getByRole('tab', { name: 'Claude Desktop' }).click()
|
||||
await expect(activePanel).toContainText('Add custom connector')
|
||||
await setup.getByRole('tab', { name: 'Codex' }).click()
|
||||
await expect(activePanel).toContainText(
|
||||
`codex mcp add comfy-cloud --url ${MCP_ENDPOINT}`
|
||||
)
|
||||
await expect(agentHeading).toHaveCount(0)
|
||||
await expect(setup.locator('video')).toBeVisible()
|
||||
|
||||
await setup.getByRole('tab', { name: 'Cursor' }).click()
|
||||
await expect(activePanel).toContainText('X-API-Key')
|
||||
await expect(
|
||||
activePanel.getByRole('link', { name: 'platform.comfy.org' })
|
||||
).toHaveAttribute('href', 'https://platform.comfy.org/profile/api-keys')
|
||||
await expect(agentHeading).toBeVisible()
|
||||
|
||||
await setup.getByRole('tab', { name: 'Codex' }).click()
|
||||
await setup.getByRole('tab', { name: 'OpenClaw' }).click()
|
||||
await expect(activePanel).toContainText(
|
||||
`codex mcp add comfy-cloud --url ${MCP_ENDPOINT}`
|
||||
'openclaw skills install @comfy-org/comfy'
|
||||
)
|
||||
await expect(agentHeading).toBeVisible()
|
||||
|
||||
await setup.getByRole('tab', { name: 'Others' }).click()
|
||||
await expect(activePanel).toContainText('remote MCP server')
|
||||
await expect(agentHeading).toBeVisible()
|
||||
})
|
||||
|
||||
test('skills plugin link lives in the agent option card', async ({
|
||||
@@ -66,6 +103,7 @@ test.describe('MCP page @smoke', () => {
|
||||
}) => {
|
||||
const setup = page.locator('#setup')
|
||||
await setup.scrollIntoViewIfNeeded()
|
||||
await selectClientTab(setup, 'Claude Code Terminal')
|
||||
await expect(
|
||||
setup.getByRole('link', { name: 'View on GitHub' })
|
||||
).toHaveAttribute('href', 'https://github.com/Comfy-Org/comfy-skills')
|
||||
@@ -105,7 +143,10 @@ test.describe('MCP page zh-CN @smoke', () => {
|
||||
await page.goto('/zh-CN/mcp')
|
||||
const setup = page.locator('#setup')
|
||||
await setup.scrollIntoViewIfNeeded()
|
||||
await expect(setup.getByText('方式一')).toBeVisible()
|
||||
await expect(
|
||||
setup.getByRole('heading', { name: '添加自定义连接器' })
|
||||
).toBeVisible()
|
||||
await selectClientTab(setup, 'Claude Code Terminal')
|
||||
await expect(setup.getByRole('heading', { name: '手动安装' })).toBeVisible()
|
||||
await expect(setup.getByText(MCP_ENDPOINT, { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
BIN
apps/website/public/videos/mcp/setup-claude-desktop-v1.mp4
Normal file
BIN
apps/website/public/videos/mcp/setup-claude-desktop-v1.mp4
Normal file
Binary file not shown.
BIN
apps/website/public/videos/mcp/setup-codex-oauth-v1.mp4
Normal file
BIN
apps/website/public/videos/mcp/setup-codex-oauth-v1.mp4
Normal file
Binary file not shown.
@@ -33,6 +33,7 @@ const {
|
||||
minimal = false,
|
||||
hideControls = false,
|
||||
fit = 'cover',
|
||||
ariaLabel,
|
||||
class: className
|
||||
} = defineProps<{
|
||||
locale?: Locale
|
||||
@@ -44,6 +45,7 @@ const {
|
||||
minimal?: boolean
|
||||
hideControls?: boolean
|
||||
fit?: 'cover' | 'contain'
|
||||
ariaLabel?: string
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
@@ -93,6 +95,28 @@ watch(videoEl, syncNativeDuration)
|
||||
useEventListener(videoEl, 'loadedmetadata', syncNativeDuration)
|
||||
useEventListener(videoEl, 'durationchange', syncNativeDuration)
|
||||
|
||||
// The muted attribute only sets defaultMuted, so SSR-rendered autoplay
|
||||
// videos count as unmuted and get blocked; force the property and kick
|
||||
// playback. Scoped to hideControls (decorative) clips so chrome-visible
|
||||
// consumers keep native semantics. flush: 'post' guarantees this runs
|
||||
// after useMediaControls' internal muted watcher on the same source.
|
||||
watch(
|
||||
[videoEl, () => src],
|
||||
([el]) => {
|
||||
if (!el || !autoplay || !hideControls) return
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
el.pause()
|
||||
return
|
||||
}
|
||||
el.muted = true
|
||||
el.play().catch((error: unknown) => {
|
||||
if (error instanceof Error && error.name === 'AbortError') return
|
||||
console.warn('VideoPlayer autoplay failed', error)
|
||||
})
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
const effectiveDuration = computed(() => duration.value || nativeDuration.value)
|
||||
|
||||
// Scrubber (modeled after VueUse demo Scrubber.vue)
|
||||
@@ -207,6 +231,7 @@ function toggleFullscreen() {
|
||||
<video
|
||||
v-if="src"
|
||||
ref="videoEl"
|
||||
:aria-label="ariaLabel"
|
||||
:class="
|
||||
cn('size-full', fit === 'contain' ? 'object-contain' : 'object-cover')
|
||||
"
|
||||
|
||||
@@ -1919,42 +1919,48 @@ const translations = {
|
||||
'zh-CN': '配置 Comfy MCP'
|
||||
},
|
||||
'mcp.setup.subtitle': {
|
||||
en: 'Two ways to connect: ask your agent to install it, or add the server yourself. Sign in once, and the full ComfyUI toolset is available right in your chat.',
|
||||
en: 'Two ways to connect: add the server yourself, or ask your agent to install it. Sign in once, and the full ComfyUI toolset is available right in your chat.',
|
||||
'zh-CN':
|
||||
'两种接入方式:让你的智能体自动安装,或自行添加服务器。登录一次,ComfyUI 全套工具即可直接在对话中使用。'
|
||||
'两种接入方式:自行添加服务器,或让你的智能体自动安装。登录一次,ComfyUI 全套工具即可直接在对话中使用。'
|
||||
},
|
||||
'mcp.setup.option1.label': { en: 'OPTION 1', 'zh-CN': '方式一' },
|
||||
'mcp.setup.option1.title': {
|
||||
en: 'Ask your agent to install Comfy MCP',
|
||||
'zh-CN': '让你的智能体安装 Comfy MCP'
|
||||
},
|
||||
'mcp.setup.option1.command': {
|
||||
en: 'Help me install Comfy MCP.\nFollow the setup guide at {url}',
|
||||
'zh-CN': '帮我安装 Comfy MCP。\n请按照 {url} 上的设置指南操作。'
|
||||
},
|
||||
'mcp.setup.option1.description': {
|
||||
en: 'Paste this into Claude, Cursor, Codex, or any MCP-compatible agent. It reads the docs and adds the connector for you.',
|
||||
'zh-CN':
|
||||
'将它粘贴到 Claude、Cursor、Codex 或任意兼容 MCP 的智能体中。它会读取文档并为你添加连接器。'
|
||||
},
|
||||
'mcp.setup.option2.label': { en: 'OPTION 2', 'zh-CN': '方式二' },
|
||||
'mcp.setup.option2.title': {
|
||||
'mcp.setup.manual.title': {
|
||||
en: 'Install manually',
|
||||
'zh-CN': '手动安装'
|
||||
},
|
||||
'mcp.setup.option2.description': {
|
||||
en: 'Prefer manual setup? Add this URL as a custom connector or remote MCP server in your client, then sign in when prompted.',
|
||||
'mcp.setup.manual.description': {
|
||||
en: 'Add this URL as a custom connector or remote MCP server in your client, then sign in when prompted.',
|
||||
'zh-CN':
|
||||
'想手动配置?将此 URL 添加为客户端的自定义连接器或远程 MCP 服务器,然后按提示登录。'
|
||||
'将此 URL 添加为客户端的自定义连接器或远程 MCP 服务器,然后按提示登录。'
|
||||
},
|
||||
'mcp.setup.option2.tabsLabel': {
|
||||
'mcp.setup.manual.tabsLabel': {
|
||||
en: 'Pick your client',
|
||||
'zh-CN': '选择你的客户端'
|
||||
},
|
||||
'mcp.setup.agent.title': {
|
||||
en: 'Ask your agent to install Comfy MCP',
|
||||
'zh-CN': '让你的智能体安装 Comfy MCP'
|
||||
},
|
||||
'mcp.setup.agent.command': {
|
||||
en: 'Help me install Comfy MCP.\nFollow the setup guide at {url}',
|
||||
'zh-CN': '帮我安装 Comfy MCP。\n请按照 {url} 上的设置指南操作。'
|
||||
},
|
||||
'mcp.setup.agent.description': {
|
||||
en: 'Prefer to let your agent do it? Paste this into Claude, Cursor, Codex, or any MCP-compatible agent. It reads the docs and adds the connector for you.',
|
||||
'zh-CN':
|
||||
'想让智能体代劳?将它粘贴到 Claude、Cursor、Codex 或任意兼容 MCP 的智能体中。它会读取文档并为你添加连接器。'
|
||||
},
|
||||
'mcp.setup.clients.claudeCode.step': {
|
||||
en: 'Run this in your terminal, then use /mcp to pick comfy-cloud and authenticate.',
|
||||
'zh-CN': '在终端运行以下命令,然后通过 /mcp 选择 comfy-cloud 并完成认证。'
|
||||
},
|
||||
'mcp.setup.walkthroughAlt': {
|
||||
en: '{client} setup walkthrough',
|
||||
'zh-CN': '{client} 设置演示'
|
||||
},
|
||||
'mcp.setup.clients.claudeDesktop.manualTitle': {
|
||||
en: 'Add Custom Connector',
|
||||
'zh-CN': '添加自定义连接器'
|
||||
},
|
||||
'mcp.setup.clients.claudeDesktop.step': {
|
||||
en: 'Click Customize in the sidebar, open Connectors, choose Add custom connector, paste the URL above, and sign in.',
|
||||
'zh-CN':
|
||||
@@ -1973,9 +1979,13 @@ const translations = {
|
||||
en: 'Run this in your terminal, then codex mcp login comfy-cloud to sign in.',
|
||||
'zh-CN': '在终端运行以下命令,然后执行 codex mcp login comfy-cloud 登录。'
|
||||
},
|
||||
'mcp.setup.clients.openclaw.step': {
|
||||
en: 'Run these in your terminal, then openclaw mcp login comfy to sign in.',
|
||||
'zh-CN': '在终端运行以下命令,然后执行 openclaw mcp login comfy 登录。'
|
||||
},
|
||||
'mcp.setup.clients.other.name': {
|
||||
en: 'Other clients',
|
||||
'zh-CN': '其他客户端'
|
||||
en: 'Others',
|
||||
'zh-CN': '其他'
|
||||
},
|
||||
'mcp.setup.clients.other.step': {
|
||||
en: 'Add the URL above as a remote MCP server. No OAuth in your client? Use an X-API-Key header instead. Full walkthroughs live in the ',
|
||||
@@ -2190,9 +2200,9 @@ const translations = {
|
||||
'zh-CN': '我需要 API 密钥吗?'
|
||||
},
|
||||
'mcp.faq.3.a': {
|
||||
en: 'Not for Claude Code, Claude Desktop, or Codex. You need a Comfy API key for Cursor, Hermes, and OpenClaw for now. Just copy https://docs.comfy.org/agent-tools/cloud and your agent will figure out the installation for you.',
|
||||
en: 'Not for Claude Code, Claude Desktop, Codex, or OpenClaw. You need a Comfy API key for Cursor and Hermes for now. Just copy https://docs.comfy.org/agent-tools/cloud and your agent will figure out the installation for you.',
|
||||
'zh-CN':
|
||||
'Claude Code、Claude Desktop 和 Codex 不需要。Cursor、Hermes 和 OpenClaw 目前需要 Comfy API 密钥。只需复制 https://docs.comfy.org/agent-tools/cloud,你的智能体就会为你完成安装。'
|
||||
'Claude Code、Claude Desktop、Codex 和 OpenClaw 不需要。Cursor 和 Hermes 目前需要 Comfy API 密钥。只需复制 https://docs.comfy.org/agent-tools/cloud,你的智能体就会为你完成安装。'
|
||||
},
|
||||
'mcp.faq.4.q': {
|
||||
en: 'Does it cost anything?',
|
||||
|
||||
@@ -78,3 +78,28 @@ describe('captureDownloadClick', () => {
|
||||
expect(hoisted.mockCapture).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('captureMcpClientTabClick', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('captures the tab click with the client id', async () => {
|
||||
const { initPostHog, captureMcpClientTabClick } = await import('./posthog')
|
||||
initPostHog()
|
||||
captureMcpClientTabClick('claude-code')
|
||||
|
||||
expect(hoisted.mockCapture).toHaveBeenCalledWith(
|
||||
'website:mcp_client_tab_clicked',
|
||||
{ client: 'claude-code' }
|
||||
)
|
||||
})
|
||||
|
||||
it('does not capture before PostHog is initialized', async () => {
|
||||
const { captureMcpClientTabClick } = await import('./posthog')
|
||||
captureMcpClientTabClick('cursor')
|
||||
|
||||
expect(hoisted.mockCapture).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -49,3 +49,12 @@ export function captureDownloadClick(platform: Platform) {
|
||||
console.error('PostHog download click capture failed', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function captureMcpClientTabClick(client: string) {
|
||||
if (!initialized) return
|
||||
try {
|
||||
posthog.capture('website:mcp_client_tab_clicked', { client })
|
||||
} catch (error) {
|
||||
console.error('PostHog MCP client tab capture failed', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import SectionHeader from '../../components/common/SectionHeader.vue'
|
||||
import SectionLabel from '../../components/common/SectionLabel.vue'
|
||||
import VideoPlayer from '../../components/common/VideoPlayer.vue'
|
||||
import CopyableField from '../../components/ui/copyable-field/CopyableField.vue'
|
||||
import { externalLinks } from '../../config/routes'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { captureMcpClientTabClick } from '../../scripts/posthog'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const agentCommand = t('mcp.setup.option1.command', locale).replace(
|
||||
const agentCommand = t('mcp.setup.agent.command', locale).replace(
|
||||
'{url}',
|
||||
externalLinks.docsMcp
|
||||
)
|
||||
@@ -21,19 +23,35 @@ interface McpClient {
|
||||
step: string
|
||||
command?: string
|
||||
link?: { label: string; href: string }
|
||||
manualTitle?: string
|
||||
showAgentCard: boolean
|
||||
// Walkthrough clip shown in place of the agent card (source: docs.comfy.org/agent-tools/mcp)
|
||||
video?: string
|
||||
}
|
||||
|
||||
const clients: McpClient[] = [
|
||||
{
|
||||
id: 'claude-code',
|
||||
name: 'Claude Code',
|
||||
step: t('mcp.setup.clients.claudeCode.step', locale),
|
||||
command: `claude mcp add --transport http comfy-cloud ${externalLinks.mcpEndpoint}`
|
||||
},
|
||||
{
|
||||
id: 'claude-desktop',
|
||||
name: 'Claude Desktop',
|
||||
step: t('mcp.setup.clients.claudeDesktop.step', locale)
|
||||
step: t('mcp.setup.clients.claudeDesktop.step', locale),
|
||||
manualTitle: t('mcp.setup.clients.claudeDesktop.manualTitle', locale),
|
||||
showAgentCard: false,
|
||||
video: '/videos/mcp/setup-claude-desktop-v1.mp4'
|
||||
},
|
||||
{
|
||||
id: 'claude-code',
|
||||
name: 'Claude Code Terminal',
|
||||
step: t('mcp.setup.clients.claudeCode.step', locale),
|
||||
command: `claude mcp add --transport http comfy-cloud ${externalLinks.mcpEndpoint}`,
|
||||
showAgentCard: true
|
||||
},
|
||||
{
|
||||
id: 'codex',
|
||||
name: 'Codex',
|
||||
step: t('mcp.setup.clients.codex.step', locale),
|
||||
command: `codex mcp add comfy-cloud --url ${externalLinks.mcpEndpoint}`,
|
||||
showAgentCard: false,
|
||||
video: '/videos/mcp/setup-codex-oauth-v1.mp4'
|
||||
},
|
||||
{
|
||||
id: 'cursor',
|
||||
@@ -42,13 +60,15 @@ const clients: McpClient[] = [
|
||||
link: {
|
||||
label: t('mcp.setup.clients.cursor.linkLabel', locale),
|
||||
href: externalLinks.apiKeys
|
||||
}
|
||||
},
|
||||
showAgentCard: true
|
||||
},
|
||||
{
|
||||
id: 'codex',
|
||||
name: 'Codex',
|
||||
step: t('mcp.setup.clients.codex.step', locale),
|
||||
command: `codex mcp add comfy-cloud --url ${externalLinks.mcpEndpoint}`
|
||||
id: 'openclaw',
|
||||
name: 'OpenClaw',
|
||||
step: t('mcp.setup.clients.openclaw.step', locale),
|
||||
command: `openclaw skills install @comfy-org/comfy\nopenclaw mcp set comfy '{"url":"${externalLinks.mcpEndpoint}","transport":"streamable-http","auth":"oauth"}'`,
|
||||
showAgentCard: true
|
||||
},
|
||||
{
|
||||
id: 'other',
|
||||
@@ -57,10 +77,38 @@ const clients: McpClient[] = [
|
||||
link: {
|
||||
label: t('mcp.setup.clients.other.linkLabel', locale),
|
||||
href: externalLinks.docsMcp
|
||||
}
|
||||
},
|
||||
showAgentCard: true
|
||||
}
|
||||
]
|
||||
|
||||
const activeClientId = ref(clients[0].id)
|
||||
const activeClient = computed(
|
||||
() =>
|
||||
clients.find((client) => client.id === activeClientId.value) ?? clients[0]
|
||||
)
|
||||
const manualTitle = computed(
|
||||
() => activeClient.value.manualTitle ?? t('mcp.setup.manual.title', locale)
|
||||
)
|
||||
|
||||
// reka-ui re-emits update:modelValue even when the value is unchanged
|
||||
// (re-clicking the active tab), so dedupe before capturing.
|
||||
let lastTrackedClientId: string | undefined
|
||||
function onClientTabChange(value: string | number | undefined) {
|
||||
if (!value) return
|
||||
const id = String(value)
|
||||
if (id === lastTrackedClientId) return
|
||||
lastTrackedClientId = id
|
||||
captureMcpClientTabClick(id)
|
||||
}
|
||||
|
||||
const walkthroughLabel = computed(() =>
|
||||
t('mcp.setup.walkthroughAlt', locale).replace(
|
||||
'{client}',
|
||||
activeClient.value.name
|
||||
)
|
||||
)
|
||||
|
||||
const copyLabel = t('ui.copy', locale)
|
||||
const copiedLabel = t('ui.copied', locale)
|
||||
</script>
|
||||
@@ -83,77 +131,48 @@ const copiedLabel = t('ui.copied', locale)
|
||||
</template>
|
||||
</SectionHeader>
|
||||
|
||||
<div class="mt-16 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div
|
||||
class="bg-transparency-white-t4 flex flex-col rounded-3xl p-6 lg:p-8"
|
||||
<TabsRoot
|
||||
v-model="activeClientId"
|
||||
activation-mode="manual"
|
||||
class="mt-10 block"
|
||||
@update:model-value="onClientTabChange"
|
||||
>
|
||||
<TabsList
|
||||
:aria-label="t('mcp.setup.manual.tabsLabel', locale)"
|
||||
class="grid grid-cols-1 gap-px rounded-2xl border border-white/15 bg-primary-comfy-ink p-1 min-[360px]:grid-cols-2 lg:inline-flex lg:flex-nowrap"
|
||||
>
|
||||
<SectionLabel>{{ t('mcp.setup.option1.label', locale) }}</SectionLabel>
|
||||
<h3
|
||||
class="mt-3 text-xl font-light text-primary-comfy-canvas lg:text-2xl"
|
||||
<TabsTrigger
|
||||
v-for="client in clients"
|
||||
:key="client.id"
|
||||
:value="client.id"
|
||||
class="focus-visible:ring-primary-comfy-yellow/50 data-[state=active]:bg-primary-comfy-yellow shrink-0 cursor-pointer rounded-lg bg-white/8 px-2 py-2.5 text-[10px] font-bold tracking-wider whitespace-nowrap text-smoke-700 uppercase transition-colors hover:text-primary-comfy-canvas focus-visible:ring-2 focus-visible:outline-none data-[state=active]:text-primary-comfy-ink lg:rounded-none lg:px-6 lg:text-xs lg:first:rounded-l-xl lg:last:rounded-r-xl"
|
||||
>
|
||||
{{ t('mcp.setup.option1.title', locale) }}
|
||||
</h3>
|
||||
<p class="mt-3 text-sm text-smoke-700">
|
||||
{{ t('mcp.setup.option1.description', locale) }}
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<CopyableField
|
||||
:value="agentCommand"
|
||||
:copy-label="copyLabel"
|
||||
:copied-label="copiedLabel"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-auto pt-6 text-sm text-smoke-700">
|
||||
{{ t('mcp.setup.skillsNote', locale)
|
||||
}}<a
|
||||
:href="externalLinks.mcpSkills"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="focus-visible:ring-primary-comfy-yellow/50 rounded-sm text-primary-comfy-canvas underline underline-offset-4 focus-visible:ring-2 focus-visible:outline-none"
|
||||
>{{ t('mcp.setup.skillsLink', locale) }}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
{{ client.name }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div
|
||||
class="bg-transparency-white-t4 flex flex-col rounded-3xl p-6 lg:p-8"
|
||||
>
|
||||
<SectionLabel>{{ t('mcp.setup.option2.label', locale) }}</SectionLabel>
|
||||
<h3
|
||||
class="mt-3 text-xl font-light text-primary-comfy-canvas lg:text-2xl"
|
||||
<div class="mt-10 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div
|
||||
class="bg-transparency-white-t4 flex flex-col rounded-3xl p-6 lg:p-8"
|
||||
>
|
||||
{{ t('mcp.setup.option2.title', locale) }}
|
||||
</h3>
|
||||
<p class="mt-3 text-sm text-smoke-700">
|
||||
{{ t('mcp.setup.option2.description', locale) }}
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<CopyableField
|
||||
:value="externalLinks.mcpEndpoint"
|
||||
:copy-label="copyLabel"
|
||||
:copied-label="copiedLabel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TabsRoot default-value="claude-code" class="mt-6">
|
||||
<TabsList
|
||||
:aria-label="t('mcp.setup.option2.tabsLabel', locale)"
|
||||
class="flex flex-wrap gap-2"
|
||||
>
|
||||
<TabsTrigger
|
||||
v-for="client in clients"
|
||||
:key="client.id"
|
||||
:value="client.id"
|
||||
class="bg-transparency-white-t4 focus-visible:ring-primary-comfy-yellow/50 data-[state=active]:bg-primary-comfy-yellow cursor-pointer rounded-full px-4 py-2 text-xs font-bold tracking-wider text-smoke-700 uppercase transition-colors hover:text-primary-comfy-canvas focus-visible:ring-2 focus-visible:outline-none data-[state=active]:text-primary-comfy-ink"
|
||||
>
|
||||
{{ client.name }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<h3 class="text-xl font-light text-primary-comfy-canvas lg:text-2xl">
|
||||
{{ manualTitle }}
|
||||
</h3>
|
||||
<p class="mt-3 text-sm text-smoke-700">
|
||||
{{ t('mcp.setup.manual.description', locale) }}
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<CopyableField
|
||||
:value="externalLinks.mcpEndpoint"
|
||||
:copy-label="copyLabel"
|
||||
:copied-label="copiedLabel"
|
||||
/>
|
||||
</div>
|
||||
<TabsContent
|
||||
v-for="client in clients"
|
||||
:key="client.id"
|
||||
:value="client.id"
|
||||
class="mt-4 flex min-h-24 flex-col gap-3"
|
||||
class="mt-6 flex min-h-36 flex-col gap-3"
|
||||
>
|
||||
<p class="text-sm text-smoke-700">
|
||||
{{ client.step
|
||||
@@ -173,8 +192,57 @@ const copiedLabel = t('ui.copied', locale)
|
||||
:copied-label="copiedLabel"
|
||||
/>
|
||||
</TabsContent>
|
||||
</TabsRoot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-transparency-white-t4 flex flex-col rounded-3xl"
|
||||
:class="
|
||||
activeClient.showAgentCard
|
||||
? 'p-6 lg:p-8'
|
||||
: 'relative overflow-hidden max-lg:aspect-video'
|
||||
"
|
||||
>
|
||||
<template v-if="activeClient.showAgentCard">
|
||||
<h3
|
||||
class="text-xl font-light text-primary-comfy-canvas lg:text-2xl"
|
||||
>
|
||||
{{ t('mcp.setup.agent.title', locale) }}
|
||||
</h3>
|
||||
<p class="mt-3 text-sm text-smoke-700">
|
||||
{{ t('mcp.setup.agent.description', locale) }}
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
<CopyableField
|
||||
:value="agentCommand"
|
||||
:copy-label="copyLabel"
|
||||
:copied-label="copiedLabel"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-6 text-sm text-smoke-700">
|
||||
{{ t('mcp.setup.skillsNote', locale)
|
||||
}}<a
|
||||
:href="externalLinks.mcpSkills"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="focus-visible:ring-primary-comfy-yellow/50 rounded-sm text-primary-comfy-canvas underline underline-offset-4 focus-visible:ring-2 focus-visible:outline-none"
|
||||
>{{ t('mcp.setup.skillsLink', locale) }}</a
|
||||
>
|
||||
</p>
|
||||
</template>
|
||||
<VideoPlayer
|
||||
v-else-if="activeClient.video"
|
||||
:key="activeClient.id"
|
||||
:locale="locale"
|
||||
:aria-label="walkthroughLabel"
|
||||
:src="activeClient.video"
|
||||
autoplay
|
||||
loop
|
||||
hide-controls
|
||||
fit="contain"
|
||||
class="absolute inset-0 size-full bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsRoot>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user