mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
Compare commits
1 Commits
backport/6
...
i18n-sync-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce30f2e03 |
@@ -5,7 +5,7 @@ import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescrip
|
||||
import { importX } from 'eslint-plugin-import-x'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
// import tailwind from 'eslint-plugin-tailwindcss'
|
||||
import tailwind from 'eslint-plugin-tailwindcss'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import { defineConfig } from 'eslint/config'
|
||||
@@ -34,11 +34,11 @@ const settings = {
|
||||
],
|
||||
noWarnOnMultipleProjects: true
|
||||
})
|
||||
]
|
||||
// tailwindcss: {
|
||||
// config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
|
||||
// functions: ['cn', 'clsx', 'tw']
|
||||
// }
|
||||
],
|
||||
tailwindcss: {
|
||||
config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
|
||||
functions: ['cn', 'clsx', 'tw']
|
||||
}
|
||||
} as const
|
||||
|
||||
const commonParserOptions = {
|
||||
@@ -97,7 +97,7 @@ export default defineConfig([
|
||||
// Difference in typecheck on CI vs Local
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Bad types in the plugin
|
||||
// tailwind.configs['flat/recommended'],
|
||||
tailwind.configs['flat/recommended'],
|
||||
pluginVue.configs['flat/recommended'],
|
||||
eslintPluginPrettierRecommended,
|
||||
storybook.configs['flat/recommended'],
|
||||
@@ -129,7 +129,7 @@ export default defineConfig([
|
||||
'import-x/no-relative-packages': 'error',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
// 'tailwindcss/no-custom-classname': 'off', // TODO: fix
|
||||
'tailwindcss/no-custom-classname': 'off', // TODO: fix
|
||||
'vue/no-v-html': 'off',
|
||||
// Enforce dark-theme: instead of dark: prefix
|
||||
'vue/no-restricted-class': ['error', '/^dark:/'],
|
||||
|
||||
1
global.d.ts
vendored
1
global.d.ts
vendored
@@ -8,7 +8,6 @@ declare const __USE_PROD_CONFIG__: boolean
|
||||
interface Window {
|
||||
__CONFIG__: {
|
||||
mixpanel_token?: string
|
||||
require_whitelist?: boolean
|
||||
subscription_required?: boolean
|
||||
server_health_alert?: {
|
||||
message: string
|
||||
|
||||
@@ -52,12 +52,12 @@
|
||||
"@nx/vite": "catalog:",
|
||||
"@pinia/testing": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"@sentry/vite-plugin": "catalog:",
|
||||
"@storybook/addon-docs": "catalog:",
|
||||
"@storybook/vue3": "catalog:",
|
||||
"@storybook/vue3-vite": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@trivago/prettier-plugin-sort-imports": "catalog:",
|
||||
"@types/eslint-plugin-tailwindcss": "catalog:",
|
||||
"@types/fs-extra": "catalog:",
|
||||
"@types/jsdom": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
@@ -74,6 +74,7 @@
|
||||
"eslint-plugin-import-x": "catalog:",
|
||||
"eslint-plugin-prettier": "catalog:",
|
||||
"eslint-plugin-storybook": "catalog:",
|
||||
"eslint-plugin-tailwindcss": "catalog:",
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
"eslint-plugin-vue": "catalog:",
|
||||
"fs-extra": "^11.2.0",
|
||||
|
||||
@@ -126,13 +126,6 @@
|
||||
--content-hover-bg: #adadad;
|
||||
--content-hover-fg: #000;
|
||||
|
||||
--button-surface: var(--color-white);
|
||||
--button-surface-contrast: var(--color-black);
|
||||
|
||||
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
|
||||
|
||||
--modal-card-button-surface: var(--color-smoke-300);
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
--code-text-color: rgb(0 122 255 / 1);
|
||||
--code-bg-color: rgb(96 165 250 / 0.2);
|
||||
@@ -175,17 +168,6 @@
|
||||
.dark-theme {
|
||||
--accent-primary: var(--color-pure-white);
|
||||
--backdrop: var(--color-neutral-900);
|
||||
|
||||
--button-surface: var(--color-charcoal-600);
|
||||
--button-surface-contrast: var(--color-white);
|
||||
--button-hover-surface: var(--color-charcoal-600);
|
||||
--button-active-surface: var(--color-charcoal-600);
|
||||
--button-icon: var(--color-smoke-800);
|
||||
|
||||
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
|
||||
|
||||
--modal-card-button-surface: var(--color-charcoal-300);
|
||||
|
||||
--dialog-surface: var(--color-neutral-700);
|
||||
--node-component-border: var(--color-stone-200);
|
||||
--node-component-border-error: var(--color-danger-100);
|
||||
@@ -214,13 +196,6 @@
|
||||
|
||||
@theme inline {
|
||||
--color-backdrop: var(--backdrop);
|
||||
--color-button-active-surface: var(--button-active-surface);
|
||||
--color-button-hover-surface: var(--button-hover-surface);
|
||||
--color-button-icon: var(--button-icon);
|
||||
--color-button-surface: var(--button-surface);
|
||||
--color-button-surface-contrast: var(--button-surface-contrast);
|
||||
--color-subscription-button-gradient: var(--subscription-button-gradient);
|
||||
--color-modal-card-button-surface: var(--modal-card-button-surface);
|
||||
--color-dialog-surface: var(--dialog-surface);
|
||||
--color-node-component-border: var(--node-component-border);
|
||||
--color-node-component-executing: var(--node-component-executing);
|
||||
|
||||
285
pnpm-lock.yaml
generated
285
pnpm-lock.yaml
generated
@@ -66,9 +66,6 @@ catalogs:
|
||||
'@primevue/themes':
|
||||
specifier: ^4.2.5
|
||||
version: 4.2.5
|
||||
'@sentry/vite-plugin':
|
||||
specifier: ^4.6.0
|
||||
version: 4.6.0
|
||||
'@sentry/vue':
|
||||
specifier: ^8.48.0
|
||||
version: 8.48.0
|
||||
@@ -87,6 +84,9 @@ catalogs:
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.2
|
||||
'@types/eslint-plugin-tailwindcss':
|
||||
specifier: ^3.17.0
|
||||
version: 3.17.0
|
||||
'@types/fs-extra':
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
@@ -150,6 +150,9 @@ catalogs:
|
||||
eslint-plugin-storybook:
|
||||
specifier: ^9.1.6
|
||||
version: 9.1.6
|
||||
eslint-plugin-tailwindcss:
|
||||
specifier: 4.0.0-beta.0
|
||||
version: 4.0.0-beta.0
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@@ -282,7 +285,6 @@ catalogs:
|
||||
|
||||
overrides:
|
||||
'@types/eslint': '-'
|
||||
'@eslint/core': 0.17.0
|
||||
|
||||
importers:
|
||||
|
||||
@@ -484,9 +486,6 @@ importers:
|
||||
'@playwright/test':
|
||||
specifier: 'catalog:'
|
||||
version: 1.52.0
|
||||
'@sentry/vite-plugin':
|
||||
specifier: 'catalog:'
|
||||
version: 4.6.0
|
||||
'@storybook/addon-docs':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
@@ -502,6 +501,9 @@ importers:
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: 'catalog:'
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)
|
||||
'@types/eslint-plugin-tailwindcss':
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.0
|
||||
'@types/fs-extra':
|
||||
specifier: 'catalog:'
|
||||
version: 11.0.4
|
||||
@@ -550,6 +552,9 @@ importers:
|
||||
eslint-plugin-storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
eslint-plugin-tailwindcss:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0-beta.0(tailwindcss@4.1.12)
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.0(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))
|
||||
@@ -1768,8 +1773,8 @@ packages:
|
||||
resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/core@0.17.0':
|
||||
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
|
||||
'@eslint/core@0.15.2':
|
||||
resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/eslintrc@3.3.1':
|
||||
@@ -2653,78 +2658,14 @@ packages:
|
||||
resolution: {integrity: sha512-csILVupc5RkrsTrncuUTGmlB56FQSFjXPYWG8I8yBTGlXEJ+o8oTuF6+55R4vbw3EIzBveXWi4kEBbnQlXW/eg==}
|
||||
engines: {node: '>=14.18'}
|
||||
|
||||
'@sentry/babel-plugin-component-annotate@4.6.0':
|
||||
resolution: {integrity: sha512-3soTX50JPQQ51FSbb4qvNBf4z/yP7jTdn43vMTp9E4IxvJ9HKJR7OEuKkCMszrZmWsVABXl02msqO7QisePdiQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
'@sentry/browser@8.48.0':
|
||||
resolution: {integrity: sha512-fuuVULB5/1vI8NoIwXwR3xwhJJqk+y4RdSdajExGF7nnUDBpwUJyXsmYJnOkBO+oLeEs58xaCpotCKiPUNnE3g==}
|
||||
engines: {node: '>=14.18'}
|
||||
|
||||
'@sentry/bundler-plugin-core@4.6.0':
|
||||
resolution: {integrity: sha512-Fub2XQqrS258jjS8qAxLLU1k1h5UCNJ76i8m4qZJJdogWWaF8t00KnnTyp9TEDJzrVD64tRXS8+HHENxmeUo3g==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
'@sentry/cli-darwin@2.57.0':
|
||||
resolution: {integrity: sha512-v1wYQU3BcCO+Z3OVxxO+EnaW4oQhuOza6CXeYZ0z5ftza9r0QQBLz3bcZKTVta86xraNm0z8GDlREwinyddOxQ==}
|
||||
engines: {node: '>=10'}
|
||||
os: [darwin]
|
||||
|
||||
'@sentry/cli-linux-arm64@2.57.0':
|
||||
resolution: {integrity: sha512-Kh1jTsMV5Fy/RvB381N/woXe1qclRMqsG6kM3Gq6m6afEF/+k3PyQdNW3HXAola6d63EptokLtxPG2xjWQ+w9Q==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux, freebsd, android]
|
||||
|
||||
'@sentry/cli-linux-arm@2.57.0':
|
||||
resolution: {integrity: sha512-uNHB8xyygqfMd1/6tFzl9NUkuVefg7jdZtM/vVCQVaF/rJLWZ++Wms+LLhYyKXKN8yd7J9wy7kTEl4Qu4jWbGQ==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm]
|
||||
os: [linux, freebsd, android]
|
||||
|
||||
'@sentry/cli-linux-i686@2.57.0':
|
||||
resolution: {integrity: sha512-EYXghoK/tKd0zqz+KD/ewXXE3u1HLCwG89krweveytBy/qw7M5z58eFvw+iGb1Vnbl1f/fRD0G4E0AbEsPfmpg==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x86, ia32]
|
||||
os: [linux, freebsd, android]
|
||||
|
||||
'@sentry/cli-linux-x64@2.57.0':
|
||||
resolution: {integrity: sha512-CyZrP/ssHmAPLSzfd4ydy7icDnwmDD6o3QjhkWwVFmCd+9slSBMQxpIqpamZmrWE6X4R+xBRbSUjmdoJoZ5yMw==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux, freebsd, android]
|
||||
|
||||
'@sentry/cli-win32-arm64@2.57.0':
|
||||
resolution: {integrity: sha512-wji/GGE4Lh5I/dNCsuVbg6fRvttvZRG6db1yPW1BSvQRh8DdnVy1CVp+HMqSq0SRy/S4z60j2u+m4yXMoCL+5g==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@sentry/cli-win32-i686@2.57.0':
|
||||
resolution: {integrity: sha512-hWvzyD7bTPh3b55qvJ1Okg3Wbl0Km8xcL6KvS7gfBl6uss+I6RldmQTP0gJKdHSdf/QlJN1FK0b7bLnCB3wHsg==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x86, ia32]
|
||||
os: [win32]
|
||||
|
||||
'@sentry/cli-win32-x64@2.57.0':
|
||||
resolution: {integrity: sha512-QWYV/Y0sbpDSTyA4XQBOTaid4a6H2Iwa1Z8UI+qNxFlk0ADSEgIqo2NrRHDU8iRnghTkecQNX1NTt/7mXN3f/A==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@sentry/cli@2.57.0':
|
||||
resolution: {integrity: sha512-oC4HPrVIX06GvUTgK0i+WbNgIA9Zl5YEcwf9N4eWFJJmjonr2j4SML9Hn2yNENbUWDgwepy4MLod3P8rM4bk/w==}
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@sentry/core@8.48.0':
|
||||
resolution: {integrity: sha512-VGwYgTfLpvJ5LRO5A+qWo1gpo6SfqaGXL9TOzVgBucAdpzbrYHpZ87sEarDVq/4275uk1b0S293/mfsskFczyw==}
|
||||
engines: {node: '>=14.18'}
|
||||
|
||||
'@sentry/vite-plugin@4.6.0':
|
||||
resolution: {integrity: sha512-fMR2d+EHwbzBa0S1fp45SNUTProxmyFBp+DeBWWQOSP9IU6AH6ea2rqrpMAnp/skkcdW4z4LSRrOEpMZ5rWXLw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
'@sentry/vue@8.48.0':
|
||||
resolution: {integrity: sha512-hqm9X7hz1vMQQB1HBYezrDBQihZk6e/MxWIG1wMJoClcBnD1Sh7y+D36UwaQlR4Gr/Ftiz+Bb0DxuAYHoUS4ow==}
|
||||
engines: {node: '>=14.18'}
|
||||
@@ -3088,6 +3029,9 @@ packages:
|
||||
'@types/diff-match-patch@1.0.36':
|
||||
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||
|
||||
'@types/eslint-plugin-tailwindcss@3.17.0':
|
||||
resolution: {integrity: sha512-ucQGf2YIdTcndYcxRU3UdZgmhUHsOlbIF4BaRtl0op+7k2JmqM2i3aXZ6XIcfZgVq1ZKov7VM5c/BR81ukmkyg==}
|
||||
|
||||
'@types/estree@1.0.5':
|
||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||
|
||||
@@ -3631,10 +3575,6 @@ packages:
|
||||
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
agent-base@6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -4634,6 +4574,12 @@ packages:
|
||||
eslint: '>=8'
|
||||
storybook: ^9.1.6
|
||||
|
||||
eslint-plugin-tailwindcss@4.0.0-beta.0:
|
||||
resolution: {integrity: sha512-WWCajZgQu38Sd67ZCl2W6i3MRzqB0d+H8s4qV9iB6lBJbsDOIpIlj6R1Fj2FXkoWErbo05pZnZYbCGIU9o/DsA==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
peerDependencies:
|
||||
tailwindcss: ^3.4.0 || ^4.0.0
|
||||
|
||||
eslint-plugin-unused-imports@4.2.0:
|
||||
resolution: {integrity: sha512-hLbJ2/wnjKq4kGA9AUaExVFIbNzyxYdVo49QZmKCnhk5pc9wcYRbfgLHvWJ8tnsdcseGhoUAddm9gn/lt+d74w==}
|
||||
peerDependencies:
|
||||
@@ -4908,9 +4854,6 @@ packages:
|
||||
resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -4991,10 +4934,6 @@ packages:
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
glob@9.3.5:
|
||||
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
global-directory@4.0.1:
|
||||
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5124,10 +5063,6 @@ packages:
|
||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -5813,10 +5748,6 @@ packages:
|
||||
magic-string@0.30.19:
|
||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
||||
|
||||
magic-string@0.30.8:
|
||||
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
magicast@0.3.5:
|
||||
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
||||
|
||||
@@ -6037,10 +5968,6 @@ packages:
|
||||
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimatch@8.0.4:
|
||||
resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimatch@9.0.1:
|
||||
resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -6056,10 +5983,6 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
minipass@4.2.8:
|
||||
resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -6491,10 +6414,6 @@ packages:
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
progress@2.0.3:
|
||||
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
promise@7.3.1:
|
||||
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
|
||||
|
||||
@@ -7093,6 +7012,11 @@ packages:
|
||||
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
tailwind-api-utils@1.0.3:
|
||||
resolution: {integrity: sha512-KpzUHkH1ug1sq4394SLJX38ZtpeTiqQ1RVyFTTSY2XuHsNSTWUkRo108KmyyrMWdDbQrLYkSHaNKj/a3bmA4sQ==}
|
||||
peerDependencies:
|
||||
tailwindcss: ^3.3.0 || ^4.0.0 || ^4.0.0-beta
|
||||
|
||||
tailwind-merge@2.6.0:
|
||||
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
|
||||
|
||||
@@ -7377,9 +7301,6 @@ packages:
|
||||
'@nuxt/kit':
|
||||
optional: true
|
||||
|
||||
unplugin@1.0.1:
|
||||
resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==}
|
||||
|
||||
unplugin@1.16.1:
|
||||
resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -7554,9 +7475,6 @@ packages:
|
||||
vue-component-type-helpers@3.1.1:
|
||||
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
|
||||
|
||||
vue-component-type-helpers@3.1.2:
|
||||
resolution: {integrity: sha512-ch3/SKBtxdZq18vsEntiGCdSszCRNfhX5QaTxjSacCAXLlNQRXfXo+ANjoQEYJMsJOJy1/vHF6Tkc4s85MS+zw==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -7653,13 +7571,6 @@ packages:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
webpack-sources@3.3.3:
|
||||
resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
webpack-virtual-modules@0.5.0:
|
||||
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
|
||||
|
||||
webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
|
||||
@@ -8940,7 +8851,7 @@ snapshots:
|
||||
|
||||
'@eslint/config-helpers@0.3.1': {}
|
||||
|
||||
'@eslint/core@0.17.0':
|
||||
'@eslint/core@0.15.2':
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
@@ -8964,7 +8875,7 @@ snapshots:
|
||||
|
||||
'@eslint/plugin-kit@0.3.5':
|
||||
dependencies:
|
||||
'@eslint/core': 0.17.0
|
||||
'@eslint/core': 0.15.2
|
||||
levn: 0.4.1
|
||||
|
||||
'@firebase/analytics-compat@0.2.18(@firebase/app-compat@0.2.53)(@firebase/app@0.11.4)':
|
||||
@@ -10113,8 +10024,6 @@ snapshots:
|
||||
'@sentry-internal/browser-utils': 8.48.0
|
||||
'@sentry/core': 8.48.0
|
||||
|
||||
'@sentry/babel-plugin-component-annotate@4.6.0': {}
|
||||
|
||||
'@sentry/browser@8.48.0':
|
||||
dependencies:
|
||||
'@sentry-internal/browser-utils': 8.48.0
|
||||
@@ -10123,74 +10032,8 @@ snapshots:
|
||||
'@sentry-internal/replay-canvas': 8.48.0
|
||||
'@sentry/core': 8.48.0
|
||||
|
||||
'@sentry/bundler-plugin-core@4.6.0':
|
||||
dependencies:
|
||||
'@babel/core': 7.27.1
|
||||
'@sentry/babel-plugin-component-annotate': 4.6.0
|
||||
'@sentry/cli': 2.57.0
|
||||
dotenv: 16.6.1
|
||||
find-up: 5.0.0
|
||||
glob: 9.3.5
|
||||
magic-string: 0.30.8
|
||||
unplugin: 1.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
'@sentry/cli-darwin@2.57.0':
|
||||
optional: true
|
||||
|
||||
'@sentry/cli-linux-arm64@2.57.0':
|
||||
optional: true
|
||||
|
||||
'@sentry/cli-linux-arm@2.57.0':
|
||||
optional: true
|
||||
|
||||
'@sentry/cli-linux-i686@2.57.0':
|
||||
optional: true
|
||||
|
||||
'@sentry/cli-linux-x64@2.57.0':
|
||||
optional: true
|
||||
|
||||
'@sentry/cli-win32-arm64@2.57.0':
|
||||
optional: true
|
||||
|
||||
'@sentry/cli-win32-i686@2.57.0':
|
||||
optional: true
|
||||
|
||||
'@sentry/cli-win32-x64@2.57.0':
|
||||
optional: true
|
||||
|
||||
'@sentry/cli@2.57.0':
|
||||
dependencies:
|
||||
https-proxy-agent: 5.0.1
|
||||
node-fetch: 2.7.0
|
||||
progress: 2.0.3
|
||||
proxy-from-env: 1.1.0
|
||||
which: 2.0.2
|
||||
optionalDependencies:
|
||||
'@sentry/cli-darwin': 2.57.0
|
||||
'@sentry/cli-linux-arm': 2.57.0
|
||||
'@sentry/cli-linux-arm64': 2.57.0
|
||||
'@sentry/cli-linux-i686': 2.57.0
|
||||
'@sentry/cli-linux-x64': 2.57.0
|
||||
'@sentry/cli-win32-arm64': 2.57.0
|
||||
'@sentry/cli-win32-i686': 2.57.0
|
||||
'@sentry/cli-win32-x64': 2.57.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
'@sentry/core@8.48.0': {}
|
||||
|
||||
'@sentry/vite-plugin@4.6.0':
|
||||
dependencies:
|
||||
'@sentry/bundler-plugin-core': 4.6.0
|
||||
unplugin: 1.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
'@sentry/vue@8.48.0(pinia@2.2.2(typescript@5.9.2)(vue@3.5.13(typescript@5.9.2)))(vue@3.5.13(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@sentry/browser': 8.48.0
|
||||
@@ -10261,7 +10104,7 @@ snapshots:
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
vue-component-type-helpers: 3.1.2
|
||||
vue-component-type-helpers: 3.1.1
|
||||
|
||||
'@swc/helpers@0.5.17':
|
||||
dependencies:
|
||||
@@ -10566,6 +10409,8 @@ snapshots:
|
||||
|
||||
'@types/diff-match-patch@1.0.36': {}
|
||||
|
||||
'@types/eslint-plugin-tailwindcss@3.17.0': {}
|
||||
|
||||
'@types/estree@1.0.5': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
@@ -11190,12 +11035,6 @@ snapshots:
|
||||
|
||||
address@1.2.2: {}
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
agentkeepalive@4.6.0:
|
||||
@@ -12337,6 +12176,14 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
eslint-plugin-tailwindcss@4.0.0-beta.0(tailwindcss@4.1.12):
|
||||
dependencies:
|
||||
fast-glob: 3.3.3
|
||||
postcss: 8.5.6
|
||||
synckit: 0.11.11
|
||||
tailwind-api-utils: 1.0.3(tailwindcss@4.1.12)
|
||||
tailwindcss: 4.1.12
|
||||
|
||||
eslint-plugin-unused-imports@4.2.0(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2)):
|
||||
dependencies:
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
@@ -12371,7 +12218,7 @@ snapshots:
|
||||
'@eslint-community/regexpp': 4.12.1
|
||||
'@eslint/config-array': 0.21.0
|
||||
'@eslint/config-helpers': 0.3.1
|
||||
'@eslint/core': 0.17.0
|
||||
'@eslint/core': 0.15.2
|
||||
'@eslint/eslintrc': 3.3.1
|
||||
'@eslint/js': 9.35.0
|
||||
'@eslint/plugin-kit': 0.3.5
|
||||
@@ -12695,8 +12542,6 @@ snapshots:
|
||||
jsonfile: 6.2.0
|
||||
universalify: 2.0.1
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
@@ -12791,13 +12636,6 @@ snapshots:
|
||||
package-json-from-dist: 1.0.0
|
||||
path-scurry: 2.0.0
|
||||
|
||||
glob@9.3.5:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
minimatch: 8.0.4
|
||||
minipass: 4.2.8
|
||||
path-scurry: 1.11.1
|
||||
|
||||
global-directory@4.0.1:
|
||||
dependencies:
|
||||
ini: 4.1.1
|
||||
@@ -12928,13 +12766,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
@@ -13624,10 +13455,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
magic-string@0.30.8:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
magicast@0.3.5:
|
||||
dependencies:
|
||||
'@babel/parser': 7.28.4
|
||||
@@ -14033,10 +13860,6 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@8.0.4:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.1:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
@@ -14051,8 +13874,6 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@4.2.8: {}
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
minizlib@3.0.2:
|
||||
@@ -14536,8 +14357,6 @@ snapshots:
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
progress@2.0.3: {}
|
||||
|
||||
promise@7.3.1:
|
||||
dependencies:
|
||||
asap: 2.0.6
|
||||
@@ -15375,6 +15194,13 @@ snapshots:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
tailwind-api-utils@1.0.3(tailwindcss@4.1.12):
|
||||
dependencies:
|
||||
enhanced-resolve: 5.18.3
|
||||
jiti: 2.5.1
|
||||
local-pkg: 1.1.2
|
||||
tailwindcss: 4.1.12
|
||||
|
||||
tailwind-merge@2.6.0: {}
|
||||
|
||||
tailwindcss-primeui@0.6.1(tailwindcss@4.1.12):
|
||||
@@ -15666,13 +15492,6 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
unplugin@1.0.1:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
chokidar: 3.6.0
|
||||
webpack-sources: 3.3.3
|
||||
webpack-virtual-modules: 0.5.0
|
||||
|
||||
unplugin@1.16.1:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
@@ -15927,8 +15746,6 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@3.1.1: {}
|
||||
|
||||
vue-component-type-helpers@3.1.2: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
@@ -16021,10 +15838,6 @@ snapshots:
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
webpack-sources@3.3.3: {}
|
||||
|
||||
webpack-virtual-modules@0.5.0: {}
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
websocket-driver@0.7.4:
|
||||
|
||||
@@ -23,13 +23,13 @@ catalog:
|
||||
'@primevue/forms': ^4.2.5
|
||||
'@primevue/icons': 4.2.5
|
||||
'@primevue/themes': ^4.2.5
|
||||
'@sentry/vite-plugin': ^4.6.0
|
||||
'@sentry/vue': ^8.48.0
|
||||
'@storybook/addon-docs': ^9.1.1
|
||||
'@storybook/vue3': ^9.1.1
|
||||
'@storybook/vue3-vite': ^9.1.1
|
||||
'@tailwindcss/vite': ^4.1.12
|
||||
'@trivago/prettier-plugin-sort-imports': ^5.2.0
|
||||
'@types/eslint-plugin-tailwindcss': ^3.17.0
|
||||
'@types/fs-extra': ^11.0.4
|
||||
'@types/jsdom': ^21.1.7
|
||||
'@types/node': ^20.14.8
|
||||
@@ -51,6 +51,7 @@ catalog:
|
||||
eslint-plugin-import-x: ^4.16.1
|
||||
eslint-plugin-prettier: ^5.5.4
|
||||
eslint-plugin-storybook: ^9.1.6
|
||||
eslint-plugin-tailwindcss: 4.0.0-beta.0
|
||||
eslint-plugin-unused-imports: ^4.2.0
|
||||
eslint-plugin-vue: ^10.4.0
|
||||
firebase: ^11.6.0
|
||||
@@ -61,7 +62,6 @@ catalog:
|
||||
jsdom: ^26.1.0
|
||||
knip: ^5.62.0
|
||||
lint-staged: ^15.2.7
|
||||
mixpanel-browser: ^2.71.0
|
||||
nx: 21.4.1
|
||||
pinia: ^2.1.7
|
||||
postcss-html: ^1.8.0
|
||||
@@ -94,9 +94,13 @@ catalog:
|
||||
zod: ^3.23.8
|
||||
zod-to-json-schema: ^3.24.1
|
||||
zod-validation-error: ^3.3.0
|
||||
mixpanel-browser: ^2.71.0
|
||||
|
||||
cleanupUnusedCatalogs: true
|
||||
|
||||
overrides:
|
||||
'@types/eslint': '-'
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@firebase/util'
|
||||
- protobufjs
|
||||
@@ -111,7 +115,3 @@ onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- nx
|
||||
- oxc-resolver
|
||||
|
||||
overrides:
|
||||
'@eslint/core': 0.17.0
|
||||
'@types/eslint': '-'
|
||||
|
||||
@@ -380,7 +380,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, provide, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
@@ -401,8 +401,6 @@ import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
|
||||
import { useLazyPagination } from '@/composables/useLazyPagination'
|
||||
import { useTemplateFiltering } from '@/composables/useTemplateFiltering'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useTemplateWorkflows } from '@/platform/workflow/templates/composables/useTemplateWorkflows'
|
||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
@@ -412,34 +410,10 @@ import { createGridStyle } from '@/utils/gridUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { onClose: originalOnClose } = defineProps<{
|
||||
const { onClose } = defineProps<{
|
||||
onClose: () => void
|
||||
}>()
|
||||
|
||||
// Track session time for telemetry
|
||||
const sessionStartTime = ref<number>(0)
|
||||
const templateWasSelected = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
sessionStartTime.value = Date.now()
|
||||
})
|
||||
|
||||
// Wrap onClose to track session end
|
||||
const onClose = () => {
|
||||
if (isCloud) {
|
||||
const timeSpentSeconds = Math.floor(
|
||||
(Date.now() - sessionStartTime.value) / 1000
|
||||
)
|
||||
|
||||
useTelemetry()?.trackTemplateLibraryClosed({
|
||||
template_selected: templateWasSelected.value,
|
||||
time_spent_seconds: timeSpentSeconds
|
||||
})
|
||||
}
|
||||
|
||||
originalOnClose()
|
||||
}
|
||||
|
||||
provide(OnCloseKey, onClose)
|
||||
|
||||
// Workflow templates store and composable
|
||||
@@ -724,7 +698,6 @@ const onLoadWorkflow = async (template: any) => {
|
||||
template.name,
|
||||
getEffectiveSourceModule(template)
|
||||
)
|
||||
templateWasSelected.value = true
|
||||
onClose()
|
||||
} finally {
|
||||
loadingTemplate.value = null
|
||||
|
||||
@@ -101,14 +101,12 @@ import {
|
||||
EventType,
|
||||
useCustomerEventsService
|
||||
} from '@/services/customerEventsService'
|
||||
import { useTopupTrackerStore } from '@/stores/topupTrackerStore'
|
||||
|
||||
const events = ref<AuditLog[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const customerEventService = useCustomerEventsService()
|
||||
const topupTracker = useTopupTrackerStore()
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
@@ -161,8 +159,6 @@ const loadEvents = async () => {
|
||||
if (response.totalPages) {
|
||||
pagination.value.totalPages = response.totalPages
|
||||
}
|
||||
|
||||
void topupTracker.reconcileWithEvents(response.events)
|
||||
} else {
|
||||
error.value = customerEventService.error.value || 'Failed to load events'
|
||||
}
|
||||
|
||||
@@ -12,18 +12,19 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const isSmall = computed(
|
||||
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
|
||||
)
|
||||
|
||||
const openTemplates = () => {
|
||||
useWorkflowTemplateSelectorDialog().show('sidebar')
|
||||
void commandStore.execute('Comfy.BrowseTemplates')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -80,7 +80,6 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
@@ -169,7 +168,7 @@ const extraMenuItems = computed(() => [
|
||||
key: 'browse-templates',
|
||||
label: t('menuLabels.Browse Templates'),
|
||||
icon: 'icon-[comfy--template]',
|
||||
command: () => useWorkflowTemplateSelectorDialog().show('menu')
|
||||
command: () => commandStore.execute('Comfy.BrowseTemplates')
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
|
||||
@@ -79,11 +79,9 @@ vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
}))
|
||||
|
||||
// Mock the useSubscription composable
|
||||
const mockFetchStatus = vi.fn().mockResolvedValue(undefined)
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: vi.fn(() => ({
|
||||
isActiveSubscription: { value: true },
|
||||
fetchStatus: mockFetchStatus
|
||||
isActiveSubscription: vi.fn().mockReturnValue(true)
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -107,15 +105,6 @@ vi.mock('@/components/common/UserCredit.vue', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/components/SubscribeButton.vue', () => ({
|
||||
default: {
|
||||
name: 'SubscribeButtonMock',
|
||||
render() {
|
||||
return h('div', 'Subscribe Button')
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
describe('CurrentUserPopover', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -148,9 +137,9 @@ describe('CurrentUserPopover', () => {
|
||||
it('renders logout button with correct props', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Find all buttons and get the logout button (last button)
|
||||
// Find all buttons and get the logout button (second one)
|
||||
const buttons = wrapper.findAllComponents(Button)
|
||||
const logoutButton = buttons[4]
|
||||
const logoutButton = buttons[1]
|
||||
|
||||
// Check that logout button has correct props
|
||||
expect(logoutButton.props('label')).toBe('Log Out')
|
||||
@@ -160,9 +149,9 @@ describe('CurrentUserPopover', () => {
|
||||
it('opens user settings and emits close event when settings button is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Find all buttons and get the settings button (third button)
|
||||
// Find all buttons and get the settings button (first one)
|
||||
const buttons = wrapper.findAllComponents(Button)
|
||||
const settingsButton = buttons[2]
|
||||
const settingsButton = buttons[0]
|
||||
|
||||
// Click the settings button
|
||||
await settingsButton.trigger('click')
|
||||
@@ -178,9 +167,9 @@ describe('CurrentUserPopover', () => {
|
||||
it('calls logout function and emits close event when logout button is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Find all buttons and get the logout button (last button)
|
||||
// Find all buttons and get the logout button (second one)
|
||||
const buttons = wrapper.findAllComponents(Button)
|
||||
const logoutButton = buttons[4]
|
||||
const logoutButton = buttons[1]
|
||||
|
||||
// Click the logout button
|
||||
await logoutButton.trigger('click')
|
||||
@@ -196,16 +185,16 @@ describe('CurrentUserPopover', () => {
|
||||
it('opens API pricing docs and emits close event when API pricing button is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Find all buttons and get the Partner Nodes info button (first one)
|
||||
// Find all buttons and get the API pricing button (third one now)
|
||||
const buttons = wrapper.findAllComponents(Button)
|
||||
const partnerNodesButton = buttons[0]
|
||||
const apiPricingButton = buttons[2]
|
||||
|
||||
// Click the Partner Nodes button
|
||||
await partnerNodesButton.trigger('click')
|
||||
// Click the API pricing button
|
||||
await apiPricingButton.trigger('click')
|
||||
|
||||
// Verify window.open was called with the correct URL
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes',
|
||||
'https://docs.comfy.org/tutorials/api-nodes/pricing',
|
||||
'_blank'
|
||||
)
|
||||
|
||||
@@ -217,9 +206,9 @@ describe('CurrentUserPopover', () => {
|
||||
it('opens top-up dialog and emits close event when top-up button is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Find all buttons and get the top-up button (second one)
|
||||
// Find all buttons and get the top-up button (last one)
|
||||
const buttons = wrapper.findAllComponents(Button)
|
||||
const topUpButton = buttons[1]
|
||||
const topUpButton = buttons[buttons.length - 1]
|
||||
|
||||
// Click the top-up button
|
||||
await topUpButton.trigger('click')
|
||||
|
||||
@@ -23,38 +23,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isActiveSubscription" class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<UserCredit text-class="text-2xl" />
|
||||
<Button
|
||||
:label="$t('subscription.partnerNodesCredits')"
|
||||
severity="secondary"
|
||||
text
|
||||
size="small"
|
||||
class="pl-6 p-0 h-auto justify-start"
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'hover:bg-transparent active:bg-transparent'
|
||||
}
|
||||
}"
|
||||
@click="handleOpenPartnerNodesInfo"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
:label="$t('credits.topUp.topUp')"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
@click="handleTopUp"
|
||||
/>
|
||||
</div>
|
||||
<SubscribeButton
|
||||
v-else
|
||||
:label="$t('subscription.subscribeToComfyCloud')"
|
||||
size="small"
|
||||
variant="gradient"
|
||||
@subscribed="handleSubscribed"
|
||||
/>
|
||||
|
||||
<Divider class="my-2" />
|
||||
|
||||
<Button
|
||||
@@ -67,17 +35,6 @@
|
||||
@click="handleOpenUserSettings"
|
||||
/>
|
||||
|
||||
<Button
|
||||
v-if="isActiveSubscription"
|
||||
class="justify-start"
|
||||
:label="$t(planSettingsLabel)"
|
||||
icon="pi pi-receipt"
|
||||
text
|
||||
fluid
|
||||
severity="secondary"
|
||||
@click="handleOpenPlanAndCreditsSettings"
|
||||
/>
|
||||
|
||||
<Divider class="my-2" />
|
||||
|
||||
<Button
|
||||
@@ -89,6 +46,34 @@
|
||||
severity="secondary"
|
||||
@click="handleLogout"
|
||||
/>
|
||||
|
||||
<Divider class="my-2" />
|
||||
|
||||
<Button
|
||||
class="justify-start"
|
||||
:label="$t('credits.apiPricing')"
|
||||
icon="pi pi-external-link"
|
||||
text
|
||||
fluid
|
||||
severity="secondary"
|
||||
@click="handleOpenApiPricing"
|
||||
/>
|
||||
|
||||
<Divider class="my-2" />
|
||||
|
||||
<div class="flex w-full flex-col gap-2 p-2">
|
||||
<div class="text-sm text-muted">
|
||||
{{ $t('credits.yourCreditBalance') }}
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<UserCredit text-class="text-2xl" />
|
||||
<Button
|
||||
v-if="isActiveSubscription"
|
||||
:label="$t('credits.topUp.topUp')"
|
||||
@click="handleTopUp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -101,60 +86,37 @@ import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const planSettingsLabel = isCloud
|
||||
? 'settingsCategories.PlanCredits'
|
||||
: 'settingsCategories.Credits'
|
||||
|
||||
const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
useCurrentUser()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const dialogService = useDialogService()
|
||||
const { isActiveSubscription, fetchStatus } = useSubscription()
|
||||
const { isActiveSubscription } = useSubscription()
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
dialogService.showSettingsDialog('user')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleOpenPlanAndCreditsSettings = () => {
|
||||
if (isCloud) {
|
||||
dialogService.showSettingsDialog('subscription')
|
||||
} else {
|
||||
dialogService.showSettingsDialog('credits')
|
||||
}
|
||||
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleTopUp = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleOpenPartnerNodesInfo = () => {
|
||||
window.open(
|
||||
'https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes',
|
||||
'_blank'
|
||||
)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await handleSignOut()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleSubscribed = async () => {
|
||||
await fetchStatus()
|
||||
const handleOpenApiPricing = () => {
|
||||
window.open('https://docs.comfy.org/tutorials/api-nodes/pricing', '_blank')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
>
|
||||
{{ badge.label }}
|
||||
</div>
|
||||
<div class="text-sm font-inter">{{ badge.text }}</div>
|
||||
<div class="text-sm font-semibold">{{ badge.text }}</div>
|
||||
<div v-if="badge.tooltip" class="text-xs">
|
||||
{{ badge.tooltip }}
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@
|
||||
>
|
||||
{{ badge.label }}
|
||||
</div>
|
||||
<div class="text-sm font-inter">{{ badge.text }}</div>
|
||||
<div class="text-sm font-semibold">{{ badge.text }}</div>
|
||||
<div v-if="badge.tooltip" class="text-xs">
|
||||
{{ badge.tooltip }}
|
||||
</div>
|
||||
@@ -117,7 +117,7 @@
|
||||
>
|
||||
{{ badge.label }}
|
||||
</div>
|
||||
<div class="font-inter text-sm" :class="textClasses">
|
||||
<div class="font-inter text-sm font-extrabold" :class="textClasses">
|
||||
{{ badge.text }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,7 +177,7 @@ const textClasses = computed(() => {
|
||||
return 'text-warning-100'
|
||||
case 'info':
|
||||
default:
|
||||
return 'text-text-primary'
|
||||
return 'text-slate-100'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { FirebaseError } from 'firebase/app'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useTopupTrackerStore } from '@/stores/topupTrackerStore'
|
||||
import { usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
@@ -18,6 +17,7 @@ export const useFirebaseAuthActions = () => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const toastStore = useToastStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
|
||||
|
||||
const accessError = ref(false)
|
||||
@@ -55,19 +55,14 @@ export const useFirebaseAuthActions = () => {
|
||||
life: 5000
|
||||
})
|
||||
|
||||
// CRITICAL: Use full page navigation for logout to prevent stale app state
|
||||
// Issue: SPA routing during logout can leave extensions loaded with stale auth state
|
||||
// This causes subscription dialogs to appear incorrectly during re-login onboarding
|
||||
// Full page reload ensures complete app state reset and proper onboarding flow
|
||||
// Redirect to login page if we're on cloud domain
|
||||
const hostname = window.location.hostname
|
||||
if (hostname.includes('cloud.comfy.org')) {
|
||||
if (route.query.inviteCode) {
|
||||
const inviteCode = Array.isArray(route.query.inviteCode)
|
||||
? route.query.inviteCode[0]
|
||||
: route.query.inviteCode
|
||||
window.location.href = `/cloud/login?inviteCode=${encodeURIComponent(inviteCode || '')}`
|
||||
const inviteCode = route.query.inviteCode
|
||||
await router.push({ name: 'cloud-login', query: { inviteCode } })
|
||||
} else {
|
||||
window.location.href = '/cloud/login'
|
||||
await router.push({ name: 'cloud-login' })
|
||||
}
|
||||
}
|
||||
}, reportError)
|
||||
@@ -99,7 +94,7 @@ export const useFirebaseAuthActions = () => {
|
||||
)
|
||||
}
|
||||
|
||||
useTopupTrackerStore().startTopup(amount)
|
||||
// Go to Stripe checkout page
|
||||
window.open(response.checkout_url, '_blank')
|
||||
}, reportError)
|
||||
|
||||
@@ -116,9 +111,7 @@ export const useFirebaseAuthActions = () => {
|
||||
}, reportError)
|
||||
|
||||
const fetchBalance = wrapWithErrorHandlingAsync(async () => {
|
||||
const result = await authStore.fetchBalance()
|
||||
void useTopupTrackerStore().reconcileByFetchingEvents()
|
||||
return result
|
||||
return await authStore.fetchBalance()
|
||||
}, reportError)
|
||||
|
||||
const signInWithGoogle = (errorHandler = reportError) =>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import WorkflowTemplateSelectorDialog from '@/components/custom/widget/WorkflowTemplateSelectorDialog.vue'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -13,9 +12,7 @@ export const useWorkflowTemplateSelectorDialog = () => {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show(source: 'sidebar' | 'menu' | 'command' = 'command') {
|
||||
useTelemetry()?.trackTemplateLibraryOpened({ source })
|
||||
|
||||
function show() {
|
||||
dialogService.showLayoutDialog({
|
||||
key: DIALOG_KEY,
|
||||
component: WorkflowTemplateSelectorDialog,
|
||||
|
||||
@@ -131,6 +131,38 @@
|
||||
"errorNotSupported": "API الحافظة غير مدعوم في متصفحك",
|
||||
"successMessage": "تم النسخ إلى الحافظة"
|
||||
},
|
||||
"cloudClaimInvite_claimButton": "استلام الدعوة",
|
||||
"cloudClaimInvite_processingTitle": "معالجة رمز الدعوة...",
|
||||
"cloudFooter_needHelp": "تحتاج مساعدة؟",
|
||||
"cloudForgotPassword_backToLogin": "العودة لتسجيل الدخول",
|
||||
"cloudForgotPassword_didntReceiveEmail": "لم تستلم بريداً إلكترونياً؟",
|
||||
"cloudForgotPassword_emailLabel": "البريد الإلكتروني",
|
||||
"cloudForgotPassword_emailPlaceholder": "أدخل بريدك الإلكتروني",
|
||||
"cloudForgotPassword_emailRequired": "البريد الإلكتروني حقل إجباري",
|
||||
"cloudForgotPassword_instructions": "أدخل عنوان بريدك الإلكتروني وسنرسل لك رابطاً لإعادة تعيين كلمة المرور.",
|
||||
"cloudForgotPassword_passwordResetError": "فشل في إرسال بريد إعادة تعيين كلمة المرور",
|
||||
"cloudForgotPassword_passwordResetSent": "تم إرسال إعادة تعيين كلمة المرور",
|
||||
"cloudForgotPassword_sendResetLink": "إرسال رابط إعادة التعيين",
|
||||
"cloudForgotPassword_title": "نسيت كلمة المرور",
|
||||
"cloudPrivateBeta_desc": "سجل الدخول للانضمام إلى قائمة الانتظار. سنخطرك عندما يصبح الوصول للبيتا متاحاً. تم إخطارك بالفعل؟ سجل الدخول لبدء استخدام Comfy Cloud.",
|
||||
"cloudPrivateBeta_title": "Cloud حالياً في مرحلة البيتا الخاصة",
|
||||
"cloudSorryContactSupport_title": "عذراً، اتصل بالدعم",
|
||||
"cloudStart_desc": "لا حاجة للإعداد. يعمل على أي جهاز.",
|
||||
"cloudStart_download": "تحميل ComfyUI",
|
||||
"cloudStart_explain": "أنتج عدة نتائج في آنٍ واحد. شارك مشاريعك بسهولة.",
|
||||
"cloudStart_learnAboutButton": "تعلم عن Cloud",
|
||||
"cloudStart_title": "ابدأ الإبداع فوراً، في ثوانٍ قليلة",
|
||||
"cloudStart_wantToRun": "تريد تشغيل ComfyUI محلياً بدلاً من ذلك؟",
|
||||
"cloudSurvey_steps_familiarity": "ما هو مستوى خبرتك مع ComfyUI؟",
|
||||
"cloudSurvey_steps_industry": "ما هي صناعتك الأساسية؟",
|
||||
"cloudSurvey_steps_making": "ما نوع المحتوى الذي تخطط لإبداعه؟",
|
||||
"cloudSurvey_steps_purpose": "ما هو الاستخدام الرئيسي لـ ComfyUI بالنسبة لك؟",
|
||||
"cloudVerifyEmail_title": "التحقق من البريد الإلكتروني",
|
||||
"cloudWaitlist_contactLink": "هنا",
|
||||
"cloudWaitlist_message": "تم تسجيلك في قائمة الانتظار. سنخطرك عندما يصبح الوصول للبيتا متاحاً.",
|
||||
"cloudWaitlist_questionsText": "لديك استفسارات؟ تواصل معنا",
|
||||
"cloudWaitlist_titleLine1": "أنت في",
|
||||
"cloudWaitlist_titleLine2": "قائمة الانتظار 🎉",
|
||||
"color": {
|
||||
"black": "أسود",
|
||||
"blue": "أزرق",
|
||||
@@ -1942,98 +1974,5 @@
|
||||
"label": "عناصر التحكم في التكبير",
|
||||
"showMinimap": "إظهار الخريطة المصغرة",
|
||||
"zoomToFit": "تكبير لتناسب الشاشة"
|
||||
},
|
||||
"cloudFooter_needHelp": "تحتاج مساعدة؟",
|
||||
"cloudStart_title": "ابدأ الإبداع فوراً، في ثوانٍ قليلة",
|
||||
"cloudStart_desc": "لا حاجة للإعداد. يعمل على أي جهاز.",
|
||||
"cloudStart_explain": "أنتج عدة نتائج في آنٍ واحد. شارك مشاريعك بسهولة.",
|
||||
"cloudStart_learnAboutButton": "تعلم عن Cloud",
|
||||
"cloudStart_wantToRun": "تريد تشغيل ComfyUI محلياً بدلاً من ذلك؟",
|
||||
"cloudStart_download": "تحميل ComfyUI",
|
||||
"cloudWaitlist_titleLine1": "أنت في",
|
||||
"cloudWaitlist_titleLine2": "قائمة الانتظار 🎉",
|
||||
"cloudWaitlist_message": "تم تسجيلك في قائمة الانتظار. سنخطرك عندما يصبح الوصول للبيتا متاحاً.",
|
||||
"cloudWaitlist_questionsText": "لديك استفسارات؟ تواصل معنا",
|
||||
"cloudWaitlist_contactLink": "هنا",
|
||||
"cloudClaimInvite_processingTitle": "معالجة رمز الدعوة...",
|
||||
"cloudClaimInvite_claimButton": "استلام الدعوة",
|
||||
"cloudSorryContactSupport_title": "عذراً، اتصل بالدعم",
|
||||
"cloudPrivateBeta_title": "Cloud حالياً في مرحلة البيتا الخاصة",
|
||||
"cloudPrivateBeta_desc": "سجل الدخول للانضمام إلى قائمة الانتظار. سنخطرك عندما يصبح الوصول للبيتا متاحاً. تم إخطارك بالفعل؟ سجل الدخول لبدء استخدام Comfy Cloud.",
|
||||
"cloudForgotPassword_title": "نسيت كلمة المرور",
|
||||
"cloudForgotPassword_instructions": "أدخل عنوان بريدك الإلكتروني وسنرسل لك رابطاً لإعادة تعيين كلمة المرور.",
|
||||
"cloudForgotPassword_emailLabel": "البريد الإلكتروني",
|
||||
"cloudForgotPassword_emailPlaceholder": "أدخل بريدك الإلكتروني",
|
||||
"cloudForgotPassword_sendResetLink": "إرسال رابط إعادة التعيين",
|
||||
"cloudForgotPassword_backToLogin": "العودة لتسجيل الدخول",
|
||||
"cloudForgotPassword_didntReceiveEmail": "لم تستلم بريداً إلكترونياً؟",
|
||||
"cloudForgotPassword_emailRequired": "البريد الإلكتروني حقل إجباري",
|
||||
"cloudForgotPassword_passwordResetSent": "تم إرسال إعادة تعيين كلمة المرور",
|
||||
"cloudForgotPassword_passwordResetError": "فشل في إرسال بريد إعادة تعيين كلمة المرور",
|
||||
"cloudSurvey_steps_familiarity": "ما هو مستوى خبرتك مع ComfyUI؟",
|
||||
"cloudSurvey_steps_purpose": "ما هو الاستخدام الرئيسي لـ ComfyUI بالنسبة لك؟",
|
||||
"cloudSurvey_steps_industry": "ما هي صناعتك الأساسية؟",
|
||||
"cloudSurvey_steps_making": "ما نوع المحتوى الذي تخطط لإبداعه؟",
|
||||
"desktopStart": {
|
||||
"initialising": "جارٍ التهيئة..."
|
||||
},
|
||||
"shape": {
|
||||
"default": "افتراضي",
|
||||
"round": "دائري",
|
||||
"CARD": "بطاقة",
|
||||
"circle": "دائرة",
|
||||
"arrow": "سهم",
|
||||
"box": "صندوق"
|
||||
},
|
||||
"commands": {
|
||||
"runWorkflow": "تشغيل سير العمل",
|
||||
"runWorkflowFront": "تشغيل سير العمل (إضافة في المقدمة)",
|
||||
"run": "تشغيل",
|
||||
"execute": "تنفيذ",
|
||||
"interrupt": "إلغاء التشغيل الحالي",
|
||||
"refresh": "تحديث تعريفات العقد",
|
||||
"clipspace": "فتح مساحة القص",
|
||||
"resetView": "إعادة تعيين عرض اللوحة",
|
||||
"clear": "مسح سير العمل",
|
||||
"toggleBottomPanel": "تبديل اللوحة السفلية",
|
||||
"theme": "المظهر",
|
||||
"dark": "داكن",
|
||||
"light": "فاتح",
|
||||
"manageExtensions": "إدارة الإضافات",
|
||||
"settings": "الإعدادات",
|
||||
"help": "مساعدة",
|
||||
"queue": "لوحة قائمة الانتظار"
|
||||
},
|
||||
"widgets": {
|
||||
"selectModel": "اختر نموذج",
|
||||
"uploadSelect": {
|
||||
"placeholder": "اختر...",
|
||||
"placeholderImage": "اختر صورة...",
|
||||
"placeholderAudio": "اختر صوت...",
|
||||
"placeholderVideo": "اختر فيديو...",
|
||||
"placeholderModel": "اختر نموذج...",
|
||||
"placeholderUnknown": "اختر وسائط..."
|
||||
}
|
||||
},
|
||||
"assetBrowser": {
|
||||
"assets": "الأصول",
|
||||
"browseAssets": "تصفح الأصول",
|
||||
"noAssetsFound": "لم يتم العثور على أصول",
|
||||
"tryAdjustingFilters": "حاول تعديل البحث أو المرشحات",
|
||||
"loadingModels": "جارٍ تحميل {type}...",
|
||||
"connectionError": "يرجى التحقق من اتصالك والمحاولة مرة أخرى",
|
||||
"failedToCreateNode": "فشل إنشاء العقدة. يرجى المحاولة مرة أخرى أو التحقق من وحدة التحكم للحصول على التفاصيل.",
|
||||
"noModelsInFolder": "لا توجد {type} متاحة في هذا المجلد",
|
||||
"searchAssetsPlaceholder": "البحث في الأصول...",
|
||||
"allModels": "جميع النماذج",
|
||||
"allCategory": "جميع {category}",
|
||||
"unknown": "غير معروف",
|
||||
"fileFormats": "تنسيقات الملفات",
|
||||
"baseModels": "النماذج الأساسية",
|
||||
"sortBy": "ترتيب حسب",
|
||||
"sortAZ": "أ-ي",
|
||||
"sortZA": "ي-أ",
|
||||
"sortRecent": "الأحدث",
|
||||
"sortPopular": "الأكثر شعبية"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1931,24 +1931,18 @@
|
||||
"renewsDate": "Renews {date}",
|
||||
"expiresDate": "Expires {date}",
|
||||
"manageSubscription": "Manage subscription",
|
||||
"partnerNodesBalance": "\"Partner Nodes\" Credit Balance",
|
||||
"partnerNodesDescription": "For running commercial/proprietary models",
|
||||
"apiNodesBalance": "\"API Nodes\" Credit Balance",
|
||||
"apiNodesDescription": "For running commercial/proprietary models",
|
||||
"totalCredits": "Total credits",
|
||||
"viewUsageHistory": "View usage history",
|
||||
"addApiCredits": "Add API credits",
|
||||
"addCredits": "Add credits",
|
||||
"monthlyCreditsRollover": "These credits will rollover to the next month",
|
||||
"monthlyBonusDescription": "Monthly credit bonus",
|
||||
"prepaidDescription": "Pre-paid credits",
|
||||
"prepaidCreditsInfo": "Credits purchased separately that don't expire",
|
||||
"nextBillingCycle": "next billing cycle",
|
||||
"yourPlanIncludes": "Your plan includes:",
|
||||
"viewMoreDetails": "View more details",
|
||||
"learnMore": "Learn more",
|
||||
"messageSupport": "Message support",
|
||||
"invoiceHistory": "Invoice history",
|
||||
"benefits": {
|
||||
"benefit1": "Monthly credits for Partner Nodes — top up when needed",
|
||||
"benefit1": "$10 in monthly credits for API models — top up when needed",
|
||||
"benefit2": "Up to 30 min runtime per job"
|
||||
},
|
||||
"required": {
|
||||
@@ -1957,9 +1951,7 @@
|
||||
"subscribe": "Subscribe"
|
||||
},
|
||||
"subscribeToRun": "Subscribe to Run",
|
||||
"subscribeNow": "Subscribe Now",
|
||||
"subscribeToComfyCloud": "Subscribe to Comfy Cloud",
|
||||
"partnerNodesCredits": "Partner Nodes credits"
|
||||
"subscribeNow": "Subscribe Now"
|
||||
},
|
||||
"userSettings": {
|
||||
"title": "User Settings",
|
||||
@@ -2122,18 +2114,7 @@
|
||||
"authTimeout": {
|
||||
"title": "Connection Taking Too Long",
|
||||
"message": "We're having trouble connecting to ComfyUI Cloud. This could be due to a slow connection or temporary service issue.",
|
||||
"restart": "Sign Out & Try Again",
|
||||
"troubleshooting": "Common causes:",
|
||||
"causes": [
|
||||
"Corporate firewall or proxy blocking authentication services",
|
||||
"VPN or network restrictions",
|
||||
"Browser extensions interfering with requests",
|
||||
"Regional network limitations",
|
||||
"Try a different browser or network"
|
||||
],
|
||||
"technicalDetails": "Technical Details",
|
||||
"helpText": "Need help? Contact",
|
||||
"supportLink": "support"
|
||||
"restart": "Sign Out & Try Again"
|
||||
}
|
||||
},
|
||||
"cloudFooter_needHelp": "Need Help?",
|
||||
@@ -2171,6 +2152,18 @@
|
||||
"cloudSurvey_steps_purpose": "What will you primarily use ComfyUI for?",
|
||||
"cloudSurvey_steps_industry": "What's your primary industry?",
|
||||
"cloudSurvey_steps_making": "What do you plan on making?",
|
||||
"cloudVerifyEmail_toast_message": "We've sent a verification email to {email}. Please check your inbox and click the link to verify your email address.",
|
||||
"cloudVerifyEmail_failed_toast_message": "Failed to send verification email. Please contact support.",
|
||||
"cloudVerifyEmail_title": "Check your email",
|
||||
"cloudVerifyEmail_back": "Back",
|
||||
"cloudVerifyEmail_sent": "A verification link was sent to:",
|
||||
"cloudVerifyEmail_clickToContinue": "Click the link in that email to automatically\ncontinue onto the next steps.",
|
||||
"cloudVerifyEmail_tip": "Tip: Don’t forget to check your spam folder\nif you don’t see it.",
|
||||
"cloudVerifyEmail_didntReceive": "Didn't receive the email?",
|
||||
"cloudVerifyEmail_resend": "Resend email",
|
||||
"cloudVerifyEmail_toast_title": "Email sent",
|
||||
"cloudVerifyEmail_toast_summary": "Check your inbox for a new verification email.",
|
||||
"cloudVerifyEmail_toast_failed": "Failed to send verification email. Please try again.",
|
||||
"cloudInvite_title": "YOU'RE INVITED",
|
||||
"cloudInvite_subtitle": "This invite can only be used once. Double check you’re signed into the account you want to use.",
|
||||
"cloudInvite_switchAccounts": "Switch accounts",
|
||||
|
||||
@@ -131,6 +131,38 @@
|
||||
"errorNotSupported": "API del portapapeles no soportada en su navegador",
|
||||
"successMessage": "Copiado al portapapeles"
|
||||
},
|
||||
"cloudClaimInvite_claimButton": "Reclamar Invitación",
|
||||
"cloudClaimInvite_processingTitle": "Procesando código de invitación...",
|
||||
"cloudFooter_needHelp": "¿Necesitas ayuda?",
|
||||
"cloudForgotPassword_backToLogin": "Volver al inicio de sesión",
|
||||
"cloudForgotPassword_didntReceiveEmail": "¿No recibiste un correo?",
|
||||
"cloudForgotPassword_emailLabel": "Correo",
|
||||
"cloudForgotPassword_emailPlaceholder": "Ingresa tu correo",
|
||||
"cloudForgotPassword_emailRequired": "El correo es obligatorio",
|
||||
"cloudForgotPassword_instructions": "Ingresa tu dirección de correo y te enviaremos un enlace para restablecer tu contraseña.",
|
||||
"cloudForgotPassword_passwordResetError": "Error al enviar correo de restablecimiento",
|
||||
"cloudForgotPassword_passwordResetSent": "Restablecimiento de contraseña enviado",
|
||||
"cloudForgotPassword_sendResetLink": "Enviar enlace de restablecimiento",
|
||||
"cloudForgotPassword_title": "Olvidé mi Contraseña",
|
||||
"cloudPrivateBeta_desc": "Inicia sesión para unirte a la lista de espera. Te notificaremos cuando tengas acceso al beta. ¿Ya recibiste la notificación? Inicia sesión para empezar a usar Comfy Cloud.",
|
||||
"cloudPrivateBeta_title": "Cloud está actualmente en beta privada",
|
||||
"cloudSorryContactSupport_title": "Lo sentimos, contacta al soporte",
|
||||
"cloudStart_desc": "Sin configuración necesaria. Funciona en cualquier dispositivo.",
|
||||
"cloudStart_download": "Descargar ComfyUI",
|
||||
"cloudStart_explain": "Genera múltiples resultados simultáneamente. Comparte proyectos fácilmente.",
|
||||
"cloudStart_learnAboutButton": "Conoce Cloud",
|
||||
"cloudStart_title": "Crea al instante, en segundos",
|
||||
"cloudStart_wantToRun": "¿Prefieres ejecutar ComfyUI localmente?",
|
||||
"cloudSurvey_steps_familiarity": "¿Cuál es tu nivel de experiencia con ComfyUI?",
|
||||
"cloudSurvey_steps_industry": "¿Cuál es tu industria principal?",
|
||||
"cloudSurvey_steps_making": "¿Qué tipo de contenido planeas crear?",
|
||||
"cloudSurvey_steps_purpose": "¿Cuál será tu uso principal de ComfyUI?",
|
||||
"cloudVerifyEmail_title": "Verificación de Correo",
|
||||
"cloudWaitlist_contactLink": "aquí",
|
||||
"cloudWaitlist_message": "Has sido añadido a la lista de espera. Te notificaremos cuando tengas acceso al beta.",
|
||||
"cloudWaitlist_questionsText": "¿Dudas? Contáctanos",
|
||||
"cloudWaitlist_titleLine1": "ESTÁS EN LA",
|
||||
"cloudWaitlist_titleLine2": "LISTA DE ESPERA 🎉",
|
||||
"color": {
|
||||
"black": "Negro",
|
||||
"blue": "Azul",
|
||||
@@ -1939,39 +1971,5 @@
|
||||
"label": "Controles de zoom",
|
||||
"showMinimap": "Mostrar minimapa",
|
||||
"zoomToFit": "Ajustar al zoom"
|
||||
},
|
||||
"cloudFooter_needHelp": "¿Necesitas ayuda?",
|
||||
"cloudStart_title": "Crea al instante, en segundos",
|
||||
"cloudStart_desc": "Sin configuración necesaria. Funciona en cualquier dispositivo.",
|
||||
"cloudStart_explain": "Genera múltiples resultados simultáneamente. Comparte proyectos fácilmente.",
|
||||
"cloudStart_learnAboutButton": "Conoce Cloud",
|
||||
"cloudStart_wantToRun": "¿Prefieres ejecutar ComfyUI localmente?",
|
||||
"cloudStart_download": "Descargar ComfyUI",
|
||||
"cloudWaitlist_titleLine1": "ESTÁS EN LA",
|
||||
"cloudWaitlist_titleLine2": "LISTA DE ESPERA 🎉",
|
||||
"cloudWaitlist_message": "Has sido añadido a la lista de espera. Te notificaremos cuando tengas acceso al beta.",
|
||||
"cloudWaitlist_questionsText": "¿Dudas? Contáctanos",
|
||||
"cloudWaitlist_contactLink": "aquí",
|
||||
"cloudClaimInvite_processingTitle": "Procesando código de invitación...",
|
||||
"cloudClaimInvite_claimButton": "Reclamar Invitación",
|
||||
"cloudSorryContactSupport_title": "Lo sentimos, contacta al soporte",
|
||||
"cloudPrivateBeta_title": "Cloud está actualmente en beta privada",
|
||||
"cloudPrivateBeta_desc": "Inicia sesión para unirte a la lista de espera. Te notificaremos cuando tengas acceso al beta. ¿Ya recibiste la notificación? Inicia sesión para empezar a usar Comfy Cloud.",
|
||||
"cloudForgotPassword_title": "Olvidé mi Contraseña",
|
||||
"cloudForgotPassword_instructions": "Ingresa tu dirección de correo y te enviaremos un enlace para restablecer tu contraseña.",
|
||||
"cloudForgotPassword_emailLabel": "Correo",
|
||||
"cloudForgotPassword_emailPlaceholder": "Ingresa tu correo",
|
||||
"cloudForgotPassword_sendResetLink": "Enviar enlace de restablecimiento",
|
||||
"cloudForgotPassword_backToLogin": "Volver al inicio de sesión",
|
||||
"cloudForgotPassword_didntReceiveEmail": "¿No recibiste un correo?",
|
||||
"cloudForgotPassword_emailRequired": "El correo es obligatorio",
|
||||
"cloudForgotPassword_passwordResetSent": "Restablecimiento de contraseña enviado",
|
||||
"cloudForgotPassword_passwordResetError": "Error al enviar correo de restablecimiento",
|
||||
"cloudSurvey_steps_familiarity": "¿Cuál es tu nivel de experiencia con ComfyUI?",
|
||||
"cloudSurvey_steps_purpose": "¿Cuál será tu uso principal de ComfyUI?",
|
||||
"cloudSurvey_steps_industry": "¿Cuál es tu industria principal?",
|
||||
"cloudSurvey_steps_making": "¿Qué tipo de contenido planeas crear?",
|
||||
"desktopStart": {
|
||||
"initialising": "Inicializando..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,38 @@
|
||||
"errorNotSupported": "L'API du presse-papiers n'est pas prise en charge par votre navigateur",
|
||||
"successMessage": "Copié dans le presse-papiers"
|
||||
},
|
||||
"cloudClaimInvite_claimButton": "Réclamer l'Invitation",
|
||||
"cloudClaimInvite_processingTitle": "Traitement du code d'invitation...",
|
||||
"cloudFooter_needHelp": "Besoin d'aide ?",
|
||||
"cloudForgotPassword_backToLogin": "Retour à la connexion",
|
||||
"cloudForgotPassword_didntReceiveEmail": "Vous n'avez pas reçu d'email ?",
|
||||
"cloudForgotPassword_emailLabel": "Email",
|
||||
"cloudForgotPassword_emailPlaceholder": "Entrez votre email",
|
||||
"cloudForgotPassword_emailRequired": "L'email est obligatoire",
|
||||
"cloudForgotPassword_instructions": "Entrez votre adresse email et nous vous enverrons un lien pour réinitialiser votre mot de passe.",
|
||||
"cloudForgotPassword_passwordResetError": "Échec de l'envoi de l'email de réinitialisation",
|
||||
"cloudForgotPassword_passwordResetSent": "Réinitialisation de mot de passe envoyée",
|
||||
"cloudForgotPassword_sendResetLink": "Envoyer le lien de réinitialisation",
|
||||
"cloudForgotPassword_title": "Mot de passe oublié",
|
||||
"cloudPrivateBeta_desc": "Connectez-vous pour rejoindre la liste d'attente. Nous vous préviendrons quand vous aurez accès à la bêta. Déjà notifié ? Connectez-vous pour commencer à utiliser Comfy Cloud.",
|
||||
"cloudPrivateBeta_title": "Cloud est actuellement en bêta privée",
|
||||
"cloudSorryContactSupport_title": "Désolé, contactez le support",
|
||||
"cloudStart_desc": "Aucune configuration requise. Fonctionne sur n'importe quel appareil.",
|
||||
"cloudStart_download": "Télécharger ComfyUI",
|
||||
"cloudStart_explain": "Générez plusieurs résultats simultanément. Partagez vos projets facilement.",
|
||||
"cloudStart_learnAboutButton": "En savoir plus sur Cloud",
|
||||
"cloudStart_title": "Créez instantanément, en quelques secondes",
|
||||
"cloudStart_wantToRun": "Vous préférez exécuter ComfyUI localement ?",
|
||||
"cloudSurvey_steps_familiarity": "Quel est votre niveau d'expérience avec ComfyUI ?",
|
||||
"cloudSurvey_steps_industry": "Quelle est votre industrie principale ?",
|
||||
"cloudSurvey_steps_making": "Quel type de contenu prévoyez-vous créer ?",
|
||||
"cloudSurvey_steps_purpose": "Quelle sera votre utilisation principale de ComfyUI ?",
|
||||
"cloudVerifyEmail_title": "Vérification d'Email",
|
||||
"cloudWaitlist_contactLink": "ici",
|
||||
"cloudWaitlist_message": "Vous avez été ajouté à la liste d'attente. Nous vous préviendrons quand vous aurez accès à la bêta.",
|
||||
"cloudWaitlist_questionsText": "Des questions ? Contactez-nous",
|
||||
"cloudWaitlist_titleLine1": "VOUS ÊTES SUR LA",
|
||||
"cloudWaitlist_titleLine2": "LISTE D'ATTENTE 🎉",
|
||||
"color": {
|
||||
"black": "Noir",
|
||||
"blue": "Bleu",
|
||||
@@ -1933,39 +1965,5 @@
|
||||
"enterFilename": "Entrez le nom du fichier",
|
||||
"exportWorkflow": "Exporter le flux de travail",
|
||||
"saveWorkflow": "Enregistrer le flux de travail"
|
||||
},
|
||||
"cloudFooter_needHelp": "Besoin d'aide ?",
|
||||
"cloudStart_title": "Créez instantanément, en quelques secondes",
|
||||
"cloudStart_desc": "Aucune configuration requise. Fonctionne sur n'importe quel appareil.",
|
||||
"cloudStart_explain": "Générez plusieurs résultats simultanément. Partagez vos projets facilement.",
|
||||
"cloudStart_learnAboutButton": "En savoir plus sur Cloud",
|
||||
"cloudStart_wantToRun": "Vous préférez exécuter ComfyUI localement ?",
|
||||
"cloudStart_download": "Télécharger ComfyUI",
|
||||
"cloudWaitlist_titleLine1": "VOUS ÊTES SUR LA",
|
||||
"cloudWaitlist_titleLine2": "LISTE D'ATTENTE 🎉",
|
||||
"cloudWaitlist_message": "Vous avez été ajouté à la liste d'attente. Nous vous préviendrons quand vous aurez accès à la bêta.",
|
||||
"cloudWaitlist_questionsText": "Des questions ? Contactez-nous",
|
||||
"cloudWaitlist_contactLink": "ici",
|
||||
"cloudClaimInvite_processingTitle": "Traitement du code d'invitation...",
|
||||
"cloudClaimInvite_claimButton": "Réclamer l'Invitation",
|
||||
"cloudSorryContactSupport_title": "Désolé, contactez le support",
|
||||
"cloudPrivateBeta_title": "Cloud est actuellement en bêta privée",
|
||||
"cloudPrivateBeta_desc": "Connectez-vous pour rejoindre la liste d'attente. Nous vous préviendrons quand vous aurez accès à la bêta. Déjà notifié ? Connectez-vous pour commencer à utiliser Comfy Cloud.",
|
||||
"cloudForgotPassword_title": "Mot de passe oublié",
|
||||
"cloudForgotPassword_instructions": "Entrez votre adresse email et nous vous enverrons un lien pour réinitialiser votre mot de passe.",
|
||||
"cloudForgotPassword_emailLabel": "Email",
|
||||
"cloudForgotPassword_emailPlaceholder": "Entrez votre email",
|
||||
"cloudForgotPassword_sendResetLink": "Envoyer le lien de réinitialisation",
|
||||
"cloudForgotPassword_backToLogin": "Retour à la connexion",
|
||||
"cloudForgotPassword_didntReceiveEmail": "Vous n'avez pas reçu d'email ?",
|
||||
"cloudForgotPassword_emailRequired": "L'email est obligatoire",
|
||||
"cloudForgotPassword_passwordResetSent": "Réinitialisation de mot de passe envoyée",
|
||||
"cloudForgotPassword_passwordResetError": "Échec de l'envoi de l'email de réinitialisation",
|
||||
"cloudSurvey_steps_familiarity": "Quel est votre niveau d'expérience avec ComfyUI ?",
|
||||
"cloudSurvey_steps_purpose": "Quelle sera votre utilisation principale de ComfyUI ?",
|
||||
"cloudSurvey_steps_industry": "Quelle est votre industrie principale ?",
|
||||
"cloudSurvey_steps_making": "Quel type de contenu prévoyez-vous créer ?",
|
||||
"desktopStart": {
|
||||
"initialising": "Initialisation..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,38 @@
|
||||
"errorNotSupported": "お使いのブラウザではクリップボードAPIがサポートされていません",
|
||||
"successMessage": "クリップボードにコピーしました"
|
||||
},
|
||||
"cloudClaimInvite_claimButton": "招待を申請",
|
||||
"cloudClaimInvite_processingTitle": "招待コードを処理中...",
|
||||
"cloudFooter_needHelp": "ヘルプが必要ですか?",
|
||||
"cloudForgotPassword_backToLogin": "ログインに戻る",
|
||||
"cloudForgotPassword_didntReceiveEmail": "メールが届きませんでしたか?",
|
||||
"cloudForgotPassword_emailLabel": "メール",
|
||||
"cloudForgotPassword_emailPlaceholder": "メールを入力",
|
||||
"cloudForgotPassword_emailRequired": "メールアドレスは必須です",
|
||||
"cloudForgotPassword_instructions": "メールアドレスを入力してください。パスワードリセット用のリンクをお送りします。",
|
||||
"cloudForgotPassword_passwordResetError": "パスワードリセットメールの送信に失敗しました",
|
||||
"cloudForgotPassword_passwordResetSent": "パスワードリセットを送信しました",
|
||||
"cloudForgotPassword_sendResetLink": "リセットリンクを送信",
|
||||
"cloudForgotPassword_title": "パスワードを忘れた",
|
||||
"cloudPrivateBeta_desc": "サインインしてウェイトリストに登録してください。ベータアクセスが可能になりましたらお知らせします。すでに通知を受け取りましたか?サインインしてComfy Cloudを始めてください。",
|
||||
"cloudPrivateBeta_title": "Cloudは現在プライベートベータ版です",
|
||||
"cloudSorryContactSupport_title": "申し訳ございません、サポートにお問い合わせください",
|
||||
"cloudStart_desc": "セットアップは不要。どのデバイスでも動作します。",
|
||||
"cloudStart_download": "ComfyUIをダウンロード",
|
||||
"cloudStart_explain": "複数の結果を同時生成。プロジェクトを簡単共有。",
|
||||
"cloudStart_learnAboutButton": "Cloudについて学ぶ",
|
||||
"cloudStart_title": "すぐに創作開始、たった数秒で",
|
||||
"cloudStart_wantToRun": "代わりにComfyUIをローカルで実行しますか?",
|
||||
"cloudSurvey_steps_familiarity": "ComfyUIの経験レベルはいかがですか?",
|
||||
"cloudSurvey_steps_industry": "あなたの主要な業界は何ですか?",
|
||||
"cloudSurvey_steps_making": "どのようなコンテンツを創作する予定ですか?",
|
||||
"cloudSurvey_steps_purpose": "ComfyUIの主な用途は何ですか?",
|
||||
"cloudVerifyEmail_title": "メール確認",
|
||||
"cloudWaitlist_contactLink": "こちら",
|
||||
"cloudWaitlist_message": "ウェイトリストに登録されました。ベータアクセスが可能になりましたらお知らせします。",
|
||||
"cloudWaitlist_questionsText": "ご不明な点がありますか?お問い合わせ",
|
||||
"cloudWaitlist_titleLine1": "あなたは",
|
||||
"cloudWaitlist_titleLine2": "ウェイトリストに登録されました 🎉",
|
||||
"color": {
|
||||
"black": "黒",
|
||||
"blue": "青",
|
||||
@@ -1939,39 +1971,5 @@
|
||||
"label": "ズームコントロール",
|
||||
"showMinimap": "ミニマップを表示",
|
||||
"zoomToFit": "全体表示にズーム"
|
||||
},
|
||||
"cloudFooter_needHelp": "ヘルプが必要ですか?",
|
||||
"cloudStart_title": "すぐに創作開始、たった数秒で",
|
||||
"cloudStart_desc": "セットアップは不要。どのデバイスでも動作します。",
|
||||
"cloudStart_explain": "複数の結果を同時生成。プロジェクトを簡単共有。",
|
||||
"cloudStart_learnAboutButton": "Cloudについて学ぶ",
|
||||
"cloudStart_wantToRun": "代わりにComfyUIをローカルで実行しますか?",
|
||||
"cloudStart_download": "ComfyUIをダウンロード",
|
||||
"cloudWaitlist_titleLine1": "あなたは",
|
||||
"cloudWaitlist_titleLine2": "ウェイトリストに登録されました 🎉",
|
||||
"cloudWaitlist_message": "ウェイトリストに登録されました。ベータアクセスが可能になりましたらお知らせします。",
|
||||
"cloudWaitlist_questionsText": "ご不明な点がありますか?お問い合わせ",
|
||||
"cloudWaitlist_contactLink": "こちら",
|
||||
"cloudClaimInvite_processingTitle": "招待コードを処理中...",
|
||||
"cloudClaimInvite_claimButton": "招待を申請",
|
||||
"cloudSorryContactSupport_title": "申し訳ございません、サポートにお問い合わせください",
|
||||
"cloudPrivateBeta_title": "Cloudは現在プライベートベータ版です",
|
||||
"cloudPrivateBeta_desc": "サインインしてウェイトリストに登録してください。ベータアクセスが可能になりましたらお知らせします。すでに通知を受け取りましたか?サインインしてComfy Cloudを始めてください。",
|
||||
"cloudForgotPassword_title": "パスワードを忘れた",
|
||||
"cloudForgotPassword_instructions": "メールアドレスを入力してください。パスワードリセット用のリンクをお送りします。",
|
||||
"cloudForgotPassword_emailLabel": "メール",
|
||||
"cloudForgotPassword_emailPlaceholder": "メールを入力",
|
||||
"cloudForgotPassword_sendResetLink": "リセットリンクを送信",
|
||||
"cloudForgotPassword_backToLogin": "ログインに戻る",
|
||||
"cloudForgotPassword_didntReceiveEmail": "メールが届きませんでしたか?",
|
||||
"cloudForgotPassword_emailRequired": "メールアドレスは必須です",
|
||||
"cloudForgotPassword_passwordResetSent": "パスワードリセットを送信しました",
|
||||
"cloudForgotPassword_passwordResetError": "パスワードリセットメールの送信に失敗しました",
|
||||
"cloudSurvey_steps_familiarity": "ComfyUIの経験レベルはいかがですか?",
|
||||
"cloudSurvey_steps_purpose": "ComfyUIの主な用途は何ですか?",
|
||||
"cloudSurvey_steps_industry": "あなたの主要な業界は何ですか?",
|
||||
"cloudSurvey_steps_making": "どのようなコンテンツを創作する予定ですか?",
|
||||
"desktopStart": {
|
||||
"initialising": "初期化中..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,38 @@
|
||||
"errorNotSupported": "브라우저가 클립보드 API를 지원하지 않습니다.",
|
||||
"successMessage": "클립보드에 복사됨"
|
||||
},
|
||||
"cloudClaimInvite_claimButton": "초대 요청하기",
|
||||
"cloudClaimInvite_processingTitle": "초대 코드 확인중...",
|
||||
"cloudFooter_needHelp": "도움이 필요하신가요?",
|
||||
"cloudForgotPassword_backToLogin": "로그인으로 돌아가기",
|
||||
"cloudForgotPassword_didntReceiveEmail": "이메일을 받지 못하셨나요?",
|
||||
"cloudForgotPassword_emailLabel": "이메일",
|
||||
"cloudForgotPassword_emailPlaceholder": "이메일을 입력하세요",
|
||||
"cloudForgotPassword_emailRequired": "이메일은 필수 입력 항목입니다",
|
||||
"cloudForgotPassword_instructions": "이메일 주소를 입력하시면 비밀번호 재설정 링크를 보내드리겠습니다.",
|
||||
"cloudForgotPassword_passwordResetError": "비밀번호 재설정 이메일 전송에 실패했습니다",
|
||||
"cloudForgotPassword_passwordResetSent": "비밀번호 재설정이 전송되었습니다",
|
||||
"cloudForgotPassword_sendResetLink": "재설정 링크 보내기",
|
||||
"cloudForgotPassword_title": "비밀번호 찾기",
|
||||
"cloudPrivateBeta_desc": "로그인하여 대기자 명단에 등록하세요. 베타 버전이 오픈될 때 알려드릴게요. 이미 알림을 받으셨다면? 로그인하여 Comfy Cloud를 시작해보세요.",
|
||||
"cloudPrivateBeta_title": "Cloud는 현재 비공개 베타 버전입니다",
|
||||
"cloudSorryContactSupport_title": "죄송합니다, 지원팀에 문의해주세요",
|
||||
"cloudStart_desc": "설정없이도, 기기의 제약없이",
|
||||
"cloudStart_download": "ComfyUI 다운로드",
|
||||
"cloudStart_explain": "한번에 다수의 결과물을, 워크플로우로 쉽게 공유해요.",
|
||||
"cloudStart_learnAboutButton": "Cloud에 대해 알아보기",
|
||||
"cloudStart_title": "창작은 단 몇 초면 충분해요",
|
||||
"cloudStart_wantToRun": "ComfyUI를 로컬에서 실행해보고 싶다면?",
|
||||
"cloudSurvey_steps_familiarity": "ComfyUI 경험도는 어떻게 되시나요?",
|
||||
"cloudSurvey_steps_industry": "어떤 업계/업종에 근무하시나요?",
|
||||
"cloudSurvey_steps_making": "어떤 종류의 콘텐츠를 만들 계획이신가요?",
|
||||
"cloudSurvey_steps_purpose": "ComfyUI의 주된 목적은 어떻게 되나요?",
|
||||
"cloudVerifyEmail_title": "이메일 확인",
|
||||
"cloudWaitlist_contactLink": "여기로",
|
||||
"cloudWaitlist_message": "대기자 명단에 등록되었습니다. 베타 버전이 오픈되면 알려드릴게요.",
|
||||
"cloudWaitlist_questionsText": "궁금한 점이 있으신가요? 문의는",
|
||||
"cloudWaitlist_titleLine1": "방금",
|
||||
"cloudWaitlist_titleLine2": "대기자 명단에 등록되었습니다 🎉",
|
||||
"color": {
|
||||
"black": "검정색",
|
||||
"blue": "파란색",
|
||||
@@ -1939,39 +1971,5 @@
|
||||
"label": "확대/축소 컨트롤",
|
||||
"showMinimap": "미니맵 표시",
|
||||
"zoomToFit": "화면에 맞게 확대"
|
||||
},
|
||||
"cloudFooter_needHelp": "도움이 필요하신가요?",
|
||||
"cloudStart_title": "창작은 단 몇 초면 충분해요",
|
||||
"cloudStart_desc": "설정없이도, 기기의 제약없이",
|
||||
"cloudStart_explain": "한번에 다수의 결과물을, 워크플로우로 쉽게 공유해요.",
|
||||
"cloudStart_learnAboutButton": "Cloud에 대해 알아보기",
|
||||
"cloudStart_wantToRun": "ComfyUI를 로컬에서 실행해보고 싶다면?",
|
||||
"cloudStart_download": "ComfyUI 다운로드",
|
||||
"cloudWaitlist_titleLine1": "방금",
|
||||
"cloudWaitlist_titleLine2": "대기자 명단에 등록되었습니다 🎉",
|
||||
"cloudWaitlist_message": "대기자 명단에 등록되었습니다. 베타 버전이 오픈되면 알려드릴게요.",
|
||||
"cloudWaitlist_questionsText": "궁금한 점이 있으신가요? 문의는",
|
||||
"cloudWaitlist_contactLink": "여기로",
|
||||
"cloudClaimInvite_processingTitle": "초대 코드 확인중...",
|
||||
"cloudClaimInvite_claimButton": "초대 요청하기",
|
||||
"cloudSorryContactSupport_title": "죄송합니다, 지원팀에 문의해주세요",
|
||||
"cloudPrivateBeta_title": "Cloud는 현재 비공개 베타 버전입니다",
|
||||
"cloudPrivateBeta_desc": "로그인하여 대기자 명단에 등록하세요. 베타 버전이 오픈될 때 알려드릴게요. 이미 알림을 받으셨다면? 로그인하여 Comfy Cloud를 시작해보세요.",
|
||||
"cloudForgotPassword_title": "비밀번호 찾기",
|
||||
"cloudForgotPassword_instructions": "이메일 주소를 입력하시면 비밀번호 재설정 링크를 보내드리겠습니다.",
|
||||
"cloudForgotPassword_emailLabel": "이메일",
|
||||
"cloudForgotPassword_emailPlaceholder": "이메일을 입력하세요",
|
||||
"cloudForgotPassword_sendResetLink": "재설정 링크 보내기",
|
||||
"cloudForgotPassword_backToLogin": "로그인으로 돌아가기",
|
||||
"cloudForgotPassword_didntReceiveEmail": "이메일을 받지 못하셨나요?",
|
||||
"cloudForgotPassword_emailRequired": "이메일은 필수 입력 항목입니다",
|
||||
"cloudForgotPassword_passwordResetSent": "비밀번호 재설정이 전송되었습니다",
|
||||
"cloudForgotPassword_passwordResetError": "비밀번호 재설정 이메일 전송에 실패했습니다",
|
||||
"cloudSurvey_steps_familiarity": "ComfyUI 경험도는 어떻게 되시나요?",
|
||||
"cloudSurvey_steps_purpose": "ComfyUI의 주된 목적은 어떻게 되나요?",
|
||||
"cloudSurvey_steps_industry": "어떤 업계/업종에 근무하시나요?",
|
||||
"cloudSurvey_steps_making": "어떤 종류의 콘텐츠를 만들 계획이신가요?",
|
||||
"desktopStart": {
|
||||
"initialising": "초기화 중..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,38 @@
|
||||
"errorNotSupported": "API буфера обмена не поддерживается в вашем браузере",
|
||||
"successMessage": "Скопировано в буфер обмена"
|
||||
},
|
||||
"cloudClaimInvite_claimButton": "Получить приглашение",
|
||||
"cloudClaimInvite_processingTitle": "Обработка кода приглашения...",
|
||||
"cloudFooter_needHelp": "Нужна помощь?",
|
||||
"cloudForgotPassword_backToLogin": "Вернуться к входу",
|
||||
"cloudForgotPassword_didntReceiveEmail": "Не получили email?",
|
||||
"cloudForgotPassword_emailLabel": "Email",
|
||||
"cloudForgotPassword_emailPlaceholder": "Введите ваш email",
|
||||
"cloudForgotPassword_emailRequired": "Email является обязательным полем",
|
||||
"cloudForgotPassword_instructions": "Введите ваш email-адрес, и мы отправим вам ссылку для сброса пароля.",
|
||||
"cloudForgotPassword_passwordResetError": "Не удалось отправить email сброса пароля",
|
||||
"cloudForgotPassword_passwordResetSent": "Сброс пароля отправлен",
|
||||
"cloudForgotPassword_sendResetLink": "Отправить ссылку сброса",
|
||||
"cloudForgotPassword_title": "Забыли пароль",
|
||||
"cloudPrivateBeta_desc": "Войдите, чтобы присоединиться к списку ожидания. Мы уведомим вас, когда будет доступен бета-доступ. Уже получили уведомление? Войдите и начните работать с Comfy Cloud.",
|
||||
"cloudPrivateBeta_title": "Cloud сейчас в приватной бете",
|
||||
"cloudSorryContactSupport_title": "Извините, свяжитесь с поддержкой",
|
||||
"cloudStart_desc": "Настройка не требуется. Работает на любом устройстве.",
|
||||
"cloudStart_download": "Скачать ComfyUI",
|
||||
"cloudStart_explain": "Генерируйте несколько результатов одновременно. Легко делитесь проектами.",
|
||||
"cloudStart_learnAboutButton": "Узнать о Cloud",
|
||||
"cloudStart_title": "Творите мгновенно, всего за секунды",
|
||||
"cloudStart_wantToRun": "Хотите запустить ComfyUI локально?",
|
||||
"cloudSurvey_steps_familiarity": "Каков ваш уровень опыта с ComfyUI?",
|
||||
"cloudSurvey_steps_industry": "Какая ваша основная отрасль?",
|
||||
"cloudSurvey_steps_making": "Какой тип контента вы планируете создавать?",
|
||||
"cloudSurvey_steps_purpose": "Каково основное назначение ComfyUI для вас?",
|
||||
"cloudVerifyEmail_title": "Подтверждение email",
|
||||
"cloudWaitlist_contactLink": "здесь",
|
||||
"cloudWaitlist_message": "Вы добавлены в список ожидания. Мы уведомим вас, когда будет доступен бета-доступ.",
|
||||
"cloudWaitlist_questionsText": "Есть вопросы? Обращайтесь",
|
||||
"cloudWaitlist_titleLine1": "ВЫ В",
|
||||
"cloudWaitlist_titleLine2": "СПИСКЕ ОЖИДАНИЯ 🎉",
|
||||
"color": {
|
||||
"black": "Черный",
|
||||
"blue": "Синий",
|
||||
@@ -1939,39 +1971,5 @@
|
||||
"label": "Элементы управления масштабом",
|
||||
"showMinimap": "Показать миникарту",
|
||||
"zoomToFit": "Масштабировать по размеру"
|
||||
},
|
||||
"cloudFooter_needHelp": "Нужна помощь?",
|
||||
"cloudStart_title": "Творите мгновенно, всего за секунды",
|
||||
"cloudStart_desc": "Настройка не требуется. Работает на любом устройстве.",
|
||||
"cloudStart_explain": "Генерируйте несколько результатов одновременно. Легко делитесь проектами.",
|
||||
"cloudStart_learnAboutButton": "Узнать о Cloud",
|
||||
"cloudStart_wantToRun": "Хотите запустить ComfyUI локально?",
|
||||
"cloudStart_download": "Скачать ComfyUI",
|
||||
"cloudWaitlist_titleLine1": "ВЫ В",
|
||||
"cloudWaitlist_titleLine2": "СПИСКЕ ОЖИДАНИЯ 🎉",
|
||||
"cloudWaitlist_message": "Вы добавлены в список ожидания. Мы уведомим вас, когда будет доступен бета-доступ.",
|
||||
"cloudWaitlist_questionsText": "Есть вопросы? Обращайтесь",
|
||||
"cloudWaitlist_contactLink": "здесь",
|
||||
"cloudClaimInvite_processingTitle": "Обработка кода приглашения...",
|
||||
"cloudClaimInvite_claimButton": "Получить приглашение",
|
||||
"cloudSorryContactSupport_title": "Извините, свяжитесь с поддержкой",
|
||||
"cloudPrivateBeta_title": "Cloud сейчас в приватной бете",
|
||||
"cloudPrivateBeta_desc": "Войдите, чтобы присоединиться к списку ожидания. Мы уведомим вас, когда будет доступен бета-доступ. Уже получили уведомление? Войдите и начните работать с Comfy Cloud.",
|
||||
"cloudForgotPassword_title": "Забыли пароль",
|
||||
"cloudForgotPassword_instructions": "Введите ваш email-адрес, и мы отправим вам ссылку для сброса пароля.",
|
||||
"cloudForgotPassword_emailLabel": "Email",
|
||||
"cloudForgotPassword_emailPlaceholder": "Введите ваш email",
|
||||
"cloudForgotPassword_sendResetLink": "Отправить ссылку сброса",
|
||||
"cloudForgotPassword_backToLogin": "Вернуться к входу",
|
||||
"cloudForgotPassword_didntReceiveEmail": "Не получили email?",
|
||||
"cloudForgotPassword_emailRequired": "Email является обязательным полем",
|
||||
"cloudForgotPassword_passwordResetSent": "Сброс пароля отправлен",
|
||||
"cloudForgotPassword_passwordResetError": "Не удалось отправить email сброса пароля",
|
||||
"cloudSurvey_steps_familiarity": "Каков ваш уровень опыта с ComfyUI?",
|
||||
"cloudSurvey_steps_purpose": "Каково основное назначение ComfyUI для вас?",
|
||||
"cloudSurvey_steps_industry": "Какая ваша основная отрасль?",
|
||||
"cloudSurvey_steps_making": "Какой тип контента вы планируете создавать?",
|
||||
"desktopStart": {
|
||||
"initialising": "Инициализация..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,38 @@
|
||||
"errorNotSupported": "您的瀏覽器不支援剪貼簿 API",
|
||||
"successMessage": "已複製到剪貼簿"
|
||||
},
|
||||
"cloudClaimInvite_claimButton": "領取邀請",
|
||||
"cloudClaimInvite_processingTitle": "處理邀請碼中...",
|
||||
"cloudFooter_needHelp": "需要幫助?",
|
||||
"cloudForgotPassword_backToLogin": "返回登入",
|
||||
"cloudForgotPassword_didntReceiveEmail": "沒有收到郵件?",
|
||||
"cloudForgotPassword_emailLabel": "郵箱",
|
||||
"cloudForgotPassword_emailPlaceholder": "輸入您的郵箱",
|
||||
"cloudForgotPassword_emailRequired": "郵箱為必填項",
|
||||
"cloudForgotPassword_instructions": "輸入您的郵箱地址,我們將發送重置密碼的連結。",
|
||||
"cloudForgotPassword_passwordResetError": "發送密碼重置郵件失敗",
|
||||
"cloudForgotPassword_passwordResetSent": "密碼重置已發送",
|
||||
"cloudForgotPassword_sendResetLink": "發送重置連結",
|
||||
"cloudForgotPassword_title": "忘記密碼",
|
||||
"cloudPrivateBeta_desc": "登入即可加入等候名單,當您有內測資格時我們將及時通知。已收到通知?登入即可開始使用 Comfy Cloud。",
|
||||
"cloudPrivateBeta_title": "Cloud 目前處於內測階段",
|
||||
"cloudSorryContactSupport_title": "抱歉,請聯繫用戶支援",
|
||||
"cloudStart_desc": "無需任何部署。可在任何設備上工作。",
|
||||
"cloudStart_download": "下載 ComfyUI",
|
||||
"cloudStart_explain": "一次生成多個結果。輕鬆分享工作流。",
|
||||
"cloudStart_learnAboutButton": "了解 Cloud",
|
||||
"cloudStart_title": "即刻開始創作,僅需幾秒",
|
||||
"cloudStart_wantToRun": "想要在本地運行 ComfyUI?",
|
||||
"cloudSurvey_steps_familiarity": "您對 ComfyUI 的熟悉程度如何?",
|
||||
"cloudSurvey_steps_industry": "您的主要行業是什麼?",
|
||||
"cloudSurvey_steps_making": "您計劃創作哪些內容?",
|
||||
"cloudSurvey_steps_purpose": "您使用 ComfyUI 的主要用途是?",
|
||||
"cloudVerifyEmail_title": "郵箱驗證",
|
||||
"cloudWaitlist_contactLink": "這裡",
|
||||
"cloudWaitlist_message": "您已被添加到等候名單。我們會在您獲得內測資格後通知您。",
|
||||
"cloudWaitlist_questionsText": "有疑問?聯繫我們",
|
||||
"cloudWaitlist_titleLine1": "您已加入",
|
||||
"cloudWaitlist_titleLine2": "等候名單 🎉",
|
||||
"color": {
|
||||
"black": "黑色",
|
||||
"blue": "藍色",
|
||||
@@ -1939,39 +1971,5 @@
|
||||
"label": "縮放控制",
|
||||
"showMinimap": "顯示小地圖",
|
||||
"zoomToFit": "縮放至適合大小"
|
||||
},
|
||||
"cloudFooter_needHelp": "需要幫助?",
|
||||
"cloudStart_title": "即刻開始創作,僅需幾秒",
|
||||
"cloudStart_desc": "無需任何部署。可在任何設備上工作。",
|
||||
"cloudStart_explain": "一次生成多個結果。輕鬆分享工作流。",
|
||||
"cloudStart_learnAboutButton": "了解 Cloud",
|
||||
"cloudStart_wantToRun": "想要在本地運行 ComfyUI?",
|
||||
"cloudStart_download": "下載 ComfyUI",
|
||||
"cloudWaitlist_titleLine1": "您已加入",
|
||||
"cloudWaitlist_titleLine2": "等候名單 🎉",
|
||||
"cloudWaitlist_message": "您已被添加到等候名單。我們會在您獲得內測資格後通知您。",
|
||||
"cloudWaitlist_questionsText": "有疑問?聯繫我們",
|
||||
"cloudWaitlist_contactLink": "這裡",
|
||||
"cloudClaimInvite_processingTitle": "處理邀請碼中...",
|
||||
"cloudClaimInvite_claimButton": "領取邀請",
|
||||
"cloudSorryContactSupport_title": "抱歉,請聯繫用戶支援",
|
||||
"cloudPrivateBeta_title": "Cloud 目前處於內測階段",
|
||||
"cloudPrivateBeta_desc": "登入即可加入等候名單,當您有內測資格時我們將及時通知。已收到通知?登入即可開始使用 Comfy Cloud。",
|
||||
"cloudForgotPassword_title": "忘記密碼",
|
||||
"cloudForgotPassword_instructions": "輸入您的郵箱地址,我們將發送重置密碼的連結。",
|
||||
"cloudForgotPassword_emailLabel": "郵箱",
|
||||
"cloudForgotPassword_emailPlaceholder": "輸入您的郵箱",
|
||||
"cloudForgotPassword_sendResetLink": "發送重置連結",
|
||||
"cloudForgotPassword_backToLogin": "返回登入",
|
||||
"cloudForgotPassword_didntReceiveEmail": "沒有收到郵件?",
|
||||
"cloudForgotPassword_emailRequired": "郵箱為必填項",
|
||||
"cloudForgotPassword_passwordResetSent": "密碼重置已發送",
|
||||
"cloudForgotPassword_passwordResetError": "發送密碼重置郵件失敗",
|
||||
"cloudSurvey_steps_familiarity": "您對 ComfyUI 的熟悉程度如何?",
|
||||
"cloudSurvey_steps_purpose": "您使用 ComfyUI 的主要用途是?",
|
||||
"cloudSurvey_steps_industry": "您的主要行業是什麼?",
|
||||
"cloudSurvey_steps_making": "您計劃創作哪些內容?",
|
||||
"desktopStart": {
|
||||
"initialising": "初始化中..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,38 @@
|
||||
"errorNotSupported": "您的浏览器不支持剪贴板API",
|
||||
"successMessage": "已复制到剪贴板"
|
||||
},
|
||||
"cloudClaimInvite_claimButton": "领取邀请",
|
||||
"cloudClaimInvite_processingTitle": "处理邀请码中...",
|
||||
"cloudFooter_needHelp": "需要帮助?",
|
||||
"cloudForgotPassword_backToLogin": "返回登录",
|
||||
"cloudForgotPassword_didntReceiveEmail": "没有收到邮件?",
|
||||
"cloudForgotPassword_emailLabel": "邮箱",
|
||||
"cloudForgotPassword_emailPlaceholder": "输入您的邮箱",
|
||||
"cloudForgotPassword_emailRequired": "邮箱为必填项",
|
||||
"cloudForgotPassword_instructions": "输入您的邮箱地址,我们将发送重置密码的链接。",
|
||||
"cloudForgotPassword_passwordResetError": "发送密码重置邮件失败",
|
||||
"cloudForgotPassword_passwordResetSent": "密码重置已发送",
|
||||
"cloudForgotPassword_sendResetLink": "发送重置链接",
|
||||
"cloudForgotPassword_title": "忘记密码",
|
||||
"cloudPrivateBeta_desc": "登录即可加入等候名单,当您有内测资格时我们将及时通知。已收到通知?登录即可开始使用 Comfy Cloud。",
|
||||
"cloudPrivateBeta_title": "Cloud 目前处于内测阶段",
|
||||
"cloudSorryContactSupport_title": "抱歉,请联系用户支持",
|
||||
"cloudStart_desc": "无需任何部署。可在任何设备上工作。",
|
||||
"cloudStart_download": "下载 ComfyUI",
|
||||
"cloudStart_explain": "一次生成多个结果。轻松分享工作流。",
|
||||
"cloudStart_learnAboutButton": "了解 Cloud",
|
||||
"cloudStart_title": "即刻开始创作,仅需几秒",
|
||||
"cloudStart_wantToRun": "想在本地运行 ComfyUI?",
|
||||
"cloudSurvey_steps_familiarity": "您对 ComfyUI 的熟悉程度如何?",
|
||||
"cloudSurvey_steps_industry": "您的主要行业是?",
|
||||
"cloudSurvey_steps_making": "您计划创作哪些内容?",
|
||||
"cloudSurvey_steps_purpose": "您使用 ComfyUI 的主要用途是?",
|
||||
"cloudVerifyEmail_title": "邮箱验证",
|
||||
"cloudWaitlist_contactLink": "这里",
|
||||
"cloudWaitlist_message": "您已被添加到等候名单。我们会在您获得内测资格后通知您。",
|
||||
"cloudWaitlist_questionsText": "有疑问?联系我们",
|
||||
"cloudWaitlist_titleLine1": "您已在",
|
||||
"cloudWaitlist_titleLine2": "等候名单中 :tada:",
|
||||
"color": {
|
||||
"black": "黑色",
|
||||
"blue": "蓝色",
|
||||
@@ -1942,98 +1974,5 @@
|
||||
"label": "缩放控制",
|
||||
"showMinimap": "显示小地图",
|
||||
"zoomToFit": "适合画面"
|
||||
},
|
||||
"cloudFooter_needHelp": "需要帮助?",
|
||||
"cloudStart_title": "即刻开始创作,仅需几秒",
|
||||
"cloudStart_desc": "无需任何部署。可在任何设备上工作。",
|
||||
"cloudStart_explain": "一次生成多个结果。轻松分享工作流。",
|
||||
"cloudStart_learnAboutButton": "了解 Cloud",
|
||||
"cloudStart_wantToRun": "想在本地运行 ComfyUI?",
|
||||
"cloudStart_download": "下载 ComfyUI",
|
||||
"cloudWaitlist_titleLine1": "您已在",
|
||||
"cloudWaitlist_titleLine2": "等候名单中 :tada:",
|
||||
"cloudWaitlist_message": "您已被添加到等候名单。我们会在您获得内测资格后通知您。",
|
||||
"cloudWaitlist_questionsText": "有疑问?联系我们",
|
||||
"cloudWaitlist_contactLink": "这里",
|
||||
"cloudClaimInvite_processingTitle": "处理邀请码中...",
|
||||
"cloudClaimInvite_claimButton": "领取邀请",
|
||||
"cloudSorryContactSupport_title": "抱歉,请联系用户支持",
|
||||
"cloudPrivateBeta_title": "Cloud 目前处于内测阶段",
|
||||
"cloudPrivateBeta_desc": "登录即可加入等候名单,当您有内测资格时我们将及时通知。已收到通知?登录即可开始使用 Comfy Cloud。",
|
||||
"cloudForgotPassword_title": "忘记密码",
|
||||
"cloudForgotPassword_instructions": "输入您的邮箱地址,我们将发送重置密码的链接。",
|
||||
"cloudForgotPassword_emailLabel": "邮箱",
|
||||
"cloudForgotPassword_emailPlaceholder": "输入您的邮箱",
|
||||
"cloudForgotPassword_sendResetLink": "发送重置链接",
|
||||
"cloudForgotPassword_backToLogin": "返回登录",
|
||||
"cloudForgotPassword_didntReceiveEmail": "没有收到邮件?",
|
||||
"cloudForgotPassword_emailRequired": "邮箱为必填项",
|
||||
"cloudForgotPassword_passwordResetSent": "密码重置已发送",
|
||||
"cloudForgotPassword_passwordResetError": "发送密码重置邮件失败",
|
||||
"cloudSurvey_steps_familiarity": "您对 ComfyUI 的熟悉程度如何?",
|
||||
"cloudSurvey_steps_purpose": "您使用 ComfyUI 的主要用途是?",
|
||||
"cloudSurvey_steps_industry": "您的主要行业是?",
|
||||
"cloudSurvey_steps_making": "您计划创作哪些内容?",
|
||||
"desktopStart": {
|
||||
"initialising": "正在初始化..."
|
||||
},
|
||||
"shape": {
|
||||
"default": "默认",
|
||||
"round": "圆角",
|
||||
"CARD": "卡片",
|
||||
"circle": "圆形",
|
||||
"arrow": "箭头",
|
||||
"box": "方框"
|
||||
},
|
||||
"commands": {
|
||||
"runWorkflow": "运行工作流",
|
||||
"runWorkflowFront": "运行工作流(队列前端)",
|
||||
"run": "运行",
|
||||
"execute": "执行",
|
||||
"interrupt": "取消当前运行",
|
||||
"refresh": "刷新节点定义",
|
||||
"clipspace": "打开 Clipspace",
|
||||
"resetView": "重置画布视图",
|
||||
"clear": "清空工作流",
|
||||
"toggleBottomPanel": "切换底部面板",
|
||||
"theme": "主题",
|
||||
"dark": "深色",
|
||||
"light": "浅色",
|
||||
"manageExtensions": "管理扩展",
|
||||
"settings": "设置",
|
||||
"help": "帮助",
|
||||
"queue": "队列面板"
|
||||
},
|
||||
"widgets": {
|
||||
"selectModel": "选择模型",
|
||||
"uploadSelect": {
|
||||
"placeholder": "请选择...",
|
||||
"placeholderImage": "请选择图片...",
|
||||
"placeholderAudio": "请选择音频...",
|
||||
"placeholderVideo": "请选择视频...",
|
||||
"placeholderModel": "请选择模型...",
|
||||
"placeholderUnknown": "请选择媒体..."
|
||||
}
|
||||
},
|
||||
"assetBrowser": {
|
||||
"assets": "资源",
|
||||
"browseAssets": "浏览资源",
|
||||
"noAssetsFound": "未找到资源",
|
||||
"tryAdjustingFilters": "请尝试调整搜索或筛选条件",
|
||||
"loadingModels": "正在加载{type}...",
|
||||
"connectionError": "请检查您的网络连接后重试",
|
||||
"failedToCreateNode": "创建节点失败。请重试或查看控制台获取详细信息。",
|
||||
"noModelsInFolder": "此文件夹中没有可用的{type}",
|
||||
"searchAssetsPlaceholder": "搜索资源...",
|
||||
"allModels": "全部模型",
|
||||
"allCategory": "全部{category}",
|
||||
"unknown": "未知",
|
||||
"fileFormats": "文件格式",
|
||||
"baseModels": "基础模型",
|
||||
"sortBy": "排序方式",
|
||||
"sortAZ": "A-Z",
|
||||
"sortZA": "Z-A",
|
||||
"sortRecent": "最近",
|
||||
"sortPopular": "最受欢迎"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,23 +16,7 @@ export const cloudOnboardingRoutes: RouteRecordRaw[] = [
|
||||
path: 'login',
|
||||
name: 'cloud-login',
|
||||
component: () =>
|
||||
import('@/platform/onboarding/cloud/CloudLoginView.vue'),
|
||||
beforeEnter: async (to, _from, next) => {
|
||||
// Only redirect if not explicitly switching accounts
|
||||
if (!to.query.switchAccount) {
|
||||
const { useCurrentUser } = await import(
|
||||
'@/composables/auth/useCurrentUser'
|
||||
)
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
|
||||
if (isLoggedIn.value) {
|
||||
// User is already logged in, redirect to user-check
|
||||
// user-check will handle survey, waitlist, or main page routing
|
||||
return next({ name: 'cloud-user-check' })
|
||||
}
|
||||
}
|
||||
next()
|
||||
}
|
||||
import('@/platform/onboarding/cloud/CloudLoginView.vue')
|
||||
},
|
||||
{
|
||||
path: 'signup',
|
||||
@@ -84,10 +68,8 @@ export const cloudOnboardingRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: 'verify-email',
|
||||
name: 'cloud-verify-email',
|
||||
redirect: (to) => ({
|
||||
name: 'cloud-user-check',
|
||||
query: to.query
|
||||
})
|
||||
component: () =>
|
||||
import('@/platform/onboarding/cloud/CloudVerifyEmailView.vue')
|
||||
},
|
||||
{
|
||||
path: 'sorry-contact-support',
|
||||
@@ -99,8 +81,7 @@ export const cloudOnboardingRoutes: RouteRecordRaw[] = [
|
||||
path: 'auth-timeout',
|
||||
name: 'cloud-auth-timeout',
|
||||
component: () =>
|
||||
import('@/platform/onboarding/cloud/CloudAuthTimeoutView.vue'),
|
||||
props: true
|
||||
import('@/platform/onboarding/cloud/CloudAuthTimeoutView.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const useSessionCookie = () => {
|
||||
if (!isCloud) return
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authHeader = await authStore.getAuthHeader(true)
|
||||
const authHeader = await authStore.getAuthHeader()
|
||||
|
||||
if (!authHeader) {
|
||||
throw new Error('No auth header available for session creation')
|
||||
|
||||
@@ -2,51 +2,34 @@
|
||||
<Button
|
||||
:label="label || $t('subscription.required.subscribe')"
|
||||
:size="size"
|
||||
:class="buttonClass"
|
||||
:loading="isLoading"
|
||||
:disabled="isPolling"
|
||||
severity="primary"
|
||||
:style="
|
||||
variant === 'gradient'
|
||||
? {
|
||||
background: 'var(--color-subscription-button-gradient)',
|
||||
color: 'var(--color-white)'
|
||||
}
|
||||
: undefined
|
||||
"
|
||||
:pt="{
|
||||
root: {
|
||||
class: rootClass
|
||||
}
|
||||
}"
|
||||
@click="handleSubscribe"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label?: string
|
||||
size?: 'small' | 'large'
|
||||
variant?: 'default' | 'gradient'
|
||||
fluid?: boolean
|
||||
buttonClass?: string
|
||||
}>(),
|
||||
{
|
||||
size: 'large',
|
||||
variant: 'default',
|
||||
fluid: true
|
||||
buttonClass: 'w-full font-bold'
|
||||
}
|
||||
)
|
||||
|
||||
const rootClass = computed(() => cn('font-bold', props.fluid && 'w-full'))
|
||||
|
||||
const emit = defineEmits<{
|
||||
subscribed: []
|
||||
}>()
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
icon="pi pi-lock"
|
||||
severity="primary"
|
||||
size="small"
|
||||
:style="{
|
||||
background: 'var(--color-subscription-button-gradient)',
|
||||
color: 'var(--color-white)'
|
||||
}"
|
||||
data-testid="subscribe-to-run-button"
|
||||
@click="handleSubscribeToRun"
|
||||
/>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-start gap-1 self-stretch">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="pi pi-check mt-1 text-xs text-text-primary" />
|
||||
<span class="text-sm text-text-primary">
|
||||
<i class="pi pi-check mt-1 text-sm" />
|
||||
<span class="text-sm">
|
||||
{{ $t('subscription.benefits.benefit1') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="pi pi-check mt-1 text-xs text-text-primary" />
|
||||
<span class="text-sm text-text-primary">
|
||||
<i class="pi pi-check mt-1 text-sm" />
|
||||
<span class="text-sm">
|
||||
{{ $t('subscription.benefits.benefit2') }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -19,15 +19,8 @@
|
||||
text
|
||||
icon="pi pi-external-link"
|
||||
icon-pos="left"
|
||||
class="flex h-8 min-h-6 py-2 px-0 items-center gap-2 rounded text-text-secondary"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-xs text-text-secondary'
|
||||
},
|
||||
label: {
|
||||
class: 'text-sm text-text-secondary'
|
||||
}
|
||||
}"
|
||||
size="small"
|
||||
class="self-start !p-0 text-sm hover:!bg-transparent [&]:!text-[inherit]"
|
||||
@click="handleViewMoreDetails"
|
||||
/>
|
||||
</div>
|
||||
@@ -37,6 +30,6 @@
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const handleViewMoreDetails = () => {
|
||||
window.open('https://www.comfy.org/cloud/pricing', '_blank')
|
||||
window.open('https://www.comfy.org/cloud', '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<TabPanel value="PlanCredits" class="subscription-container h-full">
|
||||
<div class="flex h-full flex-col gap-6">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-2xl font-inter font-semibold leading-tight">
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-2xl">
|
||||
{{ $t('subscription.title') }}
|
||||
</span>
|
||||
</h2>
|
||||
<CloudBadge
|
||||
reverse-order
|
||||
background-color="var(--p-dialog-background)"
|
||||
@@ -12,20 +12,17 @@
|
||||
</div>
|
||||
|
||||
<div class="grow overflow-auto">
|
||||
<div class="rounded-2xl border border-interface-stroke p-6">
|
||||
<div class="rounded-lg border border-charcoal-400 p-4">
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-baseline gap-1 font-inter font-semibold">
|
||||
<span class="text-2xl">{{ formattedMonthlyPrice }}</span>
|
||||
<span class="text-base">{{
|
||||
$t('subscription.perMonth')
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-2xl font-bold">{{
|
||||
formattedMonthlyPrice
|
||||
}}</span>
|
||||
<span>{{ $t('subscription.perMonth') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isActiveSubscription"
|
||||
class="text-sm text-text-secondary"
|
||||
>
|
||||
<div v-if="isActiveSubscription" class="text-xs text-muted">
|
||||
<template v-if="isCancelled">
|
||||
{{
|
||||
$t('subscription.expiresDate', {
|
||||
@@ -46,165 +43,105 @@
|
||||
v-if="isActiveSubscription"
|
||||
:label="$t('subscription.manageSubscription')"
|
||||
severity="secondary"
|
||||
class="text-xs bg-interface-menu-component-surface-selected"
|
||||
:pt="{
|
||||
root: {
|
||||
style: 'border-radius: 8px; padding: 8px 16px;'
|
||||
},
|
||||
label: {
|
||||
class: 'text-text-primary'
|
||||
}
|
||||
}"
|
||||
class="text-xs"
|
||||
@click="manageSubscription"
|
||||
/>
|
||||
<SubscribeButton
|
||||
v-else
|
||||
:label="$t('subscription.subscribeNow')"
|
||||
size="small"
|
||||
:fluid="false"
|
||||
class="text-xs"
|
||||
button-class="text-xs"
|
||||
@subscribed="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 pt-9 lg:grid-cols-2">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="grid grid-cols-1 gap-6 rounded-lg pt-10 lg:grid-cols-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm">
|
||||
{{ $t('subscription.partnerNodesBalance') }}
|
||||
{{ $t('subscription.apiNodesBalance') }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="text-sm text-muted">
|
||||
{{ $t('subscription.partnerNodesDescription') }}
|
||||
<div class="text-xs text-muted">
|
||||
{{ $t('subscription.apiNodesDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'relative flex flex-col gap-6 rounded-2xl p-5',
|
||||
'bg-smoke-100 dark-theme:bg-charcoal-600'
|
||||
)
|
||||
"
|
||||
class="flex flex-col gap-3 rounded-lg border p-4 dark-theme:border-0 dark-theme:bg-charcoal-600"
|
||||
>
|
||||
<Button
|
||||
v-tooltip="refreshTooltip"
|
||||
icon="pi pi-sync"
|
||||
text
|
||||
size="small"
|
||||
class="absolute top-0.5 right-0"
|
||||
:loading="isLoadingBalance"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
},
|
||||
loadingIcon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
@click="handleRefresh"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm text-text-secondary">
|
||||
{{ $t('subscription.totalCredits') }}
|
||||
</div>
|
||||
<Skeleton
|
||||
v-if="isLoadingBalance"
|
||||
width="8rem"
|
||||
height="2rem"
|
||||
/>
|
||||
<div v-else class="text-2xl font-bold">
|
||||
${{ totalCredits }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Credit Breakdown -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-4">
|
||||
<Skeleton
|
||||
v-if="isLoadingBalance"
|
||||
width="3rem"
|
||||
height="1rem"
|
||||
/>
|
||||
<div v-else class="text-sm text-text-secondary font-bold">
|
||||
${{ monthlyBonusCredits }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="text-sm text-text-secondary">
|
||||
{{ $t('subscription.monthlyBonusDescription') }}
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="$t('subscription.monthlyCreditsRollover')"
|
||||
icon="pi pi-question-circle"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="h-4 w-4"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<Skeleton
|
||||
v-if="isLoadingBalance"
|
||||
width="3rem"
|
||||
height="1rem"
|
||||
/>
|
||||
<div v-else class="text-sm text-text-secondary font-bold">
|
||||
${{ prepaidCredits }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="text-sm text-text-secondary">
|
||||
{{ $t('subscription.prepaidDescription') }}
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="$t('subscription.prepaidCreditsInfo')"
|
||||
icon="pi pi-question-circle"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="h-4 w-4"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<a
|
||||
href="https://platform.comfy.org/profile/usage"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-text-secondary underline hover:text-text-secondary"
|
||||
style="text-decoration: underline"
|
||||
<div>
|
||||
<div class="text-xs text-muted">
|
||||
{{ $t('subscription.totalCredits') }}
|
||||
</div>
|
||||
<div class="text-2xl font-bold">${{ totalCredits }}</div>
|
||||
</div>
|
||||
<Button
|
||||
icon="pi pi-sync"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
:loading="isLoadingBalance"
|
||||
@click="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="latestEvents.length > 0"
|
||||
class="flex flex-col gap-2 pt-3 text-xs"
|
||||
>
|
||||
<div
|
||||
v-for="event in latestEvents"
|
||||
:key="event.event_id"
|
||||
class="flex items-center justify-between py-1"
|
||||
>
|
||||
{{ $t('subscription.viewUsageHistory') }}
|
||||
</a>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-medium">
|
||||
{{
|
||||
event.event_type
|
||||
? customerEventService.formatEventType(
|
||||
event.event_type
|
||||
)
|
||||
: ''
|
||||
}}
|
||||
</span>
|
||||
<span class="text-muted">
|
||||
{{
|
||||
event.createdAt
|
||||
? customerEventService.formatDate(event.createdAt)
|
||||
: ''
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="event.params?.amount !== undefined"
|
||||
class="font-bold"
|
||||
>
|
||||
${{
|
||||
customerEventService.formatAmount(
|
||||
event.params.amount as number
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<Button
|
||||
:label="$t('subscription.viewUsageHistory')"
|
||||
text
|
||||
severity="secondary"
|
||||
class="p-0 text-xs text-muted"
|
||||
@click="handleViewUsageHistory"
|
||||
/>
|
||||
<Button
|
||||
v-if="isActiveSubscription"
|
||||
:label="$t('subscription.addCredits')"
|
||||
:label="$t('subscription.addApiCredits')"
|
||||
severity="secondary"
|
||||
class="p-2 min-h-8 bg-interface-menu-component-surface-selected"
|
||||
:pt="{
|
||||
root: {
|
||||
style: 'border-radius: 8px;'
|
||||
},
|
||||
label: {
|
||||
class: 'text-sm'
|
||||
}
|
||||
}"
|
||||
class="text-xs"
|
||||
@click="handleAddApiCredits"
|
||||
/>
|
||||
</div>
|
||||
@@ -212,7 +149,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="text-sm">
|
||||
{{ $t('subscription.yourPlanIncludes') }}
|
||||
</div>
|
||||
@@ -224,7 +161,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between border-t border-interface-stroke pt-3"
|
||||
class="flex items-center justify-between border-t border-charcoal-400 pt-3"
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
@@ -233,15 +170,7 @@
|
||||
severity="secondary"
|
||||
icon="pi pi-question-circle"
|
||||
class="text-xs"
|
||||
:pt="{
|
||||
label: {
|
||||
class: 'text-text-secondary'
|
||||
},
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
@click="handleLearnMoreClick"
|
||||
@click="handleLearnMore"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('subscription.messageSupport')"
|
||||
@@ -249,15 +178,6 @@
|
||||
severity="secondary"
|
||||
icon="pi pi-comment"
|
||||
class="text-xs"
|
||||
:loading="isLoadingSupport"
|
||||
:pt="{
|
||||
label: {
|
||||
class: 'text-text-secondary'
|
||||
},
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
@click="handleMessageSupport"
|
||||
/>
|
||||
</div>
|
||||
@@ -269,14 +189,6 @@
|
||||
icon="pi pi-external-link"
|
||||
icon-pos="right"
|
||||
class="text-xs"
|
||||
:pt="{
|
||||
label: {
|
||||
class: 'text-text-secondary'
|
||||
},
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
@click="handleInvoiceHistory"
|
||||
/>
|
||||
</div>
|
||||
@@ -286,16 +198,26 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import CloudBadge from '@/components/topbar/CloudBadge.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import SubscriptionBenefits from '@/platform/cloud/subscription/components/SubscriptionBenefits.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useSubscriptionActions } from '@/platform/cloud/subscription/composables/useSubscriptionActions'
|
||||
import { useSubscriptionCredits } from '@/platform/cloud/subscription/composables/useSubscriptionCredits'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import type { AuditLog } from '@/services/customerEventsService'
|
||||
import { useCustomerEventsService } from '@/services/customerEventsService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
const dialogService = useDialogService()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const customerEventService = useCustomerEventsService()
|
||||
|
||||
const {
|
||||
isActiveSubscription,
|
||||
@@ -304,20 +226,54 @@ const {
|
||||
formattedEndDate,
|
||||
formattedMonthlyPrice,
|
||||
manageSubscription,
|
||||
handleInvoiceHistory
|
||||
handleViewUsageHistory,
|
||||
handleLearnMore,
|
||||
handleInvoiceHistory,
|
||||
fetchStatus
|
||||
} = useSubscription()
|
||||
|
||||
const { totalCredits, monthlyBonusCredits, prepaidCredits, isLoadingBalance } =
|
||||
useSubscriptionCredits()
|
||||
const latestEvents = ref<AuditLog[]>([])
|
||||
|
||||
const {
|
||||
isLoadingSupport,
|
||||
refreshTooltip,
|
||||
handleAddApiCredits,
|
||||
handleMessageSupport,
|
||||
handleRefresh,
|
||||
handleLearnMoreClick
|
||||
} = useSubscriptionActions()
|
||||
const totalCredits = computed(() => {
|
||||
if (!authStore.balance) return '0.00'
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
})
|
||||
|
||||
const isLoadingBalance = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
const fetchLatestEvents = async () => {
|
||||
try {
|
||||
const response = await customerEventService.getMyEvents({
|
||||
page: 1,
|
||||
limit: 2
|
||||
})
|
||||
if (response?.events) {
|
||||
latestEvents.value = response.events
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SubscriptionPanel] Error fetching latest events:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void handleRefresh()
|
||||
})
|
||||
|
||||
const handleAddApiCredits = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
const handleMessageSupport = async () => {
|
||||
await commandStore.execute('Comfy.ContactSupport')
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await Promise.all([
|
||||
authActions.fetchBalance(),
|
||||
fetchStatus(),
|
||||
fetchLatestEvents()
|
||||
])
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
<template>
|
||||
<div class="relative grid h-full grid-cols-5">
|
||||
<!-- Custom close button -->
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
text
|
||||
rounded
|
||||
class="absolute top-2.5 right-2.5 z-10 h-8 w-8 p-0 text-white hover:bg-white/20"
|
||||
:aria-label="$t('g.close')"
|
||||
@click="onClose"
|
||||
/>
|
||||
|
||||
<div class="grid h-full grid-cols-5 px-10 pb-10">
|
||||
<div
|
||||
class="relative col-span-2 flex items-center justify-center overflow-hidden rounded-sm"
|
||||
>
|
||||
@@ -18,7 +8,7 @@
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
class="h-full min-w-[125%] object-cover p-0"
|
||||
class="h-full min-w-[125%] object-cover"
|
||||
style="margin-left: -20%"
|
||||
>
|
||||
<source
|
||||
@@ -28,18 +18,17 @@
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 flex flex-col justify-between p-8">
|
||||
<div class="col-span-3 flex flex-col justify-between pl-8">
|
||||
<div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<div class="text-text-primary text-sm text-muted">
|
||||
<div class="text-sm text-muted">
|
||||
{{ $t('subscription.required.title') }}
|
||||
</div>
|
||||
<CloudBadge
|
||||
reverse-order
|
||||
no-padding
|
||||
background-color="var(--p-dialog-background)"
|
||||
use-subscription
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -52,36 +41,19 @@
|
||||
<SubscriptionBenefits class="mt-6 text-muted" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col pt-8">
|
||||
<SubscribeButton
|
||||
class="rounded-lg px-4 py-2"
|
||||
:pt="{
|
||||
root: {
|
||||
style: 'background: var(--color-accent-blue, #0B8CE9);'
|
||||
},
|
||||
label: {
|
||||
class: 'font-inter font-[700] text-sm'
|
||||
}
|
||||
}"
|
||||
@subscribed="handleSubscribed"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<SubscribeButton @subscribed="handleSubscribed" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import CloudBadge from '@/components/topbar/CloudBadge.vue'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import SubscriptionBenefits from '@/platform/cloud/subscription/components/SubscriptionBenefits.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
|
||||
defineProps<{
|
||||
onClose: () => void
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [subscribed: boolean]
|
||||
}>()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
@@ -15,30 +14,34 @@ import {
|
||||
useFirebaseAuthStore
|
||||
} from '@/stores/firebaseAuthStore'
|
||||
|
||||
type CloudSubscriptionCheckoutResponse = {
|
||||
interface CloudSubscriptionCheckoutResponse {
|
||||
checkout_url: string
|
||||
}
|
||||
|
||||
type CloudSubscriptionStatusResponse = {
|
||||
interface CloudSubscriptionStatusResponse {
|
||||
is_active: boolean
|
||||
subscription_id: string
|
||||
renewal_date: string | null
|
||||
end_date?: string | null
|
||||
}
|
||||
|
||||
function useSubscriptionInternal() {
|
||||
const subscriptionStatus = ref<CloudSubscriptionStatusResponse | null>(null)
|
||||
const subscriptionStatus = ref<CloudSubscriptionStatusResponse | null>(null)
|
||||
|
||||
const isSubscribedOrIsNotCloud = computed(() => {
|
||||
if (!isCloud || !window.__CONFIG__?.subscription_required) return true
|
||||
const isActiveSubscription = computed(() => {
|
||||
if (!isCloud || !window.__CONFIG__?.subscription_required) return true
|
||||
|
||||
return subscriptionStatus.value?.is_active ?? false
|
||||
})
|
||||
const { reportError, accessBillingPortal } = useFirebaseAuthActions()
|
||||
return subscriptionStatus.value?.is_active ?? false
|
||||
})
|
||||
|
||||
let isWatchSetup = false
|
||||
|
||||
export function useSubscription() {
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
const { getAuthHeader } = useFirebaseAuthStore()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
const { reportError } = useFirebaseAuthActions()
|
||||
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
|
||||
@@ -51,7 +54,7 @@ function useSubscriptionInternal() {
|
||||
|
||||
const renewalDate = new Date(subscriptionStatus.value.renewal_date)
|
||||
|
||||
return renewalDate.toLocaleDateString(undefined, {
|
||||
return renewalDate.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
@@ -63,7 +66,7 @@ function useSubscriptionInternal() {
|
||||
|
||||
const endDate = new Date(subscriptionStatus.value.end_date)
|
||||
|
||||
return endDate.toLocaleDateString(undefined, {
|
||||
return endDate.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
@@ -74,10 +77,9 @@ function useSubscriptionInternal() {
|
||||
() => `$${MONTHLY_SUBSCRIPTION_PRICE.toFixed(0)}`
|
||||
)
|
||||
|
||||
const fetchStatus = wrapWithErrorHandlingAsync(
|
||||
fetchSubscriptionStatus,
|
||||
reportError
|
||||
)
|
||||
const fetchStatus = wrapWithErrorHandlingAsync(async () => {
|
||||
return await fetchSubscriptionStatus()
|
||||
}, reportError)
|
||||
|
||||
const subscribe = wrapWithErrorHandlingAsync(async () => {
|
||||
const response = await initiateSubscriptionCheckout()
|
||||
@@ -98,17 +100,17 @@ function useSubscriptionInternal() {
|
||||
useTelemetry()?.trackSubscription('modal_opened')
|
||||
}
|
||||
|
||||
void dialogService.showSubscriptionRequiredDialog()
|
||||
dialogService.showSubscriptionRequiredDialog()
|
||||
}
|
||||
|
||||
const manageSubscription = async () => {
|
||||
await accessBillingPortal()
|
||||
await authActions.accessBillingPortal()
|
||||
}
|
||||
|
||||
const requireActiveSubscription = async (): Promise<void> => {
|
||||
await fetchSubscriptionStatus()
|
||||
|
||||
if (!isSubscribedOrIsNotCloud.value) {
|
||||
if (!isActiveSubscription.value) {
|
||||
showSubscriptionDialog()
|
||||
}
|
||||
}
|
||||
@@ -122,55 +124,61 @@ function useSubscriptionInternal() {
|
||||
}
|
||||
|
||||
const handleInvoiceHistory = async () => {
|
||||
await accessBillingPortal()
|
||||
await authActions.accessBillingPortal()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current cloud subscription status for the authenticated user
|
||||
* @returns Subscription status or null if no subscription exists
|
||||
*/
|
||||
async function fetchSubscriptionStatus(): Promise<CloudSubscriptionStatusResponse | null> {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
const fetchSubscriptionStatus =
|
||||
async (): Promise<CloudSubscriptionStatusResponse | null> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${COMFY_API_BASE_URL}/customers/cloud-subscription-status`,
|
||||
{
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
const response = await fetch(
|
||||
`${COMFY_API_BASE_URL}/customers/cloud-subscription-status`,
|
||||
{
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToFetchSubscription', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToFetchSubscription', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const statusData = await response.json()
|
||||
subscriptionStatus.value = statusData
|
||||
return statusData
|
||||
}
|
||||
|
||||
const statusData = await response.json()
|
||||
subscriptionStatus.value = statusData
|
||||
return statusData
|
||||
if (!isWatchSetup) {
|
||||
isWatchSetup = true
|
||||
watch(
|
||||
() => isLoggedIn.value,
|
||||
async (loggedIn) => {
|
||||
if (loggedIn) {
|
||||
await fetchSubscriptionStatus()
|
||||
} else {
|
||||
subscriptionStatus.value = null
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isLoggedIn.value,
|
||||
async (loggedIn) => {
|
||||
if (loggedIn) {
|
||||
await fetchSubscriptionStatus()
|
||||
} else {
|
||||
subscriptionStatus.value = null
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const initiateSubscriptionCheckout =
|
||||
async (): Promise<CloudSubscriptionCheckoutResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
@@ -205,7 +213,7 @@ function useSubscriptionInternal() {
|
||||
|
||||
return {
|
||||
// State
|
||||
isActiveSubscription: isSubscribedOrIsNotCloud,
|
||||
isActiveSubscription,
|
||||
isCancelled,
|
||||
formattedRenewalDate,
|
||||
formattedEndDate,
|
||||
@@ -222,5 +230,3 @@ function useSubscriptionInternal() {
|
||||
handleInvoiceHistory
|
||||
}
|
||||
}
|
||||
|
||||
export const useSubscription = createSharedComposable(useSubscriptionInternal)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
/**
|
||||
* Composable for handling subscription panel actions and loading states
|
||||
*/
|
||||
export function useSubscriptionActions() {
|
||||
const { t } = useI18n()
|
||||
const dialogService = useDialogService()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const { fetchStatus, formattedRenewalDate } = useSubscription()
|
||||
|
||||
const isLoadingSupport = ref(false)
|
||||
|
||||
const refreshTooltip = computed(() => {
|
||||
const date =
|
||||
formattedRenewalDate.value || t('subscription.nextBillingCycle')
|
||||
return `Refreshes on ${date}`
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void handleRefresh()
|
||||
})
|
||||
|
||||
const handleAddApiCredits = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
const handleMessageSupport = async () => {
|
||||
try {
|
||||
isLoadingSupport.value = true
|
||||
await commandStore.execute('Comfy.ContactSupport')
|
||||
} catch (error) {
|
||||
console.error('[useSubscriptionActions] Error contacting support:', error)
|
||||
} finally {
|
||||
isLoadingSupport.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
try {
|
||||
await Promise.all([authActions.fetchBalance(), fetchStatus()])
|
||||
} catch (error) {
|
||||
console.error('[useSubscriptionActions] Error refreshing data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLearnMoreClick = () => {
|
||||
window.open('https://docs.comfy.org/get_started/cloud', '_blank')
|
||||
}
|
||||
|
||||
return {
|
||||
isLoadingSupport,
|
||||
refreshTooltip,
|
||||
handleAddApiCredits,
|
||||
handleMessageSupport,
|
||||
handleRefresh,
|
||||
handleLearnMoreClick
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Composable for handling subscription credit calculations and formatting
|
||||
*/
|
||||
export function useSubscriptionCredits() {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
const totalCredits = computed(() => {
|
||||
if (!authStore.balance?.amount_micros) return '0.00'
|
||||
try {
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[useSubscriptionCredits] Error formatting total credits:',
|
||||
error
|
||||
)
|
||||
return '0.00'
|
||||
}
|
||||
})
|
||||
|
||||
const monthlyBonusCredits = computed(() => {
|
||||
const balance = authStore.balance as any
|
||||
if (!balance?.cloud_credit_balance_micros) return '0.00'
|
||||
try {
|
||||
return formatMetronomeCurrency(balance.cloud_credit_balance_micros, 'usd')
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[useSubscriptionCredits] Error formatting monthly bonus credits:',
|
||||
error
|
||||
)
|
||||
return '0.00'
|
||||
}
|
||||
})
|
||||
|
||||
const prepaidCredits = computed(() => {
|
||||
const balance = authStore.balance as any
|
||||
if (!balance?.prepaid_balance_micros) return '0.00'
|
||||
try {
|
||||
return formatMetronomeCurrency(balance.prepaid_balance_micros, 'usd')
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[useSubscriptionCredits] Error formatting prepaid credits:',
|
||||
error
|
||||
)
|
||||
return '0.00'
|
||||
}
|
||||
})
|
||||
|
||||
const isLoadingBalance = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
return {
|
||||
totalCredits,
|
||||
monthlyBonusCredits,
|
||||
prepaidCredits,
|
||||
isLoadingBalance
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const DIALOG_KEY = 'subscription-required'
|
||||
|
||||
export const useSubscriptionDialog = () => {
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show() {
|
||||
dialogService.showLayoutDialog({
|
||||
key: DIALOG_KEY,
|
||||
component: defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'@/platform/cloud/subscription/components/SubscriptionRequiredDialogContent.vue'
|
||||
)
|
||||
),
|
||||
props: {
|
||||
onClose: hide
|
||||
},
|
||||
dialogComponentProps: {
|
||||
style: 'width: 700px;'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
hide
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,13 @@
|
||||
<template>
|
||||
<div class="flex h-full items-center justify-center p-6">
|
||||
<div class="max-w-[100vw] text-center lg:w-[500px]">
|
||||
<h2 class="mb-3 text-xl text-text-primary">
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-[100vw] text-center lg:w-96">
|
||||
<h2 class="mb-4 text-xl">
|
||||
{{ $t('cloudOnboarding.authTimeout.title') }}
|
||||
</h2>
|
||||
<p class="mb-5 text-muted">
|
||||
<p class="mb-6 text-gray-600">
|
||||
{{ $t('cloudOnboarding.authTimeout.message') }}
|
||||
</p>
|
||||
|
||||
<!-- Troubleshooting Section -->
|
||||
<div
|
||||
class="mb-4 rounded bg-surface-700 px-3 py-2 text-left dark-theme:bg-surface-800"
|
||||
>
|
||||
<h3 class="mb-2 text-sm font-semibold text-text-primary">
|
||||
{{ $t('cloudOnboarding.authTimeout.troubleshooting') }}
|
||||
</h3>
|
||||
<ul class="space-y-1.5 text-sm text-muted">
|
||||
<li
|
||||
v-for="(cause, index) in $tm('cloudOnboarding.authTimeout.causes')"
|
||||
:key="index"
|
||||
class="flex gap-2"
|
||||
>
|
||||
<span>•</span>
|
||||
<span>{{ cause }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Technical Details (Collapsible) -->
|
||||
<div v-if="errorMessage" class="mb-4 text-left">
|
||||
<button
|
||||
class="flex w-full items-center justify-between rounded bg-surface-600 px-4 py-2 text-sm text-muted transition-colors hover:bg-surface-500 dark-theme:bg-surface-700 dark-theme:hover:bg-surface-600"
|
||||
@click="showTechnicalDetails = !showTechnicalDetails"
|
||||
>
|
||||
<span>{{ $t('cloudOnboarding.authTimeout.technicalDetails') }}</span>
|
||||
<i
|
||||
:class="[
|
||||
'pi',
|
||||
showTechnicalDetails ? 'pi-chevron-up' : 'pi-chevron-down'
|
||||
]"
|
||||
></i>
|
||||
</button>
|
||||
<div
|
||||
v-if="showTechnicalDetails"
|
||||
class="mt-2 rounded bg-surface-800 p-4 font-mono text-xs text-muted break-all dark-theme:bg-surface-900"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Helpful Links -->
|
||||
<p class="mb-5 text-center text-sm text-gray-600">
|
||||
{{ $t('cloudOnboarding.authTimeout.helpText') }}
|
||||
<a
|
||||
href="https://support.comfy.org"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ $t('cloudOnboarding.authTimeout.supportLink') }}</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<Button
|
||||
:label="$t('cloudOnboarding.authTimeout.restart')"
|
||||
@@ -75,20 +21,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
|
||||
interface Props {
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const router = useRouter()
|
||||
const { logout } = useFirebaseAuthActions()
|
||||
const showTechnicalDetails = ref(false)
|
||||
|
||||
const handleRestart = async () => {
|
||||
await logout()
|
||||
|
||||
@@ -98,10 +98,7 @@ const inviteCode = computed(() => route.query.inviteCode as string)
|
||||
const onSwitchAccounts = () => {
|
||||
void router.push({
|
||||
name: 'cloud-login',
|
||||
query: {
|
||||
switchAccount: 'true',
|
||||
inviteCode: inviteCode.value
|
||||
}
|
||||
query: { inviteCode: inviteCode.value }
|
||||
})
|
||||
}
|
||||
const onClickSupport = () => {
|
||||
|
||||
@@ -17,16 +17,23 @@ onMounted(async () => {
|
||||
const inviteCode = route.params.code as string | undefined
|
||||
|
||||
if (firebaseAuthStore.isAuthenticated) {
|
||||
// User is logged in - no email verification check needed
|
||||
if (inviteCode) {
|
||||
// Handle invite code flow - go to invite check
|
||||
await router.push({
|
||||
name: 'cloud-invite-check',
|
||||
query: { inviteCode }
|
||||
})
|
||||
const { isEmailVerified } = firebaseAuthStore
|
||||
|
||||
if (!isEmailVerified) {
|
||||
// User is logged in but email not verified
|
||||
await router.push({ name: 'cloud-verify-email', query: { inviteCode } })
|
||||
} else {
|
||||
// Normal login flow - go to user check
|
||||
await router.push({ name: 'cloud-user-check' })
|
||||
// User is logged in and verified
|
||||
if (inviteCode) {
|
||||
// Handle invite code flow - go to invite check
|
||||
await router.push({
|
||||
name: 'cloud-invite-check',
|
||||
query: { inviteCode }
|
||||
})
|
||||
} else {
|
||||
// Normal login flow - go to user check
|
||||
await router.push({ name: 'cloud-user-check' })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// User is not logged in - proceed to login page
|
||||
|
||||
@@ -115,6 +115,7 @@ import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthAction
|
||||
import CloudSignInForm from '@/platform/onboarding/cloud/components/CloudSignInForm.vue'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { translateAuthError } from '@/utils/authErrorTranslation'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -139,15 +140,20 @@ const onSuccess = async () => {
|
||||
})
|
||||
// Check if there's an invite code
|
||||
const inviteCode = route.query.inviteCode as string | undefined
|
||||
if (inviteCode) {
|
||||
// Handle invite code flow - go to invite check
|
||||
await router.push({
|
||||
name: 'cloud-invite-check',
|
||||
query: { inviteCode }
|
||||
})
|
||||
const { isEmailVerified } = useFirebaseAuthStore()
|
||||
if (!isEmailVerified) {
|
||||
await router.push({ name: 'cloud-verify-email', query: { inviteCode } })
|
||||
} else {
|
||||
// Normal login flow - go to user check
|
||||
await router.push({ name: 'cloud-user-check' })
|
||||
if (inviteCode) {
|
||||
// Handle invite code flow - go to invite check
|
||||
await router.push({
|
||||
name: 'cloud-invite-check',
|
||||
query: { inviteCode }
|
||||
})
|
||||
} else {
|
||||
// Normal login flow - go to user check
|
||||
await router.push({ name: 'cloud-user-check' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { SignUpData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { translateAuthError } from '@/utils/authErrorTranslation'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
|
||||
@@ -126,8 +127,20 @@ const onSuccess = async () => {
|
||||
summary: 'Sign up Completed',
|
||||
life: 2000
|
||||
})
|
||||
// Direct redirect to main app - email verification removed
|
||||
await router.push({ path: '/', query: route.query })
|
||||
// Check if email verification is needed
|
||||
const { isEmailVerified } = useFirebaseAuthStore()
|
||||
const inviteCode = route.query.inviteCode as string | undefined
|
||||
|
||||
if (!isEmailVerified) {
|
||||
// Redirect to email verification with fromAuth flag
|
||||
await router.push({
|
||||
name: 'cloud-verify-email',
|
||||
query: { inviteCode, fromAuth: 'true' }
|
||||
})
|
||||
} else {
|
||||
// The invite code will be handled after the user is logged in
|
||||
await router.push({ path: '/', query: route.query })
|
||||
}
|
||||
}
|
||||
|
||||
// Custom error handler for inline display
|
||||
|
||||
191
src/platform/onboarding/cloud/CloudVerifyEmailView.vue
Normal file
191
src/platform/onboarding/cloud/CloudVerifyEmailView.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div class="mx-auto max-w-[640px] px-6 py-8">
|
||||
<!-- Back button -->
|
||||
<button
|
||||
type="button"
|
||||
class="text-foreground/80 flex size-10 items-center justify-center rounded-lg border border-white bg-transparent"
|
||||
aria-label="{{ t('cloudVerifyEmail_back') }}"
|
||||
@click="goBack"
|
||||
>
|
||||
<i class="pi pi-arrow-left" />
|
||||
</button>
|
||||
|
||||
<!-- Title -->
|
||||
<h1 class="mt-8 text-2xl font-semibold">
|
||||
{{ t('cloudVerifyEmail_title') }}
|
||||
</h1>
|
||||
|
||||
<!-- Body copy -->
|
||||
<p class="text-foreground/80 mt-6 mb-0 text-base">
|
||||
{{ t('cloudVerifyEmail_sent') }}
|
||||
</p>
|
||||
<p class="mt-2 text-base font-medium">{{ authStore.userEmail }}</p>
|
||||
|
||||
<p class="text-foreground/80 mt-6 text-base whitespace-pre-line">
|
||||
{{ t('cloudVerifyEmail_clickToContinue') }}
|
||||
</p>
|
||||
|
||||
<p class="text-foreground/80 mt-6 text-base whitespace-pre-line">
|
||||
{{ t('cloudVerifyEmail_tip') }}
|
||||
</p>
|
||||
|
||||
<p class="text-foreground/80 mt-6 mb-0 text-base">
|
||||
{{ t('cloudVerifyEmail_didntReceive') }}
|
||||
</p>
|
||||
|
||||
<p class="text-foreground/80 mt-1 text-base">
|
||||
<span class="cursor-pointer text-blue-400 no-underline" @click="onSend">
|
||||
{{ t('cloudVerifyEmail_resend') }}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useFirebaseAuth } from 'vuefire'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const auth = useFirebaseAuth()!
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const toastStore = useToastStore()
|
||||
|
||||
let intervalId: number | null = null
|
||||
let timeoutId: number | null = null
|
||||
const redirectInProgress = ref(false)
|
||||
|
||||
function clearPolling(): void {
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = null
|
||||
}
|
||||
}
|
||||
|
||||
async function redirectToNextStep(): Promise<void> {
|
||||
if (redirectInProgress.value) return
|
||||
|
||||
redirectInProgress.value = true
|
||||
clearPolling()
|
||||
|
||||
const inviteCode = route.query.inviteCode as string | undefined
|
||||
|
||||
if (inviteCode) {
|
||||
await router.push({
|
||||
name: 'cloud-invite-check',
|
||||
query: { inviteCode }
|
||||
})
|
||||
} else {
|
||||
await router.push({ name: 'cloud-user-check' })
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = async () => {
|
||||
const inviteCode = route.query.inviteCode as string | undefined
|
||||
const authStore = useFirebaseAuthStore()
|
||||
// If the user is already verified (email link already clicked),
|
||||
// continue to the next step automatically.
|
||||
if (authStore.isEmailVerified) {
|
||||
await router.push({
|
||||
name: 'cloud-invite-check',
|
||||
query: inviteCode ? { inviteCode } : {}
|
||||
})
|
||||
} else {
|
||||
await router.push({
|
||||
name: 'cloud-login',
|
||||
query: {
|
||||
inviteCode
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function onSend() {
|
||||
try {
|
||||
await authStore.verifyEmail()
|
||||
|
||||
// Track email verification requested
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackEmailVerification('requested')
|
||||
}
|
||||
|
||||
toastStore.add({
|
||||
severity: 'info',
|
||||
summary: t('cloudVerifyEmail_toast_title'),
|
||||
detail: t('cloudVerifyEmail_toast_summary'),
|
||||
life: 2000
|
||||
})
|
||||
} catch (e) {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('cloudVerifyEmail_toast_failed'),
|
||||
life: 2000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Track email verification screen opened
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackEmailVerification('opened')
|
||||
}
|
||||
|
||||
// If the user is already verified (email link already clicked),
|
||||
// continue to the next step automatically.
|
||||
if (authStore.isEmailVerified) {
|
||||
return redirectToNextStep()
|
||||
}
|
||||
|
||||
// Only send verification email automatically if coming from signup/login flow
|
||||
// Check if 'fromAuth' query parameter is present
|
||||
const fromAuth = route.query.fromAuth === 'true'
|
||||
if (fromAuth) {
|
||||
await onSend()
|
||||
// Remove fromAuth query parameter after sending email to prevent re-sending on refresh
|
||||
const { fromAuth: _, ...remainingQuery } = route.query
|
||||
await router.replace({
|
||||
name: route.name as string,
|
||||
query: remainingQuery
|
||||
})
|
||||
}
|
||||
|
||||
// Start polling to check email verification status
|
||||
intervalId = window.setInterval(async () => {
|
||||
if (auth.currentUser && !redirectInProgress.value) {
|
||||
await auth.currentUser.reload()
|
||||
if (auth.currentUser?.emailVerified) {
|
||||
// Track email verification completed
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackEmailVerification('completed')
|
||||
}
|
||||
void redirectToNextStep()
|
||||
}
|
||||
}
|
||||
}, 5000) // Check every 5 seconds
|
||||
|
||||
// Stop polling after 5 minutes
|
||||
timeoutId = window.setTimeout(
|
||||
() => {
|
||||
clearPolling()
|
||||
},
|
||||
5 * 60 * 1000
|
||||
)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearPolling()
|
||||
})
|
||||
</script>
|
||||
@@ -52,8 +52,7 @@ const router = useRouter()
|
||||
|
||||
const onSwitchAccounts = () => {
|
||||
void router.push({
|
||||
name: 'cloud-login',
|
||||
query: { switchAccount: 'true' }
|
||||
name: 'cloud-login'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -61,25 +61,19 @@ const {
|
||||
return
|
||||
}
|
||||
|
||||
// Survey is required for all users
|
||||
if (!surveyStatus) {
|
||||
skeletonType.value = 'survey'
|
||||
await router.replace({ name: 'cloud-survey' })
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should enforce whitelist requirement
|
||||
const requireWhitelist = window.__CONFIG__?.require_whitelist ?? true
|
||||
|
||||
// Check feature flag and redirect non-active users if whitelist is required
|
||||
if (requireWhitelist && cloudUserStats.status !== 'active') {
|
||||
// Feature flag ON: Show waitlist page for non-active users
|
||||
if (cloudUserStats.status !== 'active') {
|
||||
skeletonType.value = 'waitlist'
|
||||
await router.replace({ name: 'cloud-waitlist' })
|
||||
return
|
||||
}
|
||||
|
||||
// User is fully onboarded (active or whitelist check disabled)
|
||||
// User is fully onboarded
|
||||
window.location.href = '/'
|
||||
}),
|
||||
null,
|
||||
|
||||
@@ -14,7 +14,6 @@ type ServerHealthAlert = {
|
||||
*/
|
||||
export type RemoteConfig = {
|
||||
mixpanel_token?: string
|
||||
require_whitelist?: boolean
|
||||
subscription_required?: boolean
|
||||
server_health_alert?: ServerHealthAlert
|
||||
}
|
||||
|
||||
@@ -2,24 +2,15 @@ import type { OverridedMixpanel } from 'mixpanel-browser'
|
||||
|
||||
import type {
|
||||
AuthMetadata,
|
||||
CreditTopupMetadata,
|
||||
ExecutionContext,
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
NodeSearchMetadata,
|
||||
NodeSearchResultMetadata,
|
||||
PageVisibilityMetadata,
|
||||
RunButtonProperties,
|
||||
SurveyResponses,
|
||||
TabCountMetadata,
|
||||
TelemetryEventName,
|
||||
TelemetryEventProperties,
|
||||
TelemetryProvider,
|
||||
TemplateFilterMetadata,
|
||||
TemplateLibraryClosedMetadata,
|
||||
TemplateLibraryMetadata,
|
||||
TemplateMetadata,
|
||||
WorkflowImportMetadata
|
||||
TemplateMetadata
|
||||
} from '../../types'
|
||||
import { TelemetryEvents } from '../../types'
|
||||
|
||||
@@ -283,27 +274,6 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
this.trackEvent(eventName)
|
||||
}
|
||||
|
||||
trackAddApiCreditButtonClicked(): void {
|
||||
this.trackEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED)
|
||||
}
|
||||
|
||||
trackMonthlySubscriptionSucceeded(): void {
|
||||
this.trackEvent(TelemetryEvents.MONTHLY_SUBSCRIPTION_SUCCEEDED)
|
||||
}
|
||||
|
||||
trackApiCreditTopupButtonPurchaseClicked(amount: number): void {
|
||||
const metadata: CreditTopupMetadata = {
|
||||
credit_amount: amount
|
||||
}
|
||||
this.trackEvent(
|
||||
TelemetryEvents.API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED,
|
||||
metadata
|
||||
)
|
||||
}
|
||||
|
||||
trackApiCreditTopupSucceeded(): void {
|
||||
this.trackEvent(TelemetryEvents.API_CREDIT_TOPUP_SUCCEEDED)
|
||||
}
|
||||
trackRunButton(options?: { subscribe_to_run?: boolean }): void {
|
||||
if (this.isOnboardingMode) {
|
||||
// During onboarding, track basic run button click without workflow context
|
||||
@@ -377,42 +347,28 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
trackEmailVerification(stage: 'opened' | 'requested' | 'completed'): void {
|
||||
let eventName: TelemetryEventName
|
||||
|
||||
switch (stage) {
|
||||
case 'opened':
|
||||
eventName = TelemetryEvents.USER_EMAIL_VERIFY_OPENED
|
||||
break
|
||||
case 'requested':
|
||||
eventName = TelemetryEvents.USER_EMAIL_VERIFY_REQUESTED
|
||||
break
|
||||
case 'completed':
|
||||
eventName = TelemetryEvents.USER_EMAIL_VERIFY_COMPLETED
|
||||
break
|
||||
}
|
||||
|
||||
this.trackEvent(eventName)
|
||||
}
|
||||
|
||||
trackTemplate(metadata: TemplateMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.TEMPLATE_WORKFLOW_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.TEMPLATE_LIBRARY_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackTemplateLibraryClosed(metadata: TemplateLibraryClosedMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.TEMPLATE_LIBRARY_CLOSED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowImported(metadata: WorkflowImportMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.WORKFLOW_IMPORTED, metadata)
|
||||
}
|
||||
|
||||
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata)
|
||||
}
|
||||
|
||||
trackTabCount(metadata: TabCountMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.TAB_COUNT_TRACKING, metadata)
|
||||
}
|
||||
|
||||
trackNodeSearch(metadata: NodeSearchMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.NODE_SEARCH, metadata)
|
||||
}
|
||||
|
||||
trackNodeSearchResultSelected(metadata: NodeSearchResultMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.NODE_SEARCH_RESULT_SELECTED, metadata)
|
||||
}
|
||||
|
||||
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.TEMPLATE_FILTER_CHANGED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowExecution(): void {
|
||||
if (this.isOnboardingMode) {
|
||||
// During onboarding, track basic execution without workflow context
|
||||
|
||||
@@ -89,83 +89,6 @@ export interface TemplateMetadata {
|
||||
template_license?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit topup metadata
|
||||
*/
|
||||
export interface CreditTopupMetadata {
|
||||
credit_amount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow import metadata
|
||||
*/
|
||||
export interface WorkflowImportMetadata {
|
||||
missing_node_count: number
|
||||
missing_node_types: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Template library metadata
|
||||
*/
|
||||
export interface TemplateLibraryMetadata {
|
||||
source: 'sidebar' | 'menu' | 'command'
|
||||
}
|
||||
|
||||
/**
|
||||
* Template library closed metadata
|
||||
*/
|
||||
export interface TemplateLibraryClosedMetadata {
|
||||
template_selected: boolean
|
||||
time_spent_seconds: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Page visibility metadata
|
||||
*/
|
||||
export interface PageVisibilityMetadata {
|
||||
visibility_state: 'visible' | 'hidden'
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab count metadata
|
||||
*/
|
||||
export interface TabCountMetadata {
|
||||
tab_count: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Node search metadata
|
||||
*/
|
||||
export interface NodeSearchMetadata {
|
||||
query: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Node search result selection metadata
|
||||
*/
|
||||
export interface NodeSearchResultMetadata {
|
||||
node_type: string
|
||||
last_query: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Template filter tracking metadata
|
||||
*/
|
||||
export interface TemplateFilterMetadata {
|
||||
search_query?: string
|
||||
selected_models: string[]
|
||||
selected_use_cases: string[]
|
||||
selected_licenses: string[]
|
||||
sort_by:
|
||||
| 'default'
|
||||
| 'alphabetical'
|
||||
| 'newest'
|
||||
| 'vram-low-to-high'
|
||||
| 'model-size-low-to-high'
|
||||
filtered_count: number
|
||||
total_count: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Core telemetry provider interface
|
||||
*/
|
||||
@@ -176,35 +99,16 @@ export interface TelemetryProvider {
|
||||
|
||||
// Subscription flow events
|
||||
trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void
|
||||
trackMonthlySubscriptionSucceeded(): void
|
||||
trackAddApiCreditButtonClicked(): void
|
||||
trackApiCreditTopupButtonPurchaseClicked(amount: number): void
|
||||
trackApiCreditTopupSucceeded(): void
|
||||
trackRunButton(options?: { subscribe_to_run?: boolean }): void
|
||||
|
||||
// Survey flow events
|
||||
trackSurvey(stage: 'opened' | 'submitted', responses?: SurveyResponses): void
|
||||
|
||||
// Email verification events
|
||||
trackEmailVerification(stage: 'opened' | 'requested' | 'completed'): void
|
||||
|
||||
// Template workflow events
|
||||
trackTemplate(metadata: TemplateMetadata): void
|
||||
trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void
|
||||
trackTemplateLibraryClosed(metadata: TemplateLibraryClosedMetadata): void
|
||||
|
||||
// Workflow management events
|
||||
trackWorkflowImported(metadata: WorkflowImportMetadata): void
|
||||
|
||||
// Page visibility events
|
||||
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void
|
||||
|
||||
// Tab tracking events
|
||||
trackTabCount(metadata: TabCountMetadata): void
|
||||
|
||||
// Node search analytics events
|
||||
trackNodeSearch(metadata: NodeSearchMetadata): void
|
||||
trackNodeSearchResultSelected(metadata: NodeSearchResultMetadata): void
|
||||
|
||||
// Template filter tracking events
|
||||
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void
|
||||
|
||||
// Workflow execution events
|
||||
trackWorkflowExecution(): void
|
||||
@@ -232,36 +136,18 @@ export const TelemetryEvents = {
|
||||
RUN_BUTTON_CLICKED: 'app:run_button_click',
|
||||
SUBSCRIPTION_REQUIRED_MODAL_OPENED: 'app:subscription_required_modal_opened',
|
||||
SUBSCRIBE_NOW_BUTTON_CLICKED: 'app:subscribe_now_button_clicked',
|
||||
MONTHLY_SUBSCRIPTION_SUCCEEDED: 'app:monthly_subscription_succeeded',
|
||||
ADD_API_CREDIT_BUTTON_CLICKED: 'app:add_api_credit_button_clicked',
|
||||
API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED:
|
||||
'app:api_credit_topup_button_purchase_clicked',
|
||||
API_CREDIT_TOPUP_SUCCEEDED: 'app:api_credit_topup_succeeded',
|
||||
|
||||
// Onboarding Survey
|
||||
USER_SURVEY_OPENED: 'app:user_survey_opened',
|
||||
USER_SURVEY_SUBMITTED: 'app:user_survey_submitted',
|
||||
|
||||
// Email Verification
|
||||
USER_EMAIL_VERIFY_OPENED: 'app:user_email_verify_opened',
|
||||
USER_EMAIL_VERIFY_REQUESTED: 'app:user_email_verify_requested',
|
||||
USER_EMAIL_VERIFY_COMPLETED: 'app:user_email_verify_completed',
|
||||
|
||||
// Template Tracking
|
||||
TEMPLATE_WORKFLOW_OPENED: 'app:template_workflow_opened',
|
||||
TEMPLATE_LIBRARY_OPENED: 'app:template_library_opened',
|
||||
TEMPLATE_LIBRARY_CLOSED: 'app:template_library_closed',
|
||||
|
||||
// Workflow Management
|
||||
WORKFLOW_IMPORTED: 'app:workflow_imported',
|
||||
|
||||
// Page Visibility
|
||||
PAGE_VISIBILITY_CHANGED: 'app:page_visibility_changed',
|
||||
|
||||
// Tab Tracking
|
||||
TAB_COUNT_TRACKING: 'app:tab_count_tracking',
|
||||
|
||||
// Node Search Analytics
|
||||
NODE_SEARCH: 'app:node_search',
|
||||
NODE_SEARCH_RESULT_SELECTED: 'app:node_search_result_selected',
|
||||
|
||||
// Template Filter Analytics
|
||||
TEMPLATE_FILTER_CHANGED: 'app:template_filter_changed',
|
||||
|
||||
// Execution Lifecycle
|
||||
EXECUTION_START: 'execution_start',
|
||||
@@ -283,12 +169,3 @@ export type TelemetryEventProperties =
|
||||
| RunButtonProperties
|
||||
| ExecutionErrorMetadata
|
||||
| ExecutionSuccessMetadata
|
||||
| CreditTopupMetadata
|
||||
| WorkflowImportMetadata
|
||||
| TemplateLibraryMetadata
|
||||
| TemplateLibraryClosedMetadata
|
||||
| PageVisibilityMetadata
|
||||
| TabCountMetadata
|
||||
| NodeSearchMetadata
|
||||
| NodeSearchResultMetadata
|
||||
| TemplateFilterMetadata
|
||||
|
||||
@@ -129,13 +129,8 @@ router.beforeEach(async (to, _from, next) => {
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Auth initialization failed:', error)
|
||||
// Navigate to auth timeout recovery page with error details
|
||||
return next({
|
||||
name: 'cloud-auth-timeout',
|
||||
params: {
|
||||
errorMessage: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
})
|
||||
// Navigate to auth timeout recovery page
|
||||
return next({ name: 'cloud-auth-timeout' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,28 +178,32 @@ router.beforeEach(async (to, _from, next) => {
|
||||
// User is logged in - check if they need onboarding
|
||||
// For root path, check actual user status to handle waitlisted users
|
||||
if (!isElectron() && isLoggedIn && to.path === '/') {
|
||||
// Import auth functions dynamically to avoid circular dependency
|
||||
const { getUserCloudStatus, getSurveyCompletedStatus } = await import(
|
||||
'@/api/auth'
|
||||
)
|
||||
try {
|
||||
// Check email verification first
|
||||
const authStore = useFirebaseAuthStore()
|
||||
if (!authStore.isEmailVerified) {
|
||||
// Don't pass fromAuth here since this is from root navigation, not auth flow
|
||||
return next({ name: 'cloud-verify-email' })
|
||||
}
|
||||
|
||||
// Import auth functions dynamically to avoid circular dependency
|
||||
const { getUserCloudStatus, getSurveyCompletedStatus } = await import(
|
||||
'@/api/auth'
|
||||
)
|
||||
|
||||
// Check user's actual status
|
||||
const userStatus = await getUserCloudStatus()
|
||||
const surveyCompleted = await getSurveyCompletedStatus()
|
||||
|
||||
// Survey is required for all users regardless of whitelist status
|
||||
if (!surveyCompleted) {
|
||||
return next({ name: 'cloud-survey' })
|
||||
// If user is not active (waitlisted), redirect based on survey status
|
||||
if (userStatus.status !== 'active') {
|
||||
if (!surveyCompleted) {
|
||||
return next({ name: 'cloud-survey' })
|
||||
} else {
|
||||
return next({ name: 'cloud-waitlist' })
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should enforce whitelist requirement
|
||||
const requireWhitelist = window.__CONFIG__?.require_whitelist ?? true
|
||||
|
||||
// Check feature flag and redirect non-active users if whitelist is required
|
||||
if (requireWhitelist && userStatus.status !== 'active') {
|
||||
return next({ name: 'cloud-waitlist' })
|
||||
}
|
||||
// User is active or whitelist check disabled: Allow access to root
|
||||
// User is active, allow access to root
|
||||
} catch (error) {
|
||||
console.error('Failed to check user status:', error)
|
||||
// On error, redirect to user-check as fallback
|
||||
|
||||
@@ -542,10 +542,9 @@ export class ComfyApi extends EventTarget {
|
||||
let existingSession = window.name
|
||||
|
||||
// Get auth token if available
|
||||
// Force refresh on reconnect to avoid stale tokens
|
||||
let authToken: string | undefined
|
||||
try {
|
||||
authToken = await useFirebaseAuthStore().getIdToken(isReconnect)
|
||||
authToken = await useFirebaseAuthStore().getIdToken()
|
||||
} catch (error) {
|
||||
// Continue without auth token if there's an error
|
||||
console.warn('Could not get auth token for WebSocket connection:', error)
|
||||
|
||||
@@ -179,7 +179,7 @@ export const useCustomerEventsService = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = await executeRequest<CustomerEventsResponse>(
|
||||
return executeRequest<CustomerEventsResponse>(
|
||||
() =>
|
||||
customerApiClient.get('/customers/events', {
|
||||
params: { page, limit },
|
||||
@@ -187,8 +187,6 @@ export const useCustomerEventsService = () => {
|
||||
}),
|
||||
{ errorContext, routeSpecificErrors }
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { merge } from 'es-toolkit/compat'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
|
||||
@@ -69,7 +70,6 @@ export const useDialogService = () => {
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
| 'subscription'
|
||||
) {
|
||||
const props = panel ? { props: { defaultPanel: panel } } : undefined
|
||||
|
||||
@@ -487,16 +487,35 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
async function showSubscriptionRequiredDialog() {
|
||||
function showSubscriptionRequiredDialog() {
|
||||
if (!isCloud || !window.__CONFIG__?.subscription_required) {
|
||||
return
|
||||
}
|
||||
|
||||
const { useSubscriptionDialog } = await import(
|
||||
'@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
)
|
||||
const { show } = useSubscriptionDialog()
|
||||
show()
|
||||
dialogStore.showDialog({
|
||||
key: 'subscription-required',
|
||||
component: defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'@/platform/cloud/subscription/components/SubscriptionRequiredDialogContent.vue'
|
||||
)
|
||||
),
|
||||
props: {
|
||||
onClose: () => {
|
||||
dialogStore.closeDialog({ key: 'subscription-required' })
|
||||
}
|
||||
},
|
||||
dialogComponentProps: {
|
||||
closable: true,
|
||||
style: 'width: 700px;',
|
||||
pt: {
|
||||
header: { class: '!p-0 !m-0' },
|
||||
content: {
|
||||
class: 'overflow-hidden !p-0 !m-0'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getAdditionalUserInfo,
|
||||
onAuthStateChanged,
|
||||
onIdTokenChanged,
|
||||
sendEmailVerification,
|
||||
sendPasswordResetEmail,
|
||||
setPersistence,
|
||||
signInWithEmailAndPassword,
|
||||
@@ -81,6 +82,9 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const isAuthenticated = computed(() => !!currentUser.value)
|
||||
const userEmail = computed(() => currentUser.value?.email)
|
||||
const userId = computed(() => currentUser.value?.uid)
|
||||
const isEmailVerified = computed(
|
||||
() => currentUser.value?.emailVerified ?? false
|
||||
)
|
||||
|
||||
// Get auth from VueFire and listen for auth state changes
|
||||
// From useFirebaseAuth docs:
|
||||
@@ -106,12 +110,10 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const getIdToken = async (
|
||||
forceRefresh = false
|
||||
): Promise<string | undefined> => {
|
||||
const getIdToken = async (): Promise<string | undefined> => {
|
||||
if (!currentUser.value) return
|
||||
try {
|
||||
return await currentUser.value.getIdToken(forceRefresh)
|
||||
return await currentUser.value.getIdToken()
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error instanceof FirebaseError &&
|
||||
@@ -142,11 +144,9 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
* - An ApiKeyAuthHeader with X-API-KEY if API key exists
|
||||
* - null if neither authentication method is available
|
||||
*/
|
||||
const getAuthHeader = async (
|
||||
forceRefresh = false
|
||||
): Promise<AuthHeader | null> => {
|
||||
const getAuthHeader = async (): Promise<AuthHeader | null> => {
|
||||
// If available, set header with JWT used to identify the user to Firebase service
|
||||
const token = await getIdToken(forceRefresh)
|
||||
const token = await getIdToken()
|
||||
if (token) {
|
||||
return {
|
||||
Authorization: `Bearer ${token}`
|
||||
@@ -349,6 +349,14 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
await updatePassword(currentUser.value, newPassword)
|
||||
}
|
||||
|
||||
/** Send email verification to current user */
|
||||
const verifyEmail = async (): Promise<void> => {
|
||||
if (!currentUser.value) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
await sendEmailVerification(currentUser.value)
|
||||
}
|
||||
|
||||
/** Delete the current user account */
|
||||
const _deleteAccount = async (): Promise<void> => {
|
||||
if (!currentUser.value) {
|
||||
@@ -432,6 +440,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
// State
|
||||
loading,
|
||||
currentUser,
|
||||
isEmailVerified,
|
||||
isInitialized,
|
||||
balance,
|
||||
lastBalanceUpdateTime,
|
||||
@@ -457,6 +466,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
sendPasswordReset,
|
||||
updatePassword: _updatePassword,
|
||||
getAuthHeader,
|
||||
verifyEmail,
|
||||
deleteAccount: _deleteAccount
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { AuditLog } from '@/services/customerEventsService'
|
||||
import {
|
||||
EventType,
|
||||
useCustomerEventsService
|
||||
} from '@/services/customerEventsService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
type PendingTopupRecord = {
|
||||
startedAtIso: string
|
||||
amountUsd?: number
|
||||
expectedCents?: number
|
||||
}
|
||||
|
||||
const storageKeyForUser = (userId: string) => `topupTracker:pending:${userId}`
|
||||
|
||||
export const useTopupTrackerStore = defineStore('topupTracker', () => {
|
||||
const telemetry = useTelemetry()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const pendingTopup = ref<PendingTopupRecord | null>(null)
|
||||
const storageListenerInitialized = ref(false)
|
||||
|
||||
const loadFromStorage = () => {
|
||||
const userId = authStore.userId
|
||||
if (!userId) return
|
||||
try {
|
||||
const rawValue = localStorage.getItem(storageKeyForUser(userId))
|
||||
if (!rawValue) return
|
||||
const parsedValue = JSON.parse(rawValue) as PendingTopupRecord
|
||||
pendingTopup.value = parsedValue
|
||||
} catch {
|
||||
pendingTopup.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const persistToStorage = () => {
|
||||
const userId = authStore.userId
|
||||
if (!userId) return
|
||||
if (pendingTopup.value) {
|
||||
localStorage.setItem(
|
||||
storageKeyForUser(userId),
|
||||
JSON.stringify(pendingTopup.value)
|
||||
)
|
||||
} else {
|
||||
localStorage.removeItem(storageKeyForUser(userId))
|
||||
}
|
||||
}
|
||||
|
||||
const initializeStorageSynchronization = () => {
|
||||
if (storageListenerInitialized.value) return
|
||||
storageListenerInitialized.value = true
|
||||
loadFromStorage()
|
||||
window.addEventListener('storage', (e: StorageEvent) => {
|
||||
const userId = authStore.userId
|
||||
if (!userId) return
|
||||
if (e.key === storageKeyForUser(userId)) {
|
||||
loadFromStorage()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => authStore.userId,
|
||||
(newUserId, oldUserId) => {
|
||||
if (newUserId && newUserId !== oldUserId) {
|
||||
loadFromStorage()
|
||||
return
|
||||
}
|
||||
if (!newUserId && oldUserId) {
|
||||
pendingTopup.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const startTopup = (amountUsd: number) => {
|
||||
const userId = authStore.userId
|
||||
if (!userId) return
|
||||
const expectedCents = Math.round(amountUsd * 100)
|
||||
pendingTopup.value = {
|
||||
startedAtIso: new Date().toISOString(),
|
||||
amountUsd,
|
||||
expectedCents
|
||||
}
|
||||
persistToStorage()
|
||||
}
|
||||
|
||||
const clearTopup = () => {
|
||||
pendingTopup.value = null
|
||||
persistToStorage()
|
||||
}
|
||||
|
||||
const reconcileWithEvents = async (
|
||||
events: AuditLog[] | undefined | null
|
||||
): Promise<boolean> => {
|
||||
if (!events || events.length === 0) return false
|
||||
if (!pendingTopup.value) return false
|
||||
|
||||
const startedAt = new Date(pendingTopup.value.startedAtIso)
|
||||
if (Number.isNaN(+startedAt)) {
|
||||
clearTopup()
|
||||
return false
|
||||
}
|
||||
|
||||
const withinWindow = (createdAt: string) => {
|
||||
const created = new Date(createdAt)
|
||||
if (Number.isNaN(+created)) return false
|
||||
const maxAgeMs = 1000 * 60 * 60 * 24
|
||||
return (
|
||||
created >= startedAt &&
|
||||
created.getTime() - startedAt.getTime() <= maxAgeMs
|
||||
)
|
||||
}
|
||||
|
||||
let matched = events.filter((e) => {
|
||||
if (e.event_type !== EventType.CREDIT_ADDED) return false
|
||||
if (!e.createdAt || !withinWindow(e.createdAt)) return false
|
||||
return true
|
||||
})
|
||||
|
||||
if (pendingTopup.value.expectedCents != null) {
|
||||
matched = matched.filter((e) =>
|
||||
typeof e.params?.amount === 'number'
|
||||
? e.params.amount === pendingTopup.value?.expectedCents
|
||||
: true
|
||||
)
|
||||
}
|
||||
|
||||
if (matched.length === 0) return false
|
||||
|
||||
telemetry?.trackApiCreditTopupSucceeded()
|
||||
await authStore.fetchBalance().catch(() => {})
|
||||
clearTopup()
|
||||
return true
|
||||
}
|
||||
|
||||
const reconcileByFetchingEvents = async (): Promise<boolean> => {
|
||||
const service = useCustomerEventsService()
|
||||
const response = await service.getMyEvents({ page: 1, limit: 10 })
|
||||
if (!response) return false
|
||||
return await reconcileWithEvents(response.events)
|
||||
}
|
||||
|
||||
initializeStorageSynchronization()
|
||||
|
||||
return {
|
||||
pendingTopup,
|
||||
startTopup,
|
||||
clearTopup,
|
||||
reconcileWithEvents,
|
||||
reconcileByFetchingEvents
|
||||
}
|
||||
})
|
||||
@@ -1,213 +0,0 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SubscriptionPanel from '@/platform/cloud/subscription/components/SubscriptionPanel.vue'
|
||||
|
||||
// Mock composables
|
||||
const mockSubscriptionData = {
|
||||
isActiveSubscription: false,
|
||||
isCancelled: false,
|
||||
formattedRenewalDate: '2024-12-31',
|
||||
formattedEndDate: '2024-12-31',
|
||||
formattedMonthlyPrice: '$9.99',
|
||||
manageSubscription: vi.fn(),
|
||||
handleInvoiceHistory: vi.fn()
|
||||
}
|
||||
|
||||
const mockCreditsData = {
|
||||
totalCredits: '10.00',
|
||||
monthlyBonusCredits: '5.00',
|
||||
prepaidCredits: '5.00',
|
||||
isLoadingBalance: false
|
||||
}
|
||||
|
||||
const mockActionsData = {
|
||||
isLoadingSupport: false,
|
||||
refreshTooltip: 'Refreshes on 2024-12-31',
|
||||
handleAddApiCredits: vi.fn(),
|
||||
handleMessageSupport: vi.fn(),
|
||||
handleRefresh: vi.fn(),
|
||||
handleLearnMoreClick: vi.fn()
|
||||
}
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: () => mockSubscriptionData
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'@/platform/cloud/subscription/composables/useSubscriptionCredits',
|
||||
() => ({
|
||||
useSubscriptionCredits: () => mockCreditsData
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock(
|
||||
'@/platform/cloud/subscription/composables/useSubscriptionActions',
|
||||
() => ({
|
||||
useSubscriptionActions: () => mockActionsData
|
||||
})
|
||||
)
|
||||
|
||||
// Create i18n instance for testing
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
subscription: {
|
||||
title: 'Subscription',
|
||||
perMonth: '/ month',
|
||||
subscribeNow: 'Subscribe Now',
|
||||
manageSubscription: 'Manage Subscription',
|
||||
partnerNodesBalance: 'Partner Nodes Balance',
|
||||
partnerNodesDescription: 'Credits for partner nodes',
|
||||
totalCredits: 'Total Credits',
|
||||
monthlyBonusDescription: 'Monthly bonus',
|
||||
prepaidDescription: 'Prepaid credits',
|
||||
monthlyCreditsRollover: 'Monthly credits rollover info',
|
||||
prepaidCreditsInfo: 'Prepaid credits info',
|
||||
viewUsageHistory: 'View Usage History',
|
||||
addCredits: 'Add Credits',
|
||||
yourPlanIncludes: 'Your plan includes',
|
||||
learnMore: 'Learn More',
|
||||
messageSupport: 'Message Support',
|
||||
invoiceHistory: 'Invoice History',
|
||||
renewsDate: 'Renews {date}',
|
||||
expiresDate: 'Expires {date}'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function createWrapper(overrides = {}) {
|
||||
return mount(SubscriptionPanel, {
|
||||
global: {
|
||||
plugins: [createTestingPinia({ createSpy: vi.fn }), i18n],
|
||||
stubs: {
|
||||
CloudBadge: true,
|
||||
SubscribeButton: true,
|
||||
SubscriptionBenefits: true,
|
||||
Button: {
|
||||
template:
|
||||
'<button @click="$emit(\'click\')" :disabled="loading" :data-testid="label" :data-icon="icon">{{ label }}</button>',
|
||||
props: [
|
||||
'loading',
|
||||
'label',
|
||||
'icon',
|
||||
'text',
|
||||
'severity',
|
||||
'size',
|
||||
'iconPos',
|
||||
'pt'
|
||||
],
|
||||
emits: ['click']
|
||||
},
|
||||
Skeleton: {
|
||||
template: '<div class="skeleton"></div>'
|
||||
}
|
||||
}
|
||||
},
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
|
||||
describe('SubscriptionPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('subscription state functionality', () => {
|
||||
it('shows correct UI for active subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = true
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('Manage Subscription')
|
||||
expect(wrapper.text()).toContain('Add Credits')
|
||||
})
|
||||
|
||||
it('shows correct UI for inactive subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = false
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.findComponent({ name: 'SubscribeButton' }).exists()).toBe(
|
||||
true
|
||||
)
|
||||
expect(wrapper.text()).not.toContain('Manage Subscription')
|
||||
expect(wrapper.text()).not.toContain('Add Credits')
|
||||
})
|
||||
|
||||
it('shows renewal date for active non-cancelled subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = true
|
||||
mockSubscriptionData.isCancelled = false
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('Renews 2024-12-31')
|
||||
})
|
||||
|
||||
it('shows expiry date for cancelled subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = true
|
||||
mockSubscriptionData.isCancelled = true
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('Expires 2024-12-31')
|
||||
})
|
||||
})
|
||||
|
||||
describe('credit display functionality', () => {
|
||||
it('displays dynamic credit values correctly', () => {
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('$10.00') // totalCredits
|
||||
expect(wrapper.text()).toContain('$5.00') // both monthlyBonus and prepaid
|
||||
})
|
||||
|
||||
it('shows loading skeleton when fetching balance', () => {
|
||||
mockCreditsData.isLoadingBalance = true
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.findAll('.skeleton').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('hides skeleton when balance loaded', () => {
|
||||
mockCreditsData.isLoadingBalance = false
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.findAll('.skeleton').length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('action buttons', () => {
|
||||
it('should call handleLearnMoreClick when learn more is clicked', async () => {
|
||||
const wrapper = createWrapper()
|
||||
const learnMoreButton = wrapper.find('[data-testid="Learn More"]')
|
||||
await learnMoreButton.trigger('click')
|
||||
expect(mockActionsData.handleLearnMoreClick).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call handleMessageSupport when message support is clicked', async () => {
|
||||
const wrapper = createWrapper()
|
||||
const supportButton = wrapper.find('[data-testid="Message Support"]')
|
||||
await supportButton.trigger('click')
|
||||
expect(mockActionsData.handleMessageSupport).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call handleRefresh when refresh button is clicked', async () => {
|
||||
const wrapper = createWrapper()
|
||||
// Find the refresh button by icon
|
||||
const refreshButton = wrapper.find('[data-icon="pi pi-sync"]')
|
||||
await refreshButton.trigger('click')
|
||||
expect(mockActionsData.handleRefresh).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('loading states', () => {
|
||||
it('should show loading state on support button when loading', () => {
|
||||
mockActionsData.isLoadingSupport = true
|
||||
const wrapper = createWrapper()
|
||||
const supportButton = wrapper.find('[data-testid="Message Support"]')
|
||||
expect(supportButton.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('should show loading state on refresh button when loading balance', () => {
|
||||
mockCreditsData.isLoadingBalance = true
|
||||
const wrapper = createWrapper()
|
||||
const refreshButton = wrapper.find('[data-icon="pi pi-sync"]')
|
||||
expect(refreshButton.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,137 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSubscriptionActions } from '@/platform/cloud/subscription/composables/useSubscriptionActions'
|
||||
|
||||
// Mock dependencies
|
||||
const mockFetchBalance = vi.fn()
|
||||
const mockFetchStatus = vi.fn()
|
||||
const mockShowTopUpCreditsDialog = vi.fn()
|
||||
const mockExecute = vi.fn()
|
||||
const mockT = vi.fn((key: string) => {
|
||||
if (key === 'subscription.nextBillingCycle') return 'next billing cycle'
|
||||
return key
|
||||
})
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: mockT
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: () => ({
|
||||
fetchBalance: mockFetchBalance
|
||||
})
|
||||
}))
|
||||
|
||||
const mockFormattedRenewalDate = { value: '2024-12-31' }
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: () => ({
|
||||
fetchStatus: mockFetchStatus,
|
||||
formattedRenewalDate: mockFormattedRenewalDate
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => ({
|
||||
showTopUpCreditsDialog: mockShowTopUpCreditsDialog
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => ({
|
||||
execute: mockExecute
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock window.open
|
||||
const mockOpen = vi.fn()
|
||||
Object.defineProperty(window, 'open', {
|
||||
writable: true,
|
||||
value: mockOpen
|
||||
})
|
||||
|
||||
describe('useSubscriptionActions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockFormattedRenewalDate.value = '2024-12-31'
|
||||
})
|
||||
|
||||
describe('refreshTooltip', () => {
|
||||
it('should format tooltip with renewal date', () => {
|
||||
const { refreshTooltip } = useSubscriptionActions()
|
||||
expect(refreshTooltip.value).toBe('Refreshes on 2024-12-31')
|
||||
})
|
||||
|
||||
it('should use fallback text when no renewal date', () => {
|
||||
mockFormattedRenewalDate.value = ''
|
||||
const { refreshTooltip } = useSubscriptionActions()
|
||||
expect(refreshTooltip.value).toBe('Refreshes on next billing cycle')
|
||||
expect(mockT).toHaveBeenCalledWith('subscription.nextBillingCycle')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleAddApiCredits', () => {
|
||||
it('should call showTopUpCreditsDialog', () => {
|
||||
const { handleAddApiCredits } = useSubscriptionActions()
|
||||
handleAddApiCredits()
|
||||
expect(mockShowTopUpCreditsDialog).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleMessageSupport', () => {
|
||||
it('should execute support command and manage loading state', async () => {
|
||||
const { handleMessageSupport, isLoadingSupport } =
|
||||
useSubscriptionActions()
|
||||
|
||||
expect(isLoadingSupport.value).toBe(false)
|
||||
|
||||
const promise = handleMessageSupport()
|
||||
expect(isLoadingSupport.value).toBe(true)
|
||||
|
||||
await promise
|
||||
expect(mockExecute).toHaveBeenCalledWith('Comfy.ContactSupport')
|
||||
expect(isLoadingSupport.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
mockExecute.mockRejectedValueOnce(new Error('Command failed'))
|
||||
const { handleMessageSupport, isLoadingSupport } =
|
||||
useSubscriptionActions()
|
||||
|
||||
await handleMessageSupport()
|
||||
expect(isLoadingSupport.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleRefresh', () => {
|
||||
it('should call both fetchBalance and fetchStatus', async () => {
|
||||
const { handleRefresh } = useSubscriptionActions()
|
||||
await handleRefresh()
|
||||
|
||||
expect(mockFetchBalance).toHaveBeenCalledOnce()
|
||||
expect(mockFetchStatus).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
mockFetchBalance.mockRejectedValueOnce(new Error('Fetch failed'))
|
||||
const { handleRefresh } = useSubscriptionActions()
|
||||
|
||||
// Should not throw
|
||||
await expect(handleRefresh()).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleLearnMoreClick', () => {
|
||||
it('should open learn more URL', () => {
|
||||
const { handleLearnMoreClick } = useSubscriptionActions()
|
||||
handleLearnMoreClick()
|
||||
|
||||
expect(mockOpen).toHaveBeenCalledWith(
|
||||
'https://docs.comfy.org/get_started/cloud',
|
||||
'_blank'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,146 +0,0 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSubscriptionCredits } from '@/platform/cloud/subscription/composables/useSubscriptionCredits'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
// Mock Firebase Auth and related modules
|
||||
vi.mock('vuefire', () => ({
|
||||
useFirebaseAuth: vi.fn(() => ({
|
||||
onAuthStateChanged: vi.fn(),
|
||||
setPersistence: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('firebase/auth', () => ({
|
||||
onAuthStateChanged: vi.fn(() => {
|
||||
// Mock the callback to be called immediately for testing
|
||||
return vi.fn()
|
||||
}),
|
||||
onIdTokenChanged: vi.fn(),
|
||||
setPersistence: vi.fn().mockResolvedValue(undefined),
|
||||
browserLocalPersistence: {},
|
||||
GoogleAuthProvider: class {
|
||||
addScope = vi.fn()
|
||||
setCustomParameters = vi.fn()
|
||||
},
|
||||
GithubAuthProvider: class {
|
||||
addScope = vi.fn()
|
||||
setCustomParameters = vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock other dependencies
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => ({
|
||||
showDialog: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => ({
|
||||
track: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/toastStore', () => ({
|
||||
useToastStore: () => ({
|
||||
add: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/apiKeyAuthStore', () => ({
|
||||
useApiKeyAuthStore: () => ({
|
||||
headers: {}
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock formatMetronomeCurrency
|
||||
vi.mock('@/utils/formatUtil', () => ({
|
||||
formatMetronomeCurrency: vi.fn((micros: number) => {
|
||||
// Simple mock that converts micros to dollars
|
||||
return (micros / 1000000).toFixed(2)
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useSubscriptionCredits', () => {
|
||||
let authStore: ReturnType<typeof useFirebaseAuthStore>
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
authStore = useFirebaseAuthStore()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('totalCredits', () => {
|
||||
it('should return "0.00" when balance is null', () => {
|
||||
authStore.balance = null
|
||||
const { totalCredits } = useSubscriptionCredits()
|
||||
expect(totalCredits.value).toBe('0.00')
|
||||
})
|
||||
|
||||
it('should return "0.00" when amount_micros is missing', () => {
|
||||
authStore.balance = {} as any
|
||||
const { totalCredits } = useSubscriptionCredits()
|
||||
expect(totalCredits.value).toBe('0.00')
|
||||
})
|
||||
|
||||
it('should format amount_micros correctly', () => {
|
||||
authStore.balance = { amount_micros: 5000000 } as any
|
||||
const { totalCredits } = useSubscriptionCredits()
|
||||
expect(totalCredits.value).toBe('5.00')
|
||||
})
|
||||
|
||||
it('should handle formatting errors gracefully', async () => {
|
||||
const mockFormatMetronomeCurrency = vi.mocked(
|
||||
await import('@/utils/formatUtil')
|
||||
).formatMetronomeCurrency
|
||||
mockFormatMetronomeCurrency.mockImplementationOnce(() => {
|
||||
throw new Error('Formatting error')
|
||||
})
|
||||
|
||||
authStore.balance = { amount_micros: 5000000 } as any
|
||||
const { totalCredits } = useSubscriptionCredits()
|
||||
expect(totalCredits.value).toBe('0.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('monthlyBonusCredits', () => {
|
||||
it('should return "0.00" when cloud_credit_balance_micros is missing', () => {
|
||||
authStore.balance = {} as any
|
||||
const { monthlyBonusCredits } = useSubscriptionCredits()
|
||||
expect(monthlyBonusCredits.value).toBe('0.00')
|
||||
})
|
||||
|
||||
it('should format cloud_credit_balance_micros correctly', () => {
|
||||
authStore.balance = { cloud_credit_balance_micros: 2500000 } as any
|
||||
const { monthlyBonusCredits } = useSubscriptionCredits()
|
||||
expect(monthlyBonusCredits.value).toBe('2.50')
|
||||
})
|
||||
})
|
||||
|
||||
describe('prepaidCredits', () => {
|
||||
it('should return "0.00" when prepaid_balance_micros is missing', () => {
|
||||
authStore.balance = {} as any
|
||||
const { prepaidCredits } = useSubscriptionCredits()
|
||||
expect(prepaidCredits.value).toBe('0.00')
|
||||
})
|
||||
|
||||
it('should format prepaid_balance_micros correctly', () => {
|
||||
authStore.balance = { prepaid_balance_micros: 7500000 } as any
|
||||
const { prepaidCredits } = useSubscriptionCredits()
|
||||
expect(prepaidCredits.value).toBe('7.50')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isLoadingBalance', () => {
|
||||
it('should reflect authStore.isFetchingBalance', () => {
|
||||
authStore.isFetchingBalance = true
|
||||
const { isLoadingBalance } = useSubscriptionCredits()
|
||||
expect(isLoadingBalance.value).toBe(true)
|
||||
|
||||
authStore.isFetchingBalance = false
|
||||
expect(isLoadingBalance.value).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
import { sentryVitePlugin } from '@sentry/vite-plugin'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import dotenv from 'dotenv'
|
||||
@@ -227,28 +226,7 @@ export default defineConfig({
|
||||
deep: true,
|
||||
extensions: ['vue'],
|
||||
directoryAsNamespace: true
|
||||
}),
|
||||
|
||||
// Sentry sourcemap upload plugin
|
||||
// Only runs during cloud production builds when all Sentry env vars are present
|
||||
// Requires: SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT env vars
|
||||
...(DISTRIBUTION === 'cloud' &&
|
||||
process.env.SENTRY_AUTH_TOKEN &&
|
||||
process.env.SENTRY_ORG &&
|
||||
process.env.SENTRY_PROJECT &&
|
||||
!IS_DEV
|
||||
? [
|
||||
sentryVitePlugin({
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
sourcemaps: {
|
||||
// Delete source maps after upload to prevent public access
|
||||
filesToDeleteAfterUpload: ['**/*.map']
|
||||
}
|
||||
})
|
||||
]
|
||||
: [])
|
||||
})
|
||||
],
|
||||
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user