Compare commits
2 Commits
vue-node/s
...
graph-mode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c30e1faa22 | ||
|
|
8bb1d5724b |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 78 KiB |
@@ -73,7 +73,6 @@
|
||||
|
||||
--color-jade-400: #47e469;
|
||||
--color-jade-600: #00cd72;
|
||||
--color-graphite-400: #9C9EAB;
|
||||
|
||||
--color-gold-400: #fcbf64;
|
||||
--color-gold-500: #fdab34;
|
||||
@@ -228,7 +227,7 @@
|
||||
--brand-yellow: var(--color-electric-400);
|
||||
--brand-blue: var(--color-sapphire-700);
|
||||
--secondary-background: var(--color-smoke-200);
|
||||
--secondary-background-hover: var(--color-smoke-200);
|
||||
--secondary-background-hover: var(--color-smoke-400);
|
||||
--secondary-background-selected: var(--color-smoke-600);
|
||||
--base-background: var(--color-white);
|
||||
--primary-background: var(--color-azure-400);
|
||||
@@ -243,17 +242,6 @@
|
||||
--muted-background: var(--color-smoke-700);
|
||||
--accent-background: var(--color-smoke-800);
|
||||
|
||||
/* Component/Node tokens from design system light */
|
||||
--component-node-background: var(--color-white);
|
||||
--component-node-border: var(--color-border-default);
|
||||
--component-node-foreground: var(--base-foreground);
|
||||
--component-node-foreground-secondary: var(--color-muted-foreground);
|
||||
--component-node-widget-background: var(--secondary-background);
|
||||
--component-node-widget-background-hovered: var(--secondary-background-hover);
|
||||
--component-node-widget-background-selected: var(--secondary-background-selected);
|
||||
--component-node-widget-background-disabled: var(--color-alpha-ash-500-20);
|
||||
--component-node-widget-background-highlighted: var(--color-ash-500);
|
||||
|
||||
/* Default UI element color palette variables */
|
||||
--palette-contrast-mix-color: #fff;
|
||||
--palette-interface-panel-surface: var(--comfy-menu-bg);
|
||||
@@ -313,7 +301,7 @@
|
||||
--node-component-surface-highlight: var(--color-slate-100);
|
||||
--node-component-surface-hovered: var(--color-charcoal-600);
|
||||
--node-component-surface-selected: var(--color-charcoal-200);
|
||||
--node-component-surface: var(--color-charcoal-600);
|
||||
--node-component-surface: var(--color-charcoal-800);
|
||||
--node-component-tooltip: var(--color-white);
|
||||
--node-component-tooltip-border: var(--color-slate-300);
|
||||
--node-component-tooltip-surface: var(--color-charcoal-800);
|
||||
@@ -351,17 +339,6 @@
|
||||
--border-subtle: var(--color-charcoal-300);
|
||||
--muted-background: var(--color-charcoal-100);
|
||||
--accent-background: var(--color-charcoal-100);
|
||||
|
||||
/* Component/Node tokens from design dark system */
|
||||
--component-node-background: var(--color-charcoal-600);
|
||||
--component-node-border: var(--color-charcoal-100);
|
||||
--component-node-foreground: var(--base-foreground);
|
||||
--component-node-foreground-secondary: var(--color-muted-foreground);
|
||||
--component-node-widget-background: var(--secondary-background-hover);
|
||||
--component-node-widget-background-hovered: var(--secondary-background-selected);
|
||||
--component-node-widget-background-selected: var(--color-charcoal-100);
|
||||
--component-node-widget-background-disabled: var(--color-alpha-charcoal-600-30);
|
||||
--component-node-widget-background-highlighted: var(--color-graphite-400);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -384,14 +361,6 @@
|
||||
--interface-menu-keybind-surface-default
|
||||
);
|
||||
--color-interface-panel-surface: var(--interface-panel-surface);
|
||||
--color-interface-panel-hover-surface: var(--interface-panel-hover-surface);
|
||||
--color-interface-panel-selected-surface: var(
|
||||
--interface-panel-selected-surface
|
||||
);
|
||||
--color-interface-button-hover-surface: var(
|
||||
--interface-button-hover-surface
|
||||
);
|
||||
--color-comfy-menu-bg: var(--comfy-menu-bg);
|
||||
--color-interface-stroke: var(--interface-stroke);
|
||||
--color-nav-background: var(--nav-background);
|
||||
--color-node-border: var(--node-border);
|
||||
@@ -437,17 +406,6 @@
|
||||
--color-text-primary: var(--text-primary);
|
||||
--color-input-surface: var(--input-surface);
|
||||
|
||||
/* Component/Node design tokens */
|
||||
--color-component-node-background: var(--component-node-background);
|
||||
--color-component-node-border: var(--component-node-border);
|
||||
--color-component-node-foreground: var(--component-node-foreground);
|
||||
--color-component-node-foreground-secondary: var(--component-node-foreground-secondary);
|
||||
--color-component-node-widget-background: var(--component-node-widget-background);
|
||||
--color-component-node-widget-background-hovered: var(--component-node-widget-background-hovered);
|
||||
--color-component-node-widget-background-selected: var(--component-node-widget-background-selected);
|
||||
--color-component-node-widget-background-disabled: var(--component-node-widget-background-disabled);
|
||||
--color-component-node-widget-background-highlighted: var(--component-node-widget-background-highlighted);
|
||||
|
||||
/* Semantic tokens */
|
||||
--color-base-foreground: var(--base-foreground);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB |
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"litegraph_base": {
|
||||
"BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=",
|
||||
"CLEAR_BACKGROUND_COLOR": "#141414",
|
||||
"CLEAR_BACKGROUND_COLOR": "#222",
|
||||
"NODE_TITLE_COLOR": "#999",
|
||||
"NODE_SELECTED_TITLE_COLOR": "#FFF",
|
||||
"NODE_TEXT_SIZE": 14,
|
||||
@@ -52,7 +52,7 @@
|
||||
"comfy_base": {
|
||||
"fg-color": "#fff",
|
||||
"bg-color": "#202020",
|
||||
"comfy-menu-bg": "#171718",
|
||||
"comfy-menu-bg": "#11141a",
|
||||
"comfy-menu-secondary-bg": "#303030",
|
||||
"comfy-input-bg": "#222",
|
||||
"input-text": "#ddd",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div v-if="!workspaceStore.focusMode" class="ml-1 flex gap-x-0.5 pt-1">
|
||||
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-1">
|
||||
<div class="min-w-0 flex-1">
|
||||
<SubgraphBreadcrumb />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-[var(--interface-stroke)] px-2 shadow-interface"
|
||||
class="actionbar-container pointer-events-auto mx-1 flex h-12 items-center rounded-lg border border-[var(--interface-stroke)] px-2 shadow-interface"
|
||||
>
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Avatar
|
||||
class="bg-interface-panel-selected-surface"
|
||||
class="bg-gray-200 dark-theme:bg-[var(--interface-panel-selected-surface)]"
|
||||
:image="photoUrl ?? undefined"
|
||||
:icon="hasAvatar ? undefined : 'icon-[lucide--user]'"
|
||||
:pt:icon:class="{ 'size-4': !hasAvatar }"
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<Button
|
||||
ref="buttonRef"
|
||||
severity="secondary"
|
||||
class="group h-8 rounded-none! bg-comfy-menu-bg p-0 transition-none! hover:rounded-lg! hover:bg-interface-button-hover-surface!"
|
||||
class="group h-8 rounded-none! bg-interface-panel-surface p-0 transition-none! hover:rounded-lg! hover:bg-button-hover-surface!"
|
||||
:style="buttonStyles"
|
||||
@click="toggle"
|
||||
>
|
||||
<template #default>
|
||||
<div class="flex items-center gap-1 pr-0.5">
|
||||
<div
|
||||
class="rounded-lg bg-interface-panel-selected-surface p-2 group-hover:bg-interface-button-hover-surface"
|
||||
class="rounded-lg bg-button-active-surface p-2 group-hover:bg-button-hover-surface"
|
||||
>
|
||||
<i :class="currentModeIcon" class="block h-4 w-4" />
|
||||
</div>
|
||||
@@ -114,7 +114,7 @@ const popoverPt = computed(() => ({
|
||||
content: {
|
||||
class: [
|
||||
'mb-2 text-text-primary',
|
||||
'shadow-lg border border-interface-stroke',
|
||||
'shadow-lg border border-node-border',
|
||||
'bg-nav-background',
|
||||
'rounded-lg',
|
||||
'p-2 px-3',
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
></div>
|
||||
|
||||
<ButtonGroup
|
||||
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-interface-stroke bg-comfy-menu-bg p-2"
|
||||
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-[var(--interface-stroke)] bg-interface-panel-surface p-2"
|
||||
:style="{
|
||||
...stringifiedMinimapStyles.buttonGroupStyles
|
||||
}"
|
||||
@@ -28,7 +28,7 @@
|
||||
icon="pi pi-expand"
|
||||
:aria-label="fitViewTooltip"
|
||||
:style="stringifiedMinimapStyles.buttonStyles"
|
||||
class="h-8 w-8 bg-comfy-menu-bg p-0 hover:bg-interface-button-hover-surface!"
|
||||
class="h-8 w-8 bg-interface-panel-surface p-0 hover:bg-button-hover-surface!"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -166,18 +166,18 @@ const minimapCommandText = computed(() =>
|
||||
|
||||
// Computed properties for button classes and states
|
||||
const zoomButtonClass = computed(() => [
|
||||
'bg-comfy-menu-bg',
|
||||
isModalVisible.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
|
||||
'hover:bg-interface-button-hover-surface!',
|
||||
'bg-interface-panel-surface',
|
||||
isModalVisible.value ? 'not-active:bg-button-active-surface!' : '',
|
||||
'hover:bg-button-hover-surface!',
|
||||
'p-0',
|
||||
'h-8',
|
||||
'w-15'
|
||||
])
|
||||
|
||||
const minimapButtonClass = computed(() => ({
|
||||
'bg-comfy-menu-bg': true,
|
||||
'hover:bg-interface-button-hover-surface!': true,
|
||||
'not-active:bg-interface-panel-selected-surface!': settingStore.get(
|
||||
'bg-interface-panel-surface': true,
|
||||
'hover:bg-button-hover-surface!': true,
|
||||
'not-active:bg-button-active-surface!': settingStore.get(
|
||||
'Comfy.Minimap.Visible'
|
||||
),
|
||||
'p-0': true,
|
||||
@@ -209,9 +209,9 @@ const linkVisibilityAriaLabel = computed(() =>
|
||||
: t('graphCanvasMenu.hideLinks')
|
||||
)
|
||||
const linkVisibleClass = computed(() => [
|
||||
'bg-comfy-menu-bg',
|
||||
linkHidden.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
|
||||
'hover:bg-interface-button-hover-surface!',
|
||||
'bg-interface-panel-surface',
|
||||
linkHidden.value ? 'not-active:bg-button-active-surface!' : '',
|
||||
'hover:bg-button-hover-surface!',
|
||||
'p-0',
|
||||
'w-8',
|
||||
'h-8'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
class="absolute right-0 bottom-[62px] z-1300 flex w-[250px] justify-center border-0! bg-inherit!"
|
||||
>
|
||||
<div
|
||||
class="w-4/5 rounded-lg border border-interface-stroke bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none"
|
||||
class="w-4/5 rounded-lg border border-node-border bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none"
|
||||
:style="filteredMinimapStyles"
|
||||
@click.stop
|
||||
>
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
|
||||
import { showSubgraphNodeDialog } from '@/workbench/graph/subgraph/useSubgraphNodeDialog'
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@click="popover?.toggle($event)"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-full hover:bg-interface-button-hover-surface"
|
||||
class="flex items-center gap-1 rounded-full hover:bg-[var(--interface-button-hover-surface)]"
|
||||
>
|
||||
<UserAvatar :photo-url="photoURL" />
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
outlined
|
||||
rounded
|
||||
severity="secondary"
|
||||
class="size-8 border-black/50 bg-transparent text-black hover:bg-interface-panel-hover-surface dark-theme:border-white/50 dark-theme:text-white"
|
||||
class="size-8 border-black/50 bg-transparent text-black hover:bg-[var(--interface-panel-hover-surface)] dark-theme:border-white/50 dark-theme:text-white"
|
||||
@click="handleSignIn()"
|
||||
@mouseenter="showPopover"
|
||||
@mouseleave="hidePopover"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FirebaseError } from 'firebase/app'
|
||||
import { AuthErrorCodes } from 'firebase/auth'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type { ErrorRecoveryStrategy } from '@/composables/useErrorHandling'
|
||||
@@ -60,7 +61,8 @@ export const useFirebaseAuthActions = () => {
|
||||
|
||||
if (isCloud) {
|
||||
try {
|
||||
window.location.href = '/cloud/login'
|
||||
const router = useRouter()
|
||||
await router.push({ name: 'cloud-login' })
|
||||
} catch (error) {
|
||||
// needed for local development until we bring in cloud login pages.
|
||||
window.location.reload()
|
||||
|
||||
@@ -269,13 +269,10 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
const updatedWidgets = currentData.widgets.map((w) =>
|
||||
w.name === widgetName ? { ...w, value: validateWidgetValue(value) } : w
|
||||
)
|
||||
// Create a completely new object to ensure Vue reactivity triggers
|
||||
const updatedData = {
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
widgets: updatedWidgets
|
||||
}
|
||||
|
||||
vueNodeData.set(nodeId, updatedData)
|
||||
})
|
||||
} catch (error) {
|
||||
// Ignore widget update errors to prevent cascade failures
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@
|
||||
* Composable for managing widget value synchronization between Vue and LiteGraph
|
||||
* Provides consistent pattern for immediate UI updates and LiteGraph callbacks
|
||||
*/
|
||||
import { computed, toValue, ref, watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||
import type { MaybeRefOrGetter } from '@vueuse/core'
|
||||
|
||||
interface UseWidgetValueOptions<T extends WidgetValue = WidgetValue, U = T> {
|
||||
/** The widget configuration from LiteGraph */
|
||||
widget: SimplifiedWidget<T>
|
||||
/** The current value from parent component (can be a value or a getter function) */
|
||||
modelValue: MaybeRefOrGetter<T>
|
||||
/** The current value from parent component */
|
||||
modelValue: T
|
||||
/** Default value if modelValue is null/undefined */
|
||||
defaultValue: T
|
||||
/** Emit function from component setup */
|
||||
@@ -47,21 +46,8 @@ export function useWidgetValue<T extends WidgetValue = WidgetValue, U = T>({
|
||||
emit,
|
||||
transform
|
||||
}: UseWidgetValueOptions<T, U>): UseWidgetValueReturn<T, U> {
|
||||
// Ref for immediate UI feedback before value flows back through modelValue
|
||||
const newProcessedValue = ref<T | null>(null)
|
||||
|
||||
// Computed that prefers the immediately processed value, then falls back to modelValue
|
||||
const localValue = computed<T>(
|
||||
() => newProcessedValue.value ?? toValue(modelValue) ?? defaultValue
|
||||
)
|
||||
|
||||
// Clear newProcessedValue when modelValue updates (allowing external changes to flow through)
|
||||
watch(
|
||||
() => toValue(modelValue),
|
||||
() => {
|
||||
newProcessedValue.value = null
|
||||
}
|
||||
)
|
||||
// Local value for immediate UI updates
|
||||
const localValue = ref<T>(modelValue ?? defaultValue)
|
||||
|
||||
// Handle user changes
|
||||
const onChange = (newValue: U) => {
|
||||
@@ -85,13 +71,21 @@ export function useWidgetValue<T extends WidgetValue = WidgetValue, U = T>({
|
||||
}
|
||||
}
|
||||
|
||||
// Set for immediate UI feedback
|
||||
newProcessedValue.value = processedValue
|
||||
// 1. Update local state for immediate UI feedback
|
||||
localValue.value = processedValue
|
||||
|
||||
// Emit to parent component
|
||||
// 2. Emit to parent component
|
||||
emit('update:modelValue', processedValue)
|
||||
}
|
||||
|
||||
// Watch for external updates from LiteGraph
|
||||
watch(
|
||||
() => modelValue,
|
||||
(newValue) => {
|
||||
localValue.value = newValue ?? defaultValue
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
localValue: localValue as Ref<T>,
|
||||
onChange
|
||||
@@ -103,7 +97,7 @@ export function useWidgetValue<T extends WidgetValue = WidgetValue, U = T>({
|
||||
*/
|
||||
export function useStringWidgetValue(
|
||||
widget: SimplifiedWidget<string>,
|
||||
modelValue: string | (() => string),
|
||||
modelValue: string,
|
||||
emit: (event: 'update:modelValue', value: string) => void
|
||||
) {
|
||||
return useWidgetValue({
|
||||
@@ -120,7 +114,7 @@ export function useStringWidgetValue(
|
||||
*/
|
||||
export function useNumberWidgetValue(
|
||||
widget: SimplifiedWidget<number>,
|
||||
modelValue: number | (() => number),
|
||||
modelValue: number,
|
||||
emit: (event: 'update:modelValue', value: number) => void
|
||||
) {
|
||||
return useWidgetValue({
|
||||
@@ -143,7 +137,7 @@ export function useNumberWidgetValue(
|
||||
*/
|
||||
export function useBooleanWidgetValue(
|
||||
widget: SimplifiedWidget<boolean>,
|
||||
modelValue: boolean | (() => boolean),
|
||||
modelValue: boolean,
|
||||
emit: (event: 'update:modelValue', value: boolean) => void
|
||||
) {
|
||||
return useWidgetValue({
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
} from '@/constants/coreColorPalettes'
|
||||
import { tryToggleWidgetPromotion } from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
|
||||
import { showSubgraphNodeDialog } from '@/workbench/graph/subgraph/useSubgraphNodeDialog'
|
||||
import { t } from '@/i18n'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
@@ -37,6 +36,7 @@ import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { invokeToggleWidgetPromotion } from '@/services/widgetPromotionHandlers'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
@@ -1027,7 +1027,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
icon: 'icon-[lucide--arrow-left-right]',
|
||||
label: 'Toggle promotion of hovered widget',
|
||||
versionAdded: '1.30.1',
|
||||
function: tryToggleWidgetPromotion
|
||||
function: () => invokeToggleWidgetPromotion()
|
||||
},
|
||||
{
|
||||
id: 'Comfy.OpenManagerDialog',
|
||||
|
||||
@@ -58,9 +58,6 @@ async function uploadFile(
|
||||
getResourceURL(...splitFilePath(path))
|
||||
)
|
||||
audioWidget.value = path
|
||||
|
||||
// Manually trigger the callback to update VueNodes
|
||||
audioWidget.callback?.(path)
|
||||
}
|
||||
} else {
|
||||
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
|
||||
|
||||
@@ -29,8 +29,6 @@ export interface IWidgetOptions<TValues = unknown[]> {
|
||||
canvasOnly?: boolean
|
||||
|
||||
values?: TValues
|
||||
/** Optional function to format values for display (e.g., hash → human-readable name) */
|
||||
getOptionLabel?: (value?: string | null) => string
|
||||
callback?: IWidget['callback']
|
||||
}
|
||||
|
||||
|
||||
@@ -34,18 +34,6 @@ export class ComboWidget
|
||||
|
||||
override get _displayValue() {
|
||||
if (this.computedDisabled) return ''
|
||||
|
||||
if (this.options.getOptionLabel) {
|
||||
try {
|
||||
return this.options.getOptionLabel(
|
||||
this.value ? String(this.value) : null
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('Failed to map value:', e)
|
||||
return this.value ? String(this.value) : ''
|
||||
}
|
||||
}
|
||||
|
||||
const { values: rawValues } = this.options
|
||||
if (rawValues) {
|
||||
const values = typeof rawValues === 'function' ? rawValues() : rawValues
|
||||
@@ -143,31 +131,7 @@ export class ComboWidget
|
||||
const values = this.getValues(node)
|
||||
const values_list = toArray(values)
|
||||
|
||||
// Use addItem to solve duplicate filename issues
|
||||
if (this.options.getOptionLabel) {
|
||||
const menuOptions = {
|
||||
scale: Math.max(1, canvas.ds.scale),
|
||||
event: e,
|
||||
className: 'dark',
|
||||
callback: (value: string) => {
|
||||
this.setValue(value, { e, node, canvas })
|
||||
}
|
||||
}
|
||||
const menu = new LiteGraph.ContextMenu([], menuOptions)
|
||||
|
||||
for (const value of values_list) {
|
||||
try {
|
||||
const label = this.options.getOptionLabel(String(value))
|
||||
menu.addItem(label, value, menuOptions)
|
||||
} catch (err) {
|
||||
console.error('Failed to map value:', err)
|
||||
menu.addItem(String(value), value, menuOptions)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Show dropdown menu when user clicks on widget label
|
||||
// Handle center click - show dropdown menu
|
||||
const text_values = values != values_list ? Object.values(values) : values
|
||||
new LiteGraph.ContextMenu(text_values, {
|
||||
scale: Math.max(1, canvas.ds.scale),
|
||||
|
||||
@@ -180,10 +180,6 @@
|
||||
"title": "Title",
|
||||
"edit": "Edit",
|
||||
"copy": "Copy",
|
||||
"copyJobId": "Copy Job ID",
|
||||
"copied": "Copied",
|
||||
"jobIdCopied": "Job ID copied to clipboard",
|
||||
"failedToCopyJobId": "Failed to copy job ID",
|
||||
"imageUrl": "Image URL",
|
||||
"clear": "Clear",
|
||||
"clearAll": "Clear all",
|
||||
@@ -611,8 +607,17 @@
|
||||
"nodes": "Nodes",
|
||||
"models": "Models",
|
||||
"workflows": "Workflows",
|
||||
"templates": "Templates"
|
||||
"templates": "Templates",
|
||||
"console": "Console",
|
||||
"menu": "Menu",
|
||||
"assets": "Assets",
|
||||
"imported": "Imported",
|
||||
"generated": "Generated"
|
||||
},
|
||||
"noFilesFound": "No files found",
|
||||
"noImportedFiles": "No imported files found",
|
||||
"noGeneratedFiles": "No generated files found",
|
||||
"noFilesFoundMessage": "Upload files or generate content to see them here",
|
||||
"browseTemplates": "Browse example templates",
|
||||
"openWorkflow": "Open workflow in local file system",
|
||||
"newBlankWorkflow": "Create a new blank workflow",
|
||||
@@ -754,310 +759,6 @@
|
||||
"Partner Nodes": "Partner Nodes",
|
||||
"Generation Type": "Generation Type"
|
||||
},
|
||||
"templateDescription": {
|
||||
"Basics": {
|
||||
"default": "Generate images from text prompts.",
|
||||
"image2image": "Transform existing images using text prompts.",
|
||||
"lora": "Generate images with LoRA models for specialized styles or subjects.",
|
||||
"lora_multiple": "Generate images by combining multiple LoRA models.",
|
||||
"inpaint_example": "Edit specific parts of images seamlessly.",
|
||||
"inpaint_model_outpainting": "Extend images beyond their original boundaries.",
|
||||
"embedding_example": "Generate images using textual inversion for consistent styles.",
|
||||
"gligen_textbox_example": "Generate images with precise object placement using text boxes."
|
||||
},
|
||||
"Flux": {
|
||||
"flux_kontext_dev_basic": "Edit image using Flux Kontext with full node visibility, perfect for learning the workflow.",
|
||||
"flux_kontext_dev_grouped": "Streamlined version of Flux Kontext with grouped nodes for cleaner workspace.",
|
||||
"flux_dev_checkpoint_example": "Generate images using Flux Dev fp8 quantized version. Suitable for devices with limited VRAM, requires only one model file, but image quality is slightly lower than the full version.",
|
||||
"flux_schnell": "Quickly generate images with Flux Schnell fp8 quantized version. Ideal for low-end hardware, requires only 4 steps to generate images.",
|
||||
"flux_dev_full_text_to_image": "Generate high-quality images with Flux Dev full version. Requires larger VRAM and multiple model files, but provides the best prompt following capability and image quality.",
|
||||
"flux_schnell_full_text_to_image": "Generate images quickly with Flux Schnell full version. Uses Apache2.0 license, requires only 4 steps to generate images while maintaining good image quality.",
|
||||
"flux_fill_inpaint_example": "Fill missing parts of images using Flux inpainting.",
|
||||
"flux_fill_outpaint_example": "Extend images beyond boundaries using Flux outpainting.",
|
||||
"flux_canny_model_example": "Generate images guided by edge detection using Flux Canny.",
|
||||
"flux_depth_lora_example": "Generate images guided by depth information using Flux LoRA.",
|
||||
"flux_redux_model_example": "Generate images by transferring style from reference images using Flux Redux."
|
||||
},
|
||||
"Image": {
|
||||
"image_omnigen2_t2i": "Generate high-quality images from text prompts using OmniGen2's unified 7B multimodal model with dual-path architecture.",
|
||||
"image_omnigen2_image_edit": "Edit images with natural language instructions using OmniGen2's advanced image editing capabilities and text rendering support.",
|
||||
"image_cosmos_predict2_2B_t2i": "Generate images with Cosmos-Predict2 2B T2I, delivering physically accurate, high-fidelity, and detail-rich image generation.",
|
||||
"image_chroma_text_to_image": "Chroma is modified from flux and has some changes in the architecture.",
|
||||
"hidream_i1_dev": "Generate images with HiDream I1 Dev - Balanced version with 28 inference steps, suitable for medium-range hardware.",
|
||||
"hidream_i1_fast": "Generate images quickly with HiDream I1 Fast - Lightweight version with 16 inference steps, ideal for rapid previews on lower-end hardware.",
|
||||
"hidream_i1_full": "Generate images with HiDream I1 Full - Complete version with 50 inference steps for highest quality output.",
|
||||
"hidream_e1_full": "Edit images with HiDream E1 - Professional natural language image editing model.",
|
||||
"sd3_5_simple_example": "Generate images using SD 3.5.",
|
||||
"sd3_5_large_canny_controlnet_example": "Generate images guided by edge detection using SD 3.5 Canny ControlNet.",
|
||||
"sd3_5_large_depth": "Generate images guided by depth information using SD 3.5.",
|
||||
"sd3_5_large_blur": "Generate images guided by blurred reference images using SD 3.5.",
|
||||
"sdxl_simple_example": "Generate high-quality images using SDXL.",
|
||||
"sdxl_refiner_prompt_example": "Enhance SDXL images using refiner models.",
|
||||
"sdxl_revision_text_prompts": "Generate images by transferring concepts from reference images using SDXL Revision.",
|
||||
"sdxl_revision_zero_positive": "Generate images using both text prompts and reference images with SDXL Revision.",
|
||||
"sdxlturbo_example": "Generate images in a single step using SDXL Turbo.",
|
||||
"image_lotus_depth_v1_1": "Run Lotus Depth in ComfyUI for zero-shot, efficient monocular depth estimation with high detail retention."
|
||||
},
|
||||
"Video": {
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Generate videos with Cosmos-Predict2 2B Video2World, generating physically accurate, high-fidelity, and consistent video simulations.",
|
||||
"video_wan_vace_14B_t2v": "Transform text descriptions into high-quality videos. Supports both 480p and 720p with VACE-14B model.",
|
||||
"video_wan_vace_14B_ref2v": "Create videos that match the style and content of a reference image. Perfect for style-consistent video generation.",
|
||||
"video_wan_vace_14B_v2v": "Generate videos by controlling input videos and reference images using Wan VACE.",
|
||||
"video_wan_vace_outpainting": "Generate extended videos by expanding video size using Wan VACE outpainting.",
|
||||
"video_wan_vace_flf2v": "Generate smooth video transitions by defining start and end frames. Supports custom keyframe sequences.",
|
||||
"video_wan_vace_inpainting": "Edit specific regions in videos while preserving surrounding content. Great for object removal or replacement.",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Generate dynamic videos with cinematic camera movements using Wan 2.1 Fun Camera 1.3B model.",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Generate high-quality videos with advanced camera control using the full 14B model",
|
||||
"text_to_video_wan": "Generate videos from text prompts using Wan 2.1.",
|
||||
"image_to_video_wan": "Generate videos from images using Wan 2.1.",
|
||||
"wan2_1_fun_inp": "Generate videos from start and end frames using Wan 2.1 inpainting.",
|
||||
"wan2_1_fun_control": "Generate videos guided by pose, depth, and edge controls using Wan 2.1 ControlNet.",
|
||||
"wan2_1_flf2v_720_f16": "Generate videos by controlling first and last frames using Wan 2.1 FLF2V.",
|
||||
"ltxv_text_to_video": "Generate videos from text prompts.",
|
||||
"ltxv_image_to_video": "Generate videos from still images.",
|
||||
"mochi_text_to_video_example": "Generate videos from text prompts using Mochi model.",
|
||||
"hunyuan_video_text_to_video": "Generate videos from text prompts using Hunyuan model.",
|
||||
"image_to_video": "Generate videos from still images.",
|
||||
"txt_to_image_to_video": "Generate videos by first creating images from text prompts."
|
||||
},
|
||||
"Image API": {
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "Input multiple images and edit them with Flux.1 Kontext.",
|
||||
"api_bfl_flux_1_kontext_pro_image": "Edit images with Flux.1 Kontext pro image.",
|
||||
"api_bfl_flux_1_kontext_max_image": "Edit images with Flux.1 Kontext max image.",
|
||||
"api_bfl_flux_pro_t2i": "Generate images with excellent prompt following and visual quality using FLUX.1 Pro.",
|
||||
"api_luma_photon_i2i": "Guide image generation using a combination of images and prompt.",
|
||||
"api_luma_photon_style_ref": "Generate images by blending style references with precise control using Luma Photon.",
|
||||
"api_recraft_image_gen_with_color_control": "Generate images with custom color palettes and brand-specific visuals using Recraft.",
|
||||
"api_recraft_image_gen_with_style_control": "Control style with visual examples, align positioning, and fine-tune objects. Store and share styles for perfect brand consistency.",
|
||||
"api_recraft_vector_gen": "Generate high-quality vector images from text prompts using Recraft's AI vector generator.",
|
||||
"api_runway_text_to_image": "Generate high-quality images from text prompts using Runway's AI model.",
|
||||
"api_runway_reference_to_image": "Generate new images based on reference styles and compositions with Runway's AI.",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Generate high quality images with excellent prompt adherence. Perfect for professional use cases at 1 megapixel resolution.",
|
||||
"api_stability_ai_i2i": "Transform images with high-quality generation using Stability AI, perfect for professional editing and style transfer.",
|
||||
"api_stability_ai_sd3_5_t2i": "Generate high quality images with excellent prompt adherence. Perfect for professional use cases at 1 megapixel resolution.",
|
||||
"api_stability_ai_sd3_5_i2i": "Generate high quality images with excellent prompt adherence. Perfect for professional use cases at 1 megapixel resolution.",
|
||||
"api_ideogram_v3_t2i": "Generate professional-quality images with excellent prompt alignment, photorealism, and text rendering using Ideogram V3.",
|
||||
"api_openai_image_1_t2i": "Generate images from text prompts using OpenAI GPT Image 1 API.",
|
||||
"api_openai_image_1_i2i": "Generate images from input images using OpenAI GPT Image 1 API.",
|
||||
"api_openai_image_1_inpaint": "Edit images using inpainting with OpenAI GPT Image 1 API.",
|
||||
"api_openai_image_1_multi_inputs": "Generate images from multiple inputs using OpenAI GPT Image 1 API.",
|
||||
"api_openai_dall_e_2_t2i": "Generate images from text prompts using OpenAI Dall-E 2 API.",
|
||||
"api_openai_dall_e_2_inpaint": "Edit images using inpainting with OpenAI Dall-E 2 API.",
|
||||
"api_openai_dall_e_3_t2i": "Generate images from text prompts using OpenAI Dall-E 3 API."
|
||||
},
|
||||
"Video API": {
|
||||
"api_moonvalley_text_to_video": "Generate cinematic, 1080p videos from text prompts through a model trained exclusively on licensed data.",
|
||||
"api_moonvalley_image_to_video": "Generate cinematic, 1080p videos with an image through a model trained exclusively on licensed data.",
|
||||
"api_kling_i2v": "Generate videos with excellent prompt adherence for actions, expressions, and camera movements using Kling.",
|
||||
"api_kling_effects": "Generate dynamic videos by applying visual effects to images using Kling.",
|
||||
"api_kling_flf": "Generate videos through controlling the first and last frames.",
|
||||
"api_luma_i2v": "Take static images and instantly create magical high quality animations.",
|
||||
"api_luma_t2v": "High-quality videos can be generated using simple prompts.",
|
||||
"api_hailuo_minimax_t2v": "Generate high-quality videos directly from text prompts. Explore MiniMax's advanced AI capabilities to create diverse visual narratives with professional CGI effects and stylistic elements to bring your descriptions to life.",
|
||||
"api_hailuo_minimax_i2v": "Generate refined videos from images and text with CGI integration using MiniMax.",
|
||||
"api_pixverse_i2v": "Generate dynamic videos from static images with motion and effects using PixVerse.",
|
||||
"api_pixverse_template_i2v": "Generate dynamic videos from static images with motion and effects using PixVerse.",
|
||||
"api_pixverse_t2v": "Generate videos with accurate prompt interpretation and stunning video dynamics.",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Generate cinematic videos from static images using Runway Gen3a Turbo.",
|
||||
"api_runway_gen4_turo_image_to_video": "Generate dynamic videos from images using Runway Gen4 Turbo.",
|
||||
"api_runway_first_last_frame": "Generate smooth video transitions between two keyframes with Runway's precision.",
|
||||
"api_pika_i2v": "Generate smooth animated videos from single static images using Pika AI.",
|
||||
"api_pika_scene": "Generate videos that incorporate multiple input images using Pika Scenes.",
|
||||
"api_veo2_i2v": "Generate videos from images using Google Veo2 API."
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Generate detailed 3D models from single photos using Rodin AI.",
|
||||
"api_rodin_multiview_to_model": "Sculpt comprehensive 3D models using Rodin's multi-angle reconstruction.",
|
||||
"api_tripo_text_to_model": "Craft 3D objects from descriptions with Tripo's text-driven modeling.",
|
||||
"api_tripo_image_to_model": "Generate professional 3D assets from 2D images using Tripo engine.",
|
||||
"api_tripo_multiview_to_model": "Build 3D models from multiple angles with Tripo's advanced scanner."
|
||||
},
|
||||
"LLM API": {
|
||||
"api_openai_chat": "Engage with OpenAI's advanced language models for intelligent conversations.",
|
||||
"api_google_gemini": "Experience Google's multimodal AI with Gemini's reasoning capabilities."
|
||||
},
|
||||
"Upscaling": {
|
||||
"hiresfix_latent_workflow": "Upscale images by enhancing quality in latent space.",
|
||||
"esrgan_example": "Upscale images using ESRGAN models to enhance quality.",
|
||||
"hiresfix_esrgan_workflow": "Upscale images using ESRGAN models during intermediate generation steps.",
|
||||
"latent_upscale_different_prompt_model": "Upscale images while changing prompts across generation passes."
|
||||
},
|
||||
"ControlNet": {
|
||||
"controlnet_example": "Generate images guided by scribble reference images using ControlNet.",
|
||||
"2_pass_pose_worship": "Generate images guided by pose references using ControlNet.",
|
||||
"depth_controlnet": "Generate images guided by depth information using ControlNet.",
|
||||
"depth_t2i_adapter": "Generate images guided by depth information using T2I adapter.",
|
||||
"mixing_controlnets": "Generate images by combining multiple ControlNet models."
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Generate images by controlling composition with defined areas.",
|
||||
"area_composition_square_area_for_subject": "Generate images with consistent subject placement using area composition."
|
||||
},
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Generate 3D models from single images using Hunyuan3D 2.0.",
|
||||
"3d_hunyuan3d_multiview_to_model": "Generate 3D models from multiple views using Hunyuan3D 2.0 MV.",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Generate 3D models from multiple views using Hunyuan3D 2.0 MV Turbo.",
|
||||
"stable_zero123_example": "Generate 3D views from single images using Stable Zero123."
|
||||
},
|
||||
"Audio": {
|
||||
"audio_stable_audio_example": "Generate audio from text prompts using Stable Audio.",
|
||||
"audio_ace_step_1_t2a_instrumentals": "Generate instrumental music from text prompts using ACE-Step v1.",
|
||||
"audio_ace_step_1_t2a_song": "Generate songs with vocals from text prompts using ACE-Step v1, supporting multilingual and style customization.",
|
||||
"audio_ace_step_1_m2m_editing": "Edit existing songs to change style and lyrics using ACE-Step v1 M2M."
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"Basics": {
|
||||
"default": "Image Generation",
|
||||
"image2image": "Image to Image",
|
||||
"lora": "LoRA",
|
||||
"lora_multiple": "LoRA Multiple",
|
||||
"inpaint_example": "Inpaint",
|
||||
"inpaint_model_outpainting": "Outpaint",
|
||||
"embedding_example": "Embedding",
|
||||
"gligen_textbox_example": "Gligen Textbox"
|
||||
},
|
||||
"Flux": {
|
||||
"flux_kontext_dev_basic": "Flux Kontext Dev(Basic)",
|
||||
"flux_kontext_dev_grouped": "Flux Kontext Dev(Grouped)",
|
||||
"flux_dev_checkpoint_example": "Flux Dev fp8",
|
||||
"flux_schnell": "Flux Schnell fp8",
|
||||
"flux_dev_full_text_to_image": "Flux Dev full text to image",
|
||||
"flux_schnell_full_text_to_image": "Flux Schnell full text to image",
|
||||
"flux_fill_inpaint_example": "Flux Inpaint",
|
||||
"flux_fill_outpaint_example": "Flux Outpaint",
|
||||
"flux_canny_model_example": "Flux Canny Model",
|
||||
"flux_depth_lora_example": "Flux Depth LoRA",
|
||||
"flux_redux_model_example": "Flux Redux Model"
|
||||
},
|
||||
"Image": {
|
||||
"image_omnigen2_t2i": "OmniGen2 Text to Image",
|
||||
"image_omnigen2_image_edit": "OmniGen2 Image Edit",
|
||||
"image_cosmos_predict2_2B_t2i": "Cosmos Predict2 2B T2I",
|
||||
"image_chroma_text_to_image": "Chroma text to image",
|
||||
"hidream_i1_dev": "HiDream I1 Dev",
|
||||
"hidream_i1_fast": "HiDream I1 Fast",
|
||||
"hidream_i1_full": "HiDream I1 Full",
|
||||
"hidream_e1_full": "HiDream E1 Full",
|
||||
"sd3_5_simple_example": "SD3.5 Simple",
|
||||
"sd3_5_large_canny_controlnet_example": "SD3.5 Large Canny ControlNet",
|
||||
"sd3_5_large_depth": "SD3.5 Large Depth",
|
||||
"sd3_5_large_blur": "SD3.5 Large Blur",
|
||||
"sdxl_simple_example": "SDXL Simple",
|
||||
"sdxl_refiner_prompt_example": "SDXL Refiner Prompt",
|
||||
"sdxl_revision_text_prompts": "SDXL Revision Text Prompts",
|
||||
"sdxl_revision_zero_positive": "SDXL Revision Zero Positive",
|
||||
"sdxlturbo_example": "SDXL Turbo",
|
||||
"image_lotus_depth_v1_1": "Lotus Depth"
|
||||
},
|
||||
"Video": {
|
||||
"video_cosmos_predict2_2B_video2world_480p_16fps": "Cosmos Predict2 2B Video2World 480p 16fps",
|
||||
"video_wan_vace_14B_t2v": "Wan VACE Text to Video",
|
||||
"video_wan_vace_14B_ref2v": "Wan VACE Reference to Video",
|
||||
"video_wan_vace_14B_v2v": "Wan VACE Control Video",
|
||||
"video_wan_vace_outpainting": "Wan VACE Outpainting",
|
||||
"video_wan_vace_flf2v": "Wan VACE First-Last Frame",
|
||||
"video_wan_vace_inpainting": "Wan VACE Inpainting",
|
||||
"video_wan2_1_fun_camera_v1_1_1_3B": "Wan 2.1 Fun Camera 1.3B",
|
||||
"video_wan2_1_fun_camera_v1_1_14B": "Wan 2.1 Fun Camera 14B",
|
||||
"text_to_video_wan": "Wan 2.1 Text to Video",
|
||||
"image_to_video_wan": "Wan 2.1 Image to Video",
|
||||
"wan2_1_fun_inp": "Wan 2.1 Inpainting",
|
||||
"wan2_1_fun_control": "Wan 2.1 ControlNet",
|
||||
"wan2_1_flf2v_720_f16": "Wan 2.1 FLF2V 720p F16",
|
||||
"ltxv_text_to_video": "LTXV Text to Video",
|
||||
"ltxv_image_to_video": "LTXV Image to Video",
|
||||
"mochi_text_to_video_example": "Mochi Text to Video",
|
||||
"hunyuan_video_text_to_video": "Hunyuan Video Text to Video",
|
||||
"image_to_video": "SVD Image to Video",
|
||||
"txt_to_image_to_video": "SVD Text to Image to Video"
|
||||
},
|
||||
"Image API": {
|
||||
"api_bfl_flux_1_kontext_multiple_images_input": "BFL Flux.1 Kontext Multiple Image Input",
|
||||
"api_bfl_flux_1_kontext_pro_image": "BFL Flux.1 Kontext Pro",
|
||||
"api_bfl_flux_1_kontext_max_image": "BFL Flux.1 Kontext Max",
|
||||
"api_bfl_flux_pro_t2i": "BFL Flux[Pro]: Text to Image",
|
||||
"api_luma_photon_i2i": "Luma Photon: Image to Image",
|
||||
"api_luma_photon_style_ref": "Luma Photon: Style Reference",
|
||||
"api_recraft_image_gen_with_color_control": "Recraft: Color Control Image Generation",
|
||||
"api_recraft_image_gen_with_style_control": "Recraft: Style Control Image Generation",
|
||||
"api_recraft_vector_gen": "Recraft: Vector Generation",
|
||||
"api_runway_text_to_image": "Runway: Text to Image",
|
||||
"api_runway_reference_to_image": "Runway: Reference to Image",
|
||||
"api_stability_ai_stable_image_ultra_t2i": "Stability AI: Stable Image Ultra Text to Image",
|
||||
"api_stability_ai_i2i": "Stability AI: Image to Image",
|
||||
"api_stability_ai_sd3_5_t2i": "Stability AI: SD3.5 Text to Image",
|
||||
"api_stability_ai_sd3_5_i2i": "Stability AI: SD3.5 Image to Image",
|
||||
"api_ideogram_v3_t2i": "Ideogram V3: Text to Image",
|
||||
"api_openai_image_1_t2i": "OpenAI: GPT-Image-1 Text to Image",
|
||||
"api_openai_image_1_i2i": "OpenAI: GPT-Image-1 Image to Image",
|
||||
"api_openai_image_1_inpaint": "OpenAI: GPT-Image-1 Inpaint",
|
||||
"api_openai_image_1_multi_inputs": "OpenAI: GPT-Image-1 Multi Inputs",
|
||||
"api_openai_dall_e_2_t2i": "OpenAI: Dall-E 2 Text to Image",
|
||||
"api_openai_dall_e_2_inpaint": "OpenAI: Dall-E 2 Inpaint",
|
||||
"api_openai_dall_e_3_t2i": "OpenAI: Dall-E 3 Text to Image"
|
||||
},
|
||||
"Video API": {
|
||||
"api_moonvalley_text_to_video": "Moonvalley: Text to Video",
|
||||
"api_moonvalley_image_to_video": "Moonvalley: Image to Video",
|
||||
"api_kling_i2v": "Kling: Image to Video",
|
||||
"api_kling_effects": "Kling: Video Effects",
|
||||
"api_kling_flf": "Kling: FLF2V",
|
||||
"api_luma_i2v": "Luma: Image to Video",
|
||||
"api_luma_t2v": "Luma: Text to Video",
|
||||
"api_hailuo_minimax_t2v": "MiniMax: Text to Video",
|
||||
"api_hailuo_minimax_i2v": "MiniMax: Image to Video",
|
||||
"api_pixverse_i2v": "PixVerse: Image to Video",
|
||||
"api_pixverse_template_i2v": "PixVerse Templates: Image to Video",
|
||||
"api_pixverse_t2v": "PixVerse: Text to Video",
|
||||
"api_runway_gen3a_turbo_image_to_video": "Runway: Gen3a Turbo Image to Video",
|
||||
"api_runway_gen4_turo_image_to_video": "Runway: Gen4 Turbo Image to Video",
|
||||
"api_runway_first_last_frame": "Runway: First Last Frame to Video",
|
||||
"api_pika_i2v": "Pika: Image to Video",
|
||||
"api_pika_scene": "Pika Scenes: Images to Video",
|
||||
"api_veo2_i2v": "Veo2: Image to Video"
|
||||
},
|
||||
"3D API": {
|
||||
"api_rodin_image_to_model": "Rodin: Image to Model",
|
||||
"api_rodin_multiview_to_model": "Rodin: Multiview to Model",
|
||||
"api_tripo_text_to_model": "Tripo: Text to Model",
|
||||
"api_tripo_image_to_model": "Tripo: Image to Model",
|
||||
"api_tripo_multiview_to_model": "Tripo: Multiview to Model"
|
||||
},
|
||||
"LLM API": {
|
||||
"api_openai_chat": "OpenAI: Chat",
|
||||
"api_google_gemini": "Google Gemini: Chat"
|
||||
},
|
||||
"Upscaling": {
|
||||
"hiresfix_latent_workflow": "Upscale",
|
||||
"esrgan_example": "ESRGAN",
|
||||
"hiresfix_esrgan_workflow": "HiresFix ESRGAN Workflow",
|
||||
"latent_upscale_different_prompt_model": "Latent Upscale Different Prompt Model"
|
||||
},
|
||||
"ControlNet": {
|
||||
"controlnet_example": "Scribble ControlNet",
|
||||
"2_pass_pose_worship": "Pose ControlNet 2 Pass",
|
||||
"depth_controlnet": "Depth ControlNet",
|
||||
"depth_t2i_adapter": "Depth T2I Adapter",
|
||||
"mixing_controlnets": "Mixing ControlNets"
|
||||
},
|
||||
"Area Composition": {
|
||||
"area_composition": "Area Composition",
|
||||
"area_composition_square_area_for_subject": "Area Composition Square Area for Subject"
|
||||
},
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "Hunyuan3D 2.0",
|
||||
"3d_hunyuan3d_multiview_to_model": "Hunyuan3D 2.0 MV",
|
||||
"3d_hunyuan3d_multiview_to_model_turbo": "Hunyuan3D 2.0 MV Turbo",
|
||||
"stable_zero123_example": "Stable Zero123"
|
||||
},
|
||||
"Audio": {
|
||||
"audio_stable_audio_example": "Stable Audio",
|
||||
"audio_ace_step_1_t2a_instrumentals": "ACE-Step v1 Text to Instrumentals Music",
|
||||
"audio_ace_step_1_t2a_song": "ACE Step v1 Text to Song",
|
||||
"audio_ace_step_1_m2m_editing": "ACE Step v1 M2M Editing"
|
||||
}
|
||||
},
|
||||
"categories": "Categories",
|
||||
"resetFilters": "Clear Filters",
|
||||
"sorting": "Sort by",
|
||||
@@ -1734,7 +1435,6 @@
|
||||
"camera": "Camera",
|
||||
"light": "Light",
|
||||
"switchingMaterialMode": "Switching Material Mode...",
|
||||
"edgeThreshold": "Edge Threshold",
|
||||
"export": "Export",
|
||||
"exportModel": "Export Model",
|
||||
"exportingModel": "Exporting model...",
|
||||
@@ -1745,8 +1445,7 @@
|
||||
"normal": "Normal",
|
||||
"wireframe": "Wireframe",
|
||||
"original": "Original",
|
||||
"depth": "Depth",
|
||||
"lineart": "Lineart"
|
||||
"depth": "Depth"
|
||||
},
|
||||
"upDirections": {
|
||||
"original": "Original"
|
||||
@@ -1772,7 +1471,10 @@
|
||||
"exportSettings": "Export Settings",
|
||||
"modelSettings": "Model Settings"
|
||||
},
|
||||
"openIn3DViewer": "Open in 3D Viewer"
|
||||
"openIn3DViewer": "Open in 3D Viewer",
|
||||
"dropToLoad": "Drop 3D model to load",
|
||||
"unsupportedFileType": "Unsupported file type (supports .gltf, .glb, .obj, .fbx, .stl)",
|
||||
"uploadingModel": "Uploading 3D model..."
|
||||
},
|
||||
"toastMessages": {
|
||||
"nothingToQueue": "Nothing to queue",
|
||||
@@ -1857,12 +1559,7 @@
|
||||
"confirmPasswordLabel": "Confirm Password",
|
||||
"confirmPasswordPlaceholder": "Enter the same password again",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"passwordResetInstructions": "Enter your email address and we'll send you a link to reset your password.",
|
||||
"sendResetLink": "Send reset link",
|
||||
"backToLogin": "Back to login",
|
||||
"didntReceiveEmail": "Didn't receive an email? Contact us at",
|
||||
"passwordResetError": "Failed to send password reset email. Please try again.",
|
||||
"loginButton": "Sign in",
|
||||
"loginButton": "Log in",
|
||||
"orContinueWith": "Or continue with",
|
||||
"loginWithGoogle": "Log in with Google",
|
||||
"loginWithGithub": "Log in with Github",
|
||||
@@ -1899,20 +1596,6 @@
|
||||
"success": "Password Updated",
|
||||
"successDetail": "Your password has been updated successfully"
|
||||
},
|
||||
"errors": {
|
||||
"auth/invalid-email": "Please enter a valid email address.",
|
||||
"auth/user-disabled": "This account has been disabled. Please contact support.",
|
||||
"auth/user-not-found": "No account found with this email. Would you like to create a new account?",
|
||||
"auth/wrong-password": "The password you entered is incorrect. Please try again.",
|
||||
"auth/email-already-in-use": "An account with this email already exists. Try signing in instead.",
|
||||
"auth/weak-password": "Password is too weak. Please use a stronger password with at least 6 characters.",
|
||||
"auth/too-many-requests": "Too many login attempts. Please wait a moment and try again.",
|
||||
"auth/operation-not-allowed": "This sign-in method is not currently supported.",
|
||||
"auth/invalid-credential": "Invalid login credentials. Please check your email and password.",
|
||||
"auth/network-request-failed": "Network error. Please check your connection and try again.",
|
||||
"auth/popup-closed-by-user": "Sign-in was cancelled. Please try again.",
|
||||
"auth/cancelled-popup-request": "Sign-in was cancelled. Please try again."
|
||||
},
|
||||
"deleteAccount": {
|
||||
"deleteAccount": "Delete Account",
|
||||
"confirmTitle": "Delete Account",
|
||||
@@ -2013,8 +1696,7 @@
|
||||
"waitingForSubscription": "Complete your subscription in the new tab. We'll automatically detect when you're done!",
|
||||
"subscribe": "Subscribe"
|
||||
},
|
||||
"subscribeToRun": "Subscribe",
|
||||
"subscribeToRunFull": "Subscribe to Run",
|
||||
"subscribeToRun": "Subscribe to Run",
|
||||
"subscribeNow": "Subscribe Now",
|
||||
"subscribeToComfyCloud": "Subscribe to Comfy Cloud",
|
||||
"partnerNodesCredits": "Partner Nodes credits"
|
||||
@@ -2095,128 +1777,6 @@
|
||||
"renderBypassState": "Render Bypass State",
|
||||
"renderErrorState": "Render Error State"
|
||||
},
|
||||
"cloudOnboarding": {
|
||||
"survey": {
|
||||
"title": "Cloud Survey",
|
||||
"placeholder": "Survey questions placeholder",
|
||||
"steps": {
|
||||
"familiarity": "How familiar are you with ComfyUI?",
|
||||
"purpose": "What will you primarily use ComfyUI for?",
|
||||
"industry": "What's your primary industry?",
|
||||
"making": "What do you plan on making?"
|
||||
},
|
||||
"questions": {
|
||||
"familiarity": "How familiar are you with ComfyUI?",
|
||||
"purpose": "What will you primarily use ComfyUI for?",
|
||||
"industry": "What's your primary industry?",
|
||||
"making": "What do you plan on making?"
|
||||
},
|
||||
"options": {
|
||||
"familiarity": {
|
||||
"new": "New to ComfyUI (never used it before)",
|
||||
"starting": "Just getting started (following tutorials)",
|
||||
"basics": "Comfortable with basics",
|
||||
"advanced": "Advanced user (custom workflows)",
|
||||
"expert": "Expert (help others)"
|
||||
},
|
||||
"purpose": {
|
||||
"personal": "Personal projects / hobby",
|
||||
"community": "Community contributions (nodes, workflows, etc.)",
|
||||
"client": "Client work (freelance)",
|
||||
"inhouse": "My own workplace (in-house)",
|
||||
"research": "Academic research"
|
||||
},
|
||||
"industry": {
|
||||
"film_tv_animation": "Film, TV, & animation",
|
||||
"gaming": "Gaming",
|
||||
"marketing": "Marketing & advertising",
|
||||
"architecture": "Architecture",
|
||||
"product_design": "Product & graphic design",
|
||||
"fine_art": "Fine art & illustration",
|
||||
"software": "Software & technology",
|
||||
"education": "Education",
|
||||
"other": "Other",
|
||||
"otherPlaceholder": "Please specify"
|
||||
},
|
||||
"making": {
|
||||
"images": "Images",
|
||||
"video": "Video & animation",
|
||||
"3d": "3D assets",
|
||||
"audio": "Audio / music",
|
||||
"custom_nodes": "Custom nodes & workflows"
|
||||
}
|
||||
}
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Forgot Password",
|
||||
"instructions": "Enter your email address and we'll send you a link to reset your password.",
|
||||
"emailLabel": "Email",
|
||||
"emailPlaceholder": "Enter your email",
|
||||
"sendResetLink": "Send reset link",
|
||||
"backToLogin": "Back to login",
|
||||
"didntReceiveEmail": "Didn't receive an email? Contact us at",
|
||||
"passwordResetSent": "Password reset email sent",
|
||||
"passwordResetError": "Failed to send password reset email. Please try again.",
|
||||
"emailRequired": "Email is required"
|
||||
},
|
||||
"privateBeta": {
|
||||
"title": "Cloud is currently in private beta",
|
||||
"desc": "Sign in to join the waitlist. We’ll notify you when it’s your turn. Already been notified? Sign in start using Cloud."
|
||||
},
|
||||
"start": {
|
||||
"title": "start creating in seconds",
|
||||
"desc": "Zero setup required. Works on any device.",
|
||||
"explain": "Generate multiple outputs at once. Share workflows with ease.",
|
||||
"learnAboutButton": "Learn about Cloud",
|
||||
"wantToRun": "Want to run ComfyUI locally instead?",
|
||||
"download": "Download ComfyUI"
|
||||
},
|
||||
"checkingStatus": "Checking your account status...",
|
||||
"retrying": "Retrying...",
|
||||
"retry": "Try Again",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"cloudFooter_needHelp": "Need Help?",
|
||||
"cloudStart_title": "start creating in seconds",
|
||||
"cloudStart_desc": "Zero setup required. Works on any device.",
|
||||
"cloudStart_explain": "Generate multiple outputs at once. Share workflows with ease.",
|
||||
"cloudStart_learnAboutButton": "Learn about Cloud",
|
||||
"cloudStart_wantToRun": "Want to run ComfyUI locally instead?",
|
||||
"cloudStart_download": "Download ComfyUI",
|
||||
"cloudWaitlist_questionsText": "Questions? Contact us",
|
||||
"cloudWaitlist_contactLink": "here",
|
||||
"cloudSorryContactSupport_title": "Sorry, contact support",
|
||||
"cloudPrivateBeta_title": "Cloud is currently in private beta",
|
||||
"cloudPrivateBeta_desc": "Sign in to join the waitlist. We'll notify you when it's your turn. Already been notified? Sign in start using Cloud.",
|
||||
"cloudForgotPassword_title": "Forgot Password",
|
||||
"cloudForgotPassword_instructions": "Enter your email address and we'll send you a link to reset your password.",
|
||||
"cloudForgotPassword_emailLabel": "Email",
|
||||
"cloudForgotPassword_emailPlaceholder": "Enter your email",
|
||||
"cloudForgotPassword_sendResetLink": "Send reset link",
|
||||
"cloudForgotPassword_backToLogin": "Back to login",
|
||||
"cloudForgotPassword_didntReceiveEmail": "Didn't receive an email?",
|
||||
"cloudForgotPassword_emailRequired": "Email is required",
|
||||
"cloudForgotPassword_passwordResetSent": "Password reset sent",
|
||||
"cloudForgotPassword_passwordResetError": "Failed to send password reset email",
|
||||
"cloudSurvey_steps_familiarity": "How familiar are you with ComfyUI?",
|
||||
"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?",
|
||||
"assetBrowser": {
|
||||
"assets": "Assets",
|
||||
"browseAssets": "Browse Assets",
|
||||
|
||||
@@ -4640,10 +4640,7 @@
|
||||
},
|
||||
"height": {
|
||||
"name": "height"
|
||||
},
|
||||
"clear": {},
|
||||
"upload 3d model": {},
|
||||
"upload extra resources": {}
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
@@ -8805,9 +8802,6 @@
|
||||
},
|
||||
"camera_info": {
|
||||
"name": "camera_info"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -194,31 +194,20 @@ function createAssetService() {
|
||||
/**
|
||||
* Gets assets filtered by a specific tag
|
||||
*
|
||||
* @param tag - The tag to filter by (e.g., 'models', 'input')
|
||||
* @param tag - The tag to filter by (e.g., 'models')
|
||||
* @param includePublic - Whether to include public assets (default: true)
|
||||
* @param options - Pagination options
|
||||
* @param options.limit - Maximum number of assets to return (default: 500)
|
||||
* @param options.offset - Number of assets to skip (default: 0)
|
||||
* @returns Promise<AssetItem[]> - Full asset objects filtered by tag, excluding missing assets
|
||||
*/
|
||||
async function getAssetsByTag(
|
||||
tag: string,
|
||||
includePublic: boolean = true,
|
||||
{
|
||||
limit = DEFAULT_LIMIT,
|
||||
offset = 0
|
||||
}: { limit?: number; offset?: number } = {}
|
||||
includePublic: boolean = true
|
||||
): Promise<AssetItem[]> {
|
||||
const queryParams = new URLSearchParams({
|
||||
include_tags: tag,
|
||||
limit: limit.toString(),
|
||||
limit: DEFAULT_LIMIT.toString(),
|
||||
include_public: includePublic ? 'true' : 'false'
|
||||
})
|
||||
|
||||
if (offset > 0) {
|
||||
queryParams.set('offset', offset.toString())
|
||||
}
|
||||
|
||||
const data = await handleAssetRequest(
|
||||
`${ASSETS_ENDPOINT}?${queryParams.toString()}`,
|
||||
`assets for tag ${tag}`
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<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">
|
||||
{{ $t('cloudOnboarding.authTimeout.title') }}
|
||||
</h2>
|
||||
<p class="mb-5 text-muted">
|
||||
{{ $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')"
|
||||
class="w-full"
|
||||
@click="handleRestart"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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()
|
||||
await router.replace({ name: 'cloud-login' })
|
||||
}
|
||||
</script>
|
||||
@@ -1,126 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-[100vw] p-2 lg:w-96">
|
||||
<!-- Header -->
|
||||
<div class="mb-8 flex flex-col gap-4">
|
||||
<h1 class="my-0 text-xl leading-normal font-medium">
|
||||
{{ t('cloudForgotPassword_title') }}
|
||||
</h1>
|
||||
<p class="my-0 text-base text-muted">
|
||||
{{ t('cloudForgotPassword_instructions') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form class="flex flex-col gap-6" @submit.prevent="handleSubmit">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="mb-2 text-base font-medium opacity-80"
|
||||
for="reset-email"
|
||||
>
|
||||
{{ t('cloudForgotPassword_emailLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
id="reset-email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
:placeholder="t('cloudForgotPassword_emailPlaceholder')"
|
||||
class="h-10"
|
||||
:invalid="!!errorMessage && !email"
|
||||
autocomplete="email"
|
||||
required
|
||||
/>
|
||||
<small v-if="errorMessage" class="text-red-500">
|
||||
{{ errorMessage }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<Message v-if="successMessage" severity="success">
|
||||
{{ successMessage }}
|
||||
</Message>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<Button
|
||||
type="submit"
|
||||
:label="t('cloudForgotPassword_sendResetLink')"
|
||||
:loading="loading"
|
||||
:disabled="!email || loading"
|
||||
class="h-10 font-medium text-white"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
:label="t('cloudForgotPassword_backToLogin')"
|
||||
severity="secondary"
|
||||
class="h-10 bg-[#2d2e32]"
|
||||
@click="navigateToLogin"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Help text -->
|
||||
<p class="mt-5 text-sm text-gray-600">
|
||||
{{ t('cloudForgotPassword_didntReceiveEmail') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
|
||||
const email = ref('')
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const successMessage = ref('')
|
||||
|
||||
const navigateToLogin = () => {
|
||||
void router.push({ name: 'cloud-login' })
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!email.value) {
|
||||
errorMessage.value = t('cloudForgotPassword_emailRequired')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
successMessage.value = ''
|
||||
|
||||
try {
|
||||
// sendPasswordReset is already wrapped and returns a promise
|
||||
await authActions.sendPasswordReset(email.value)
|
||||
|
||||
successMessage.value = t('cloudForgotPassword_passwordResetSent')
|
||||
|
||||
// Optionally redirect to login after a delay
|
||||
setTimeout(() => {
|
||||
navigateToLogin()
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
console.error('Password reset error:', error)
|
||||
errorMessage.value = t('cloudForgotPassword_passwordResetError')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.p-inputtext) {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: #2d2e32 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-screen p-2 lg:w-96">
|
||||
<!-- Header -->
|
||||
<div class="mt-6 mb-8 flex flex-col gap-4">
|
||||
<h1 class="my-0 text-xl leading-normal font-medium">
|
||||
{{ t('auth.login.title') }}
|
||||
</h1>
|
||||
<p class="my-0 text-base">
|
||||
<span class="text-muted">{{ t('auth.login.newUser') }}</span>
|
||||
<span
|
||||
class="ml-1 cursor-pointer text-blue-500"
|
||||
@click="navigateToSignup"
|
||||
>{{ t('auth.login.signUp') }}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Message v-if="!isSecureContext" severity="warn" class="mb-4">
|
||||
{{ t('auth.login.insecureContextWarning') }}
|
||||
</Message>
|
||||
|
||||
<!-- Form -->
|
||||
<CloudSignInForm :auth-error="authError" @submit="signInWithEmail" />
|
||||
|
||||
<!-- Divider -->
|
||||
<Divider align="center" layout="horizontal" class="my-8">
|
||||
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
|
||||
</Divider>
|
||||
|
||||
<!-- Social Login Buttons -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10 bg-[#2d2e32]"
|
||||
severity="secondary"
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{ t('auth.login.loginWithGoogle') }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10 bg-[#2d2e32]"
|
||||
severity="secondary"
|
||||
@click="signInWithGithub"
|
||||
>
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
{{ t('auth.login.loginWithGithub') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Terms & Contact -->
|
||||
<p class="mt-5 text-sm text-gray-600">
|
||||
{{ t('auth.login.termsText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/terms-of-service"
|
||||
target="_blank"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
>
|
||||
{{ t('auth.login.termsLink') }}
|
||||
</a>
|
||||
{{ t('auth.login.andText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/privacy-policy"
|
||||
target="_blank"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
>
|
||||
{{ t('auth.login.privacyLink') }} </a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import Message from 'primevue/message'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import CloudSignInForm from '@/platform/cloud/onboarding/components/CloudSignInForm.vue'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const isSecureContext = window.isSecureContext
|
||||
const authError = ref('')
|
||||
const toastStore = useToastStore()
|
||||
|
||||
const navigateToSignup = () => {
|
||||
void router.push({ name: 'cloud-signup', query: route.query })
|
||||
}
|
||||
|
||||
const onSuccess = async () => {
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: 'Login Completed',
|
||||
life: 2000
|
||||
})
|
||||
await router.push({ name: 'cloud-user-check' })
|
||||
}
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
authError.value = ''
|
||||
if (await authActions.signInWithGoogle()) {
|
||||
await onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const signInWithGithub = async () => {
|
||||
authError.value = ''
|
||||
if (await authActions.signInWithGithub()) {
|
||||
await onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const signInWithEmail = async (values: SignInData) => {
|
||||
authError.value = ''
|
||||
if (await authActions.signInWithEmail(values.email, values.password)) {
|
||||
await onSuccess()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,177 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-screen p-2 lg:w-96">
|
||||
<!-- Header -->
|
||||
<div class="mb-8 flex flex-col gap-4">
|
||||
<h1 class="my-0 text-xl leading-normal font-medium">
|
||||
{{ t('auth.signup.title') }}
|
||||
</h1>
|
||||
<p class="my-0 text-base">
|
||||
<span class="text-muted">{{
|
||||
t('auth.signup.alreadyHaveAccount')
|
||||
}}</span>
|
||||
<span
|
||||
class="ml-1 cursor-pointer text-blue-500"
|
||||
@click="navigateToLogin"
|
||||
>{{ t('auth.signup.signIn') }}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Message v-if="!isSecureContext" severity="warn" class="mb-4">
|
||||
{{ t('auth.login.insecureContextWarning') }}
|
||||
</Message>
|
||||
|
||||
<!-- Form -->
|
||||
<Message v-if="userIsInChina" severity="warn" class="mb-4">
|
||||
{{ t('auth.signup.regionRestrictionChina') }}
|
||||
</Message>
|
||||
<SignUpForm v-else :auth-error="authError" @submit="signUpWithEmail" />
|
||||
|
||||
<!-- Divider -->
|
||||
<Divider align="center" layout="horizontal" class="my-8">
|
||||
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
|
||||
</Divider>
|
||||
|
||||
<!-- Social Login Buttons -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10 bg-[#2d2e32]"
|
||||
severity="secondary"
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{ t('auth.signup.signUpWithGoogle') }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10 bg-[#2d2e32]"
|
||||
severity="secondary"
|
||||
@click="signInWithGithub"
|
||||
>
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
{{ t('auth.signup.signUpWithGithub') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Terms & Contact -->
|
||||
<div class="mt-5 text-sm text-gray-600">
|
||||
{{ t('auth.login.termsText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/terms-of-service"
|
||||
target="_blank"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
>
|
||||
{{ t('auth.login.termsLink') }}
|
||||
</a>
|
||||
{{ t('auth.login.andText') }}
|
||||
<a
|
||||
href="/privacy-policy"
|
||||
target="_blank"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
>
|
||||
{{ t('auth.login.privacyLink') }} </a
|
||||
>.
|
||||
<p class="mt-2">
|
||||
{{ t('cloudWaitlist_questionsText') }}
|
||||
<a
|
||||
href="https://support.comfy.org"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ t('cloudWaitlist_contactLink') }}</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import Message from 'primevue/message'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import SignUpForm from '@/components/dialog/content/signin/SignUpForm.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
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 { isInChina } from '@/utils/networkUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const isSecureContext = window.isSecureContext
|
||||
const authError = ref('')
|
||||
const userIsInChina = ref(false)
|
||||
const toastStore = useToastStore()
|
||||
|
||||
const navigateToLogin = () => {
|
||||
void router.push({ name: 'cloud-login', query: route.query })
|
||||
}
|
||||
|
||||
const onSuccess = async () => {
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: 'Sign up Completed',
|
||||
life: 2000
|
||||
})
|
||||
// Direct redirect to main app - email verification removed
|
||||
await router.push({ path: '/', query: route.query })
|
||||
}
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
authError.value = ''
|
||||
if (await authActions.signInWithGoogle()) {
|
||||
await onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const signInWithGithub = async () => {
|
||||
authError.value = ''
|
||||
if (await authActions.signInWithGithub()) {
|
||||
await onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const signUpWithEmail = async (values: SignUpData) => {
|
||||
authError.value = ''
|
||||
if (await authActions.signUpWithEmail(values.email, values.password)) {
|
||||
await onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Track signup screen opened
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackSignupOpened()
|
||||
}
|
||||
|
||||
userIsInChina.value = await isInChina()
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.p-inputtext) {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: #2d2e32 !important;
|
||||
}
|
||||
|
||||
:deep(.p-password input) {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
:deep(.p-checkbox-checked .p-checkbox-box) {
|
||||
background-color: #f0ff41 !important;
|
||||
border-color: #f0ff41 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<div class="cloud-sorry-contact-support">
|
||||
<h1>{{ t('cloudSorryContactSupport_title') }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cloud-sorry-contact-support {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
font-family: monospace;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,387 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Stepper
|
||||
value="1"
|
||||
class="flex h-[638px] max-h-[80vh] w-[320px] max-w-[90vw] flex-col"
|
||||
>
|
||||
<ProgressBar
|
||||
:value="progressPercent"
|
||||
:show-value="false"
|
||||
class="mb-8 h-2"
|
||||
/>
|
||||
|
||||
<StepPanels class="flex flex-1 flex-col p-0">
|
||||
<StepPanel
|
||||
v-slot="{ activateCallback }"
|
||||
value="1"
|
||||
class="flex min-h-full flex-1 flex-col justify-between bg-transparent"
|
||||
>
|
||||
<div>
|
||||
<label class="mb-8 block text-lg font-medium">{{
|
||||
t('cloudSurvey_steps_familiarity')
|
||||
}}</label>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div
|
||||
v-for="opt in familiarityOptions"
|
||||
:key="opt.value"
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<RadioButton
|
||||
v-model="surveyData.familiarity"
|
||||
:input-id="`fam-${opt.value}`"
|
||||
name="familiarity"
|
||||
:value="opt.value"
|
||||
/>
|
||||
<label
|
||||
:for="`fam-${opt.value}`"
|
||||
class="cursor-pointer text-sm"
|
||||
>{{ opt.label }}</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between pt-4">
|
||||
<span />
|
||||
<Button
|
||||
label="Next"
|
||||
:disabled="!validStep1"
|
||||
class="h-10 w-full border-none text-white"
|
||||
@click="goTo(2, activateCallback)"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
|
||||
<StepPanel
|
||||
v-slot="{ activateCallback }"
|
||||
value="2"
|
||||
class="flex min-h-full flex-1 flex-col justify-between bg-transparent"
|
||||
>
|
||||
<div>
|
||||
<label class="mb-8 block text-lg font-medium">{{
|
||||
t('cloudSurvey_steps_purpose')
|
||||
}}</label>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div
|
||||
v-for="opt in purposeOptions"
|
||||
:key="opt.value"
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<RadioButton
|
||||
v-model="surveyData.useCase"
|
||||
:input-id="`purpose-${opt.value}`"
|
||||
name="purpose"
|
||||
:value="opt.value"
|
||||
/>
|
||||
<label
|
||||
:for="`purpose-${opt.value}`"
|
||||
class="cursor-pointer text-sm"
|
||||
>{{ opt.label }}</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="surveyData.useCase === 'other'" class="mt-4 ml-8">
|
||||
<InputText
|
||||
v-model="surveyData.useCaseOther"
|
||||
class="w-full"
|
||||
placeholder="Please specify"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-6 pt-4">
|
||||
<Button
|
||||
label="Back"
|
||||
severity="secondary"
|
||||
class="flex-1 text-white"
|
||||
@click="goTo(1, activateCallback)"
|
||||
/>
|
||||
<Button
|
||||
label="Next"
|
||||
:disabled="!validStep2"
|
||||
class="h-10 flex-1 text-white"
|
||||
@click="goTo(3, activateCallback)"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
|
||||
<StepPanel
|
||||
v-slot="{ activateCallback }"
|
||||
value="3"
|
||||
class="flex min-h-full flex-1 flex-col justify-between bg-transparent"
|
||||
>
|
||||
<div>
|
||||
<label class="mb-8 block text-lg font-medium">{{
|
||||
t('cloudSurvey_steps_industry')
|
||||
}}</label>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div
|
||||
v-for="opt in industryOptions"
|
||||
:key="opt.value"
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<RadioButton
|
||||
v-model="surveyData.industry"
|
||||
:input-id="`industry-${opt.value}`"
|
||||
name="industry"
|
||||
:value="opt.value"
|
||||
/>
|
||||
<label
|
||||
:for="`industry-${opt.value}`"
|
||||
class="cursor-pointer text-sm"
|
||||
>{{ opt.label }}</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="surveyData.industry === 'other'" class="mt-4 ml-8">
|
||||
<InputText
|
||||
v-model="surveyData.industryOther"
|
||||
class="w-full"
|
||||
placeholder="Please specify"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-6 pt-4">
|
||||
<Button
|
||||
label="Back"
|
||||
severity="secondary"
|
||||
class="flex-1 text-white"
|
||||
@click="goTo(2, activateCallback)"
|
||||
/>
|
||||
<Button
|
||||
label="Next"
|
||||
:disabled="!validStep3"
|
||||
class="h-10 flex-1 border-none text-white"
|
||||
@click="goTo(4, activateCallback)"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
|
||||
<StepPanel
|
||||
v-slot="{ activateCallback }"
|
||||
value="4"
|
||||
class="flex min-h-full flex-1 flex-col justify-between bg-transparent"
|
||||
>
|
||||
<div>
|
||||
<label class="mb-8 block text-lg font-medium">{{
|
||||
t('cloudSurvey_steps_making')
|
||||
}}</label>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div
|
||||
v-for="opt in makingOptions"
|
||||
:key="opt.value"
|
||||
class="flex items-center gap-3"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="surveyData.making"
|
||||
:input-id="`making-${opt.value}`"
|
||||
:value="opt.value"
|
||||
/>
|
||||
<label
|
||||
:for="`making-${opt.value}`"
|
||||
class="cursor-pointer text-sm"
|
||||
>{{ opt.label }}</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-6 pt-4">
|
||||
<Button
|
||||
label="Back"
|
||||
severity="secondary"
|
||||
class="flex-1 text-white"
|
||||
@click="goTo(3, activateCallback)"
|
||||
/>
|
||||
<Button
|
||||
label="Submit"
|
||||
:disabled="!validStep4 || isSubmitting"
|
||||
:loading="isSubmitting"
|
||||
class="h-10 flex-1 border-none text-white"
|
||||
@click="onSubmitSurvey"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
</Stepper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import StepPanel from 'primevue/steppanel'
|
||||
import StepPanels from 'primevue/steppanels'
|
||||
import Stepper from 'primevue/stepper'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
getSurveyCompletedStatus,
|
||||
submitSurvey
|
||||
} from '@/platform/cloud/onboarding/auth'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
// Check if survey is already completed on mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const surveyCompleted = await getSurveyCompletedStatus()
|
||||
if (surveyCompleted) {
|
||||
// User already completed survey, redirect to waitlist
|
||||
await router.replace({ name: 'cloud-waitlist' })
|
||||
} else {
|
||||
// Track survey opened event
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackSurvey('opened')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check survey status:', error)
|
||||
}
|
||||
})
|
||||
|
||||
const activeStep = ref(1)
|
||||
const totalSteps = 4
|
||||
const progressPercent = computed(() =>
|
||||
Math.max(20, Math.min(100, ((activeStep.value - 1) / (totalSteps - 1)) * 100))
|
||||
)
|
||||
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const surveyData = ref({
|
||||
familiarity: '',
|
||||
useCase: '',
|
||||
useCaseOther: '',
|
||||
industry: '',
|
||||
industryOther: '',
|
||||
making: [] as string[]
|
||||
})
|
||||
|
||||
// Options
|
||||
const familiarityOptions = [
|
||||
{ label: 'New to ComfyUI (never used it before)', value: 'new' },
|
||||
{ label: 'Just getting started (following tutorials)', value: 'starting' },
|
||||
{ label: 'Comfortable with basics', value: 'basics' },
|
||||
{ label: 'Advanced user (custom workflows)', value: 'advanced' },
|
||||
{ label: 'Expert (help others)', value: 'expert' }
|
||||
]
|
||||
|
||||
const purposeOptions = [
|
||||
{ label: 'Personal projects/hobby', value: 'personal' },
|
||||
{
|
||||
label: 'Community contributions (nodes, workflows, etc.)',
|
||||
value: 'community'
|
||||
},
|
||||
{ label: 'Client work (freelance)', value: 'client' },
|
||||
{ label: 'My own workplace (in-house)', value: 'inhouse' },
|
||||
{ label: 'Academic research', value: 'research' },
|
||||
{ label: 'Other', value: 'other' }
|
||||
]
|
||||
|
||||
const industryOptions = [
|
||||
{ label: 'Film, TV, & animation', value: 'film_tv_animation' },
|
||||
{ label: 'Gaming', value: 'gaming' },
|
||||
{ label: 'Marketing & advertising', value: 'marketing' },
|
||||
{ label: 'Architecture', value: 'architecture' },
|
||||
{ label: 'Product & graphic design', value: 'product_design' },
|
||||
{ label: 'Fine art & illustration', value: 'fine_art' },
|
||||
{ label: 'Software & technology', value: 'software' },
|
||||
{ label: 'Education', value: 'education' },
|
||||
{ label: 'Other', value: 'other' }
|
||||
]
|
||||
|
||||
const makingOptions = [
|
||||
{ label: 'Images', value: 'images' },
|
||||
{ label: 'Video & animation', value: 'video' },
|
||||
{ label: '3D assets', value: '3d' },
|
||||
{ label: 'Audio/music', value: 'audio' },
|
||||
{ label: 'Custom nodes & workflows', value: 'custom_nodes' }
|
||||
]
|
||||
|
||||
// Validation per step
|
||||
const validStep1 = computed(() => !!surveyData.value.familiarity)
|
||||
const validStep2 = computed(() => {
|
||||
if (!surveyData.value.useCase) return false
|
||||
if (surveyData.value.useCase === 'other') {
|
||||
return !!surveyData.value.useCaseOther?.trim()
|
||||
}
|
||||
return true
|
||||
})
|
||||
const validStep3 = computed(() => {
|
||||
if (!surveyData.value.industry) return false
|
||||
if (surveyData.value.industry === 'other') {
|
||||
return !!surveyData.value.industryOther?.trim()
|
||||
}
|
||||
return true
|
||||
})
|
||||
const validStep4 = computed(() => surveyData.value.making.length > 0)
|
||||
|
||||
const changeActiveStep = (step: number) => {
|
||||
activeStep.value = step
|
||||
}
|
||||
|
||||
const goTo = (step: number, activate: (val: string | number) => void) => {
|
||||
// keep Stepper panel and progress bar in sync; Stepper values are strings
|
||||
changeActiveStep(step)
|
||||
activate(String(step))
|
||||
}
|
||||
|
||||
// Submit
|
||||
const onSubmitSurvey = async () => {
|
||||
try {
|
||||
isSubmitting.value = true
|
||||
// prepare payload with consistent structure
|
||||
const payload = {
|
||||
familiarity: surveyData.value.familiarity,
|
||||
useCase:
|
||||
surveyData.value.useCase === 'other'
|
||||
? surveyData.value.useCaseOther?.trim() || 'other'
|
||||
: surveyData.value.useCase,
|
||||
industry:
|
||||
surveyData.value.industry === 'other'
|
||||
? surveyData.value.industryOther?.trim() || 'other'
|
||||
: surveyData.value.industry,
|
||||
making: surveyData.value.making
|
||||
}
|
||||
|
||||
await submitSurvey(payload)
|
||||
|
||||
// Track survey submitted event with responses
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackSurvey('submitted', {
|
||||
industry: payload.industry,
|
||||
useCase: payload.useCase,
|
||||
familiarity: payload.familiarity,
|
||||
making: payload.making
|
||||
})
|
||||
}
|
||||
|
||||
await router.push({ name: 'cloud-user-check' })
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-progressbar .p-progressbar-value) {
|
||||
background-color: #f0ff41 !important;
|
||||
}
|
||||
:deep(.p-radiobutton-checked .p-radiobutton-box) {
|
||||
background-color: #f0ff41 !important;
|
||||
border-color: #f0ff41 !important;
|
||||
}
|
||||
:deep(.p-checkbox-checked .p-checkbox-box) {
|
||||
background-color: #f0ff41 !important;
|
||||
border-color: #f0ff41 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,102 +0,0 @@
|
||||
<template>
|
||||
<CloudLoginViewSkeleton v-if="skeletonType === 'login'" />
|
||||
<CloudSurveyViewSkeleton v-else-if="skeletonType === 'survey'" />
|
||||
<CloudWaitlistViewSkeleton v-else-if="skeletonType === 'waitlist'" />
|
||||
<div v-else-if="error" class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-[100vw] p-2 text-center lg:w-96">
|
||||
<p class="mb-4 text-red-500">{{ errorMessage }}</p>
|
||||
<Button
|
||||
:label="
|
||||
isRetrying
|
||||
? $t('cloudOnboarding.retrying')
|
||||
: $t('cloudOnboarding.retry')
|
||||
"
|
||||
:loading="isRetrying"
|
||||
class="w-full"
|
||||
@click="handleRetry"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center">
|
||||
<ProgressSpinner class="h-8 w-8" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import {
|
||||
getSurveyCompletedStatus,
|
||||
getUserCloudStatus
|
||||
} from '@/platform/cloud/onboarding/auth'
|
||||
|
||||
import CloudLoginViewSkeleton from './skeletons/CloudLoginViewSkeleton.vue'
|
||||
import CloudSurveyViewSkeleton from './skeletons/CloudSurveyViewSkeleton.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const skeletonType = ref<'login' | 'survey' | 'waitlist' | 'loading'>('loading')
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
execute: checkUserStatus
|
||||
} = useAsyncState(
|
||||
wrapWithErrorHandlingAsync(async () => {
|
||||
await nextTick()
|
||||
|
||||
const [cloudUserStats, surveyStatus] = await Promise.all([
|
||||
getUserCloudStatus(),
|
||||
getSurveyCompletedStatus()
|
||||
])
|
||||
|
||||
// Navigate based on user status
|
||||
if (!cloudUserStats) {
|
||||
skeletonType.value = 'login'
|
||||
await router.replace({ name: 'cloud-login' })
|
||||
return
|
||||
}
|
||||
|
||||
// Survey is required for all users
|
||||
if (!surveyStatus) {
|
||||
skeletonType.value = 'survey'
|
||||
await router.replace({ name: 'cloud-survey' })
|
||||
return
|
||||
}
|
||||
|
||||
// User is fully onboarded (active or whitelist check disabled)
|
||||
window.location.href = '/'
|
||||
}),
|
||||
null,
|
||||
{ resetOnExecute: false }
|
||||
)
|
||||
|
||||
const errorMessage = computed(() => {
|
||||
if (!error.value) return ''
|
||||
|
||||
// Provide user-friendly error messages
|
||||
const errorStr = error.value.toString().toLowerCase()
|
||||
|
||||
if (errorStr.includes('network') || errorStr.includes('fetch')) {
|
||||
return 'Connection problem. Please check your internet connection.'
|
||||
}
|
||||
|
||||
if (errorStr.includes('timeout')) {
|
||||
return 'Request timed out. Please try again.'
|
||||
}
|
||||
|
||||
return 'Unable to check account status. Please try again.'
|
||||
})
|
||||
|
||||
const isRetrying = computed(() => isLoading.value && !!error.value)
|
||||
|
||||
const handleRetry = async () => {
|
||||
await checkUserStatus()
|
||||
}
|
||||
</script>
|
||||
@@ -1,33 +0,0 @@
|
||||
/* ABC ROM Extended — full face mapping */
|
||||
@font-face {
|
||||
font-family: 'ABC ROM Extended';
|
||||
src:
|
||||
local('ABC ROM Extended Black Italic'),
|
||||
local('ABCRom BlackItalic'),
|
||||
url('../fonts/ABCROMExtended-BlackItalic.woff2') format('woff2'),
|
||||
url('../fonts/ABCROMExtended-BlackItalic.woff') format('woff');
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Prevent browser from synthesizing fake bold/italic which can cause mismatches */
|
||||
.hero-title,
|
||||
.font-abcrom {
|
||||
font-family: 'ABC ROM Extended', sans-serif;
|
||||
font-synthesis: none; /* no faux bold/italic */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* Figma-like hero style */
|
||||
.hero-title {
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 0 4px 4px rgb(0 0 0 / 0.25);
|
||||
/* Figma has leading-trim/text-edge which CSS doesn't support; emulate with tight line-height */
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 887 KiB |
@@ -1,235 +0,0 @@
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { isEmpty } from 'es-toolkit/compat'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
interface UserCloudStatus {
|
||||
status: 'active'
|
||||
}
|
||||
|
||||
const ONBOARDING_SURVEY_KEY = 'onboarding_survey'
|
||||
|
||||
/**
|
||||
* Helper function to capture API errors with Sentry
|
||||
*/
|
||||
function captureApiError(
|
||||
error: Error,
|
||||
endpoint: string,
|
||||
errorType: 'http_error' | 'network_error',
|
||||
httpStatus?: number,
|
||||
operation?: string,
|
||||
extraContext?: Record<string, any>
|
||||
) {
|
||||
const tags: Record<string, any> = {
|
||||
api_endpoint: endpoint,
|
||||
error_type: errorType
|
||||
}
|
||||
|
||||
if (httpStatus !== undefined) {
|
||||
tags.http_status = httpStatus
|
||||
}
|
||||
|
||||
if (operation) {
|
||||
tags.operation = operation
|
||||
}
|
||||
|
||||
const sentryOptions: any = {
|
||||
tags,
|
||||
extra: extraContext ? { ...extraContext } : undefined
|
||||
}
|
||||
|
||||
Sentry.captureException(error, sentryOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check if error is already handled HTTP error
|
||||
*/
|
||||
function isHttpError(error: unknown, errorMessagePrefix: string): boolean {
|
||||
return error instanceof Error && error.message.startsWith(errorMessagePrefix)
|
||||
}
|
||||
|
||||
export async function getUserCloudStatus(): Promise<UserCloudStatus> {
|
||||
try {
|
||||
const response = await api.fetchApi('/user', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) {
|
||||
const error = new Error(`Failed to get user: ${response.statusText}`)
|
||||
captureApiError(
|
||||
error,
|
||||
'/user',
|
||||
'http_error',
|
||||
response.status,
|
||||
undefined,
|
||||
{
|
||||
api: {
|
||||
method: 'GET',
|
||||
endpoint: '/user',
|
||||
status_code: response.status,
|
||||
status_text: response.statusText
|
||||
}
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
return response.json()
|
||||
} catch (error) {
|
||||
// Only capture network errors (not HTTP errors we already captured)
|
||||
if (!isHttpError(error, 'Failed to get user:')) {
|
||||
captureApiError(error as Error, '/user', 'network_error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSurveyCompletedStatus(): Promise<boolean> {
|
||||
try {
|
||||
const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) {
|
||||
// Not an error case - survey not completed is a valid state
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'auth',
|
||||
message: 'Survey status check returned non-ok response',
|
||||
level: 'info',
|
||||
data: {
|
||||
status: response.status,
|
||||
endpoint: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
const data = await response.json()
|
||||
// Check if data exists and is not empty
|
||||
return !isEmpty(data.value)
|
||||
} catch (error) {
|
||||
// Network error - still capture it as it's not thrown from above
|
||||
Sentry.captureException(error, {
|
||||
tags: {
|
||||
api_endpoint: '/settings/{key}',
|
||||
error_type: 'network_error'
|
||||
},
|
||||
extra: {
|
||||
route_template: '/settings/{key}',
|
||||
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||
},
|
||||
level: 'warning'
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error - Unused function kept for future use
|
||||
async function postSurveyStatus(): Promise<void> {
|
||||
try {
|
||||
const response = await api.fetchApi(`/settings/${ONBOARDING_SURVEY_KEY}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: undefined })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(
|
||||
`Failed to post survey status: ${response.statusText}`
|
||||
)
|
||||
captureApiError(
|
||||
error,
|
||||
'/settings/{key}',
|
||||
'http_error',
|
||||
response.status,
|
||||
'post_survey_status',
|
||||
{
|
||||
route_template: '/settings/{key}',
|
||||
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
// Only capture network errors (not HTTP errors we already captured)
|
||||
if (!isHttpError(error, 'Failed to post survey status:')) {
|
||||
captureApiError(
|
||||
error as Error,
|
||||
'/settings/{key}',
|
||||
'network_error',
|
||||
undefined,
|
||||
'post_survey_status',
|
||||
{
|
||||
route_template: '/settings/{key}',
|
||||
route_actual: `/settings/${ONBOARDING_SURVEY_KEY}`
|
||||
}
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitSurvey(
|
||||
survey: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
try {
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'auth',
|
||||
message: 'Submitting survey',
|
||||
level: 'info',
|
||||
data: {
|
||||
survey_fields: Object.keys(survey)
|
||||
}
|
||||
})
|
||||
|
||||
const response = await api.fetchApi('/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ [ONBOARDING_SURVEY_KEY]: survey })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(`Failed to submit survey: ${response.statusText}`)
|
||||
captureApiError(
|
||||
error,
|
||||
'/settings',
|
||||
'http_error',
|
||||
response.status,
|
||||
'submit_survey',
|
||||
{
|
||||
survey: {
|
||||
field_count: Object.keys(survey).length,
|
||||
field_names: Object.keys(survey)
|
||||
}
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Log successful survey submission
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'auth',
|
||||
message: 'Survey submitted successfully',
|
||||
level: 'info'
|
||||
})
|
||||
} catch (error) {
|
||||
// Only capture network errors (not HTTP errors we already captured)
|
||||
if (!isHttpError(error, 'Failed to submit survey:')) {
|
||||
captureApiError(
|
||||
error as Error,
|
||||
'/settings',
|
||||
'network_error',
|
||||
undefined,
|
||||
'submit_survey'
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<CloudTemplate>
|
||||
<!-- This will render the nested route components -->
|
||||
<RouterView />
|
||||
</CloudTemplate>
|
||||
<!-- Global Toast for displaying notifications -->
|
||||
<GlobalToast />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
import GlobalToast from '@/components/toast/GlobalToast.vue'
|
||||
|
||||
import CloudTemplate from './CloudTemplate.vue'
|
||||
</script>
|
||||
@@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div class="mx-auto flex h-[7%] max-h-[70px] w-5/6 items-end">
|
||||
<img
|
||||
src="/assets/images/comfy-cloud-logo.svg"
|
||||
alt="Comfy Cloud Logo"
|
||||
class="h-3/4 max-h-10 w-auto"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(signInSchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<!-- Email Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="mb-2 text-base font-medium opacity-80" :for="emailInputId">
|
||||
{{ t('auth.login.emailLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
:id="emailInputId"
|
||||
autocomplete="email"
|
||||
class="h-10"
|
||||
name="email"
|
||||
type="text"
|
||||
:placeholder="t('auth.login.emailPlaceholder')"
|
||||
:invalid="$form.email?.invalid"
|
||||
/>
|
||||
<small v-if="$form.email?.invalid" class="text-red-500">{{
|
||||
$form.email.error.message
|
||||
}}</small>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<label
|
||||
class="text-base font-medium opacity-80"
|
||||
for="cloud-sign-in-password"
|
||||
>
|
||||
{{ t('auth.login.passwordLabel') }}
|
||||
</label>
|
||||
</div>
|
||||
<Password
|
||||
input-id="cloud-sign-in-password"
|
||||
pt:pc-input-text:root:autocomplete="current-password"
|
||||
name="password"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
:placeholder="t('auth.login.passwordPlaceholder')"
|
||||
:class="{ 'p-invalid': $form.password?.invalid }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<small v-if="$form.password?.invalid" class="text-red-500">{{
|
||||
$form.password.error.message
|
||||
}}</small>
|
||||
|
||||
<router-link
|
||||
:to="{ name: 'cloud-forgot-password' }"
|
||||
class="text-sm font-medium text-muted no-underline"
|
||||
>
|
||||
{{ t('auth.login.forgotPassword') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Auth Error Message -->
|
||||
<Message v-if="authError" severity="error">
|
||||
{{ authError }}
|
||||
</Message>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<ProgressSpinner v-if="loading" class="h-8 w-8" />
|
||||
<Button
|
||||
v-else
|
||||
type="submit"
|
||||
:label="t('auth.login.loginButton')"
|
||||
class="mt-4 h-10 font-medium text-white"
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import Password from 'primevue/password'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { signInSchema } from '@/schemas/signInSchema'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
authError?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [values: SignInData]
|
||||
}>()
|
||||
|
||||
const emailInputId = 'cloud-sign-in-email'
|
||||
|
||||
const onSubmit = (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignInData)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.p-inputtext) {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: #2d2e32 !important;
|
||||
}
|
||||
|
||||
:deep(.p-password input) {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
:deep(.p-checkbox-checked .p-checkbox-box) {
|
||||
background-color: #f0ff41 !important;
|
||||
border-color: #f0ff41 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<BaseViewTemplate dark class="flex-1">
|
||||
<template #header>
|
||||
<CloudLogo />
|
||||
</template>
|
||||
<slot />
|
||||
<template #footer>
|
||||
<CloudTemplateFooter />
|
||||
</template>
|
||||
</BaseViewTemplate>
|
||||
<div class="relative hidden flex-1 overflow-hidden bg-black lg:block">
|
||||
<!-- Video Background -->
|
||||
<video
|
||||
class="absolute inset-0 h-full w-full object-cover"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
:poster="videoPoster"
|
||||
>
|
||||
<source :src="videoSrc" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
<div class="absolute inset-0 h-full w-full bg-black/30"></div>
|
||||
|
||||
<!-- Optional Overlay for better visual -->
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center text-center text-white"
|
||||
>
|
||||
<div>
|
||||
<h1 class="font-abcrom hero-title font-black uppercase italic">
|
||||
{{ t('cloudStart_title') }}
|
||||
</h1>
|
||||
<p class="m-2 text-center text-xl text-white">
|
||||
{{ t('cloudStart_desc') }}
|
||||
</p>
|
||||
<p class="m-0 text-center text-xl text-white">
|
||||
{{ t('cloudStart_explain') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 flex flex-col justify-end px-14 pb-[64px]">
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex items-center gap-3">
|
||||
<p class="text-md text-white">
|
||||
{{ t('cloudStart_wantToRun') }}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10 bg-black font-bold text-white"
|
||||
severity="secondary"
|
||||
@click="handleDownloadClick"
|
||||
>
|
||||
{{ t('cloudStart_download') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import videoPoster from '@/platform/cloud/onboarding/assets/videos/thumbnail.png'
|
||||
import videoSrc from '@/platform/cloud/onboarding/assets/videos/video.mp4'
|
||||
import CloudLogo from '@/platform/cloud/onboarding/components/CloudLogo.vue'
|
||||
import CloudTemplateFooter from '@/platform/cloud/onboarding/components/CloudTemplateFooter.vue'
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
const handleDownloadClick = () => {
|
||||
window.open('https://www.comfy.org/download', '_blank')
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@import '../assets/css/fonts.css';
|
||||
</style>
|
||||
@@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<footer class="mx-auto flex h-[5%] max-h-[60px] w-5/6 items-start gap-2.5">
|
||||
<a
|
||||
href="https://www.comfy.org/terms-of-service"
|
||||
target="_blank"
|
||||
class="cursor-pointer text-sm text-gray-600 no-underline"
|
||||
>
|
||||
{{ t('auth.login.termsLink') }}
|
||||
</a>
|
||||
<a
|
||||
href="https://www.comfy.org/privacy-policy"
|
||||
target="_blank"
|
||||
class="cursor-pointer text-sm text-gray-600 no-underline"
|
||||
>
|
||||
{{ t('auth.login.privacyLink') }}
|
||||
</a>
|
||||
<a
|
||||
href="https://support.comfy.org"
|
||||
class="cursor-pointer text-sm text-gray-600 no-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ t('cloudFooter_needHelp') }}
|
||||
</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
@@ -1,72 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const cloudOnboardingRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/cloud',
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/components/CloudLayoutView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'login',
|
||||
name: 'cloud-login',
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/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, or main page routing
|
||||
return next({ name: 'cloud-user-check' })
|
||||
}
|
||||
}
|
||||
next()
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'signup',
|
||||
name: 'cloud-signup',
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/CloudSignupView.vue')
|
||||
},
|
||||
{
|
||||
path: 'forgot-password',
|
||||
name: 'cloud-forgot-password',
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/CloudForgotPasswordView.vue')
|
||||
},
|
||||
{
|
||||
path: 'survey',
|
||||
name: 'cloud-survey',
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/CloudSurveyView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'user-check',
|
||||
name: 'cloud-user-check',
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/UserCheckView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'sorry-contact-support',
|
||||
name: 'cloud-sorry-contact-support',
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/CloudSorryContactSupportView.vue')
|
||||
},
|
||||
{
|
||||
path: 'auth-timeout',
|
||||
name: 'cloud-auth-timeout',
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/CloudAuthTimeoutView.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-[100vw] lg:w-96">
|
||||
<div class="rounded-lg bg-[#2d2e32] p-4">
|
||||
<Skeleton width="60%" height="1.125rem" class="mb-2" />
|
||||
<Skeleton width="90%" height="1rem" class="mb-2" />
|
||||
<Skeleton width="80%" height="1rem" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 mb-8 flex flex-col gap-4">
|
||||
<Skeleton width="45%" height="1.5rem" class="my-0" />
|
||||
<div class="flex items-center">
|
||||
<Skeleton width="25%" height="1rem" class="mr-1" />
|
||||
<Skeleton width="20%" height="1rem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<Skeleton width="20%" height="1rem" class="mb-2" />
|
||||
<Skeleton width="100%" height="2.5rem" class="mb-4" />
|
||||
<Skeleton width="25%" height="1rem" class="mb-4" />
|
||||
<Skeleton width="100%" height="2.5rem" class="mb-6" />
|
||||
<Skeleton width="80%" height="1rem" class="mb-4" />
|
||||
<Skeleton width="100%" height="2.5rem" />
|
||||
</div>
|
||||
|
||||
<div class="my-8 flex items-center">
|
||||
<div class="flex-1 border-t border-gray-300"></div>
|
||||
<Skeleton width="30%" height="1rem" class="mx-4" />
|
||||
<div class="flex-1 border-t border-gray-300"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<Skeleton width="100%" height="2.5rem" />
|
||||
<Skeleton width="100%" height="2.5rem" />
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<Skeleton width="70%" height="0.875rem" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
</script>
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex min-h-[638px] min-w-[320px] flex-col">
|
||||
<Skeleton width="100%" height="0.5rem" class="mb-8" />
|
||||
|
||||
<div class="flex flex-1 flex-col p-0">
|
||||
<div class="flex min-h-full flex-1 flex-col justify-between">
|
||||
<div>
|
||||
<Skeleton width="70%" height="1.75rem" class="mb-8" />
|
||||
<div class="flex flex-col gap-6">
|
||||
<div v-for="i in 5" :key="i" class="flex items-center gap-3">
|
||||
<Skeleton width="1.25rem" height="1.25rem" shape="circle" />
|
||||
<Skeleton width="85%" height="0.875rem" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between pt-4">
|
||||
<span />
|
||||
<Skeleton width="100%" height="2.5rem" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
</script>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.bottom="{
|
||||
value: $t('subscription.subscribeToRunFull'),
|
||||
value: $t('subscription.subscribeToRun'),
|
||||
showDelay: 600
|
||||
}"
|
||||
class="subscribe-to-run-button"
|
||||
:label="buttonLabel"
|
||||
:label="$t('subscription.subscribeToRun')"
|
||||
icon="pi pi-lock"
|
||||
severity="primary"
|
||||
size="small"
|
||||
@@ -15,7 +15,6 @@
|
||||
}"
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'whitespace-nowrap',
|
||||
style: {
|
||||
borderColor: 'transparent'
|
||||
}
|
||||
@@ -27,25 +26,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
|
||||
const { t } = useI18n()
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isMdOrLarger = breakpoints.greaterOrEqual('md')
|
||||
|
||||
const buttonLabel = computed(() =>
|
||||
isMdOrLarger.value
|
||||
? t('subscription.subscribeToRunFull')
|
||||
: t('subscription.subscribeToRun')
|
||||
)
|
||||
|
||||
const { showSubscriptionDialog } = useSubscription()
|
||||
|
||||
const handleSubscribeToRun = () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ type ServerHealthAlert = {
|
||||
*/
|
||||
export type RemoteConfig = {
|
||||
mixpanel_token?: string
|
||||
require_whitelist?: boolean
|
||||
subscription_required?: boolean
|
||||
server_health_alert?: ServerHealthAlert
|
||||
}
|
||||
|
||||
@@ -146,10 +146,6 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
trackSignupOpened(): void {
|
||||
this.trackEvent(TelemetryEvents.USER_SIGN_UP_OPENED)
|
||||
}
|
||||
|
||||
trackAuth(metadata: AuthMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.USER_AUTH_COMPLETED, metadata)
|
||||
}
|
||||
|
||||
@@ -258,7 +258,6 @@ export interface WorkflowCreatedMetadata {
|
||||
*/
|
||||
export interface TelemetryProvider {
|
||||
// Authentication flow events
|
||||
trackSignupOpened(): void
|
||||
trackAuth(metadata: AuthMetadata): void
|
||||
trackUserLoggedIn(): void
|
||||
|
||||
@@ -335,7 +334,6 @@ export interface TelemetryProvider {
|
||||
*/
|
||||
export const TelemetryEvents = {
|
||||
// Authentication Flow
|
||||
USER_SIGN_UP_OPENED: 'app:user_sign_up_opened',
|
||||
USER_AUTH_COMPLETED: 'app:user_auth_completed',
|
||||
USER_LOGGED_IN: 'app:user_logged_in',
|
||||
|
||||
|
||||
@@ -20,9 +20,7 @@ export const resolveSlotTargetCandidate = (
|
||||
const { state: dragState, setCompatibleForKey } = useSlotLinkDragUIState()
|
||||
if (!(target instanceof HTMLElement)) return null
|
||||
|
||||
const elWithKey = target
|
||||
.closest('.lg-slot, .lg-node-widget')
|
||||
?.querySelector<HTMLElement>('[data-slot-key]')
|
||||
const elWithKey = target.closest<HTMLElement>('[data-slot-key]')
|
||||
const key = elWithKey?.dataset['slotKey']
|
||||
if (!key) return null
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="litegraph-minimap relative border border-interface-stroke bg-comfy-menu-bg shadow-interface"
|
||||
class="litegraph-minimap relative border border-[var(--interface-stroke)] bg-interface-panel-surface shadow-interface"
|
||||
:style="containerStyles"
|
||||
>
|
||||
<Button
|
||||
class="absolute top-1 left-1 z-10 hover:bg-interface-button-hover-surface!"
|
||||
class="absolute top-1 left-1 z-10 hover:bg-button-hover-surface!"
|
||||
size="small"
|
||||
text
|
||||
severity="secondary"
|
||||
@@ -32,7 +32,7 @@
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
class="absolute top-1 right-1 z-10 hover:bg-interface-button-hover-surface!"
|
||||
class="absolute top-1 right-1 z-10 hover:bg-button-hover-surface!"
|
||||
size="small"
|
||||
text
|
||||
severity="secondary"
|
||||
@@ -61,12 +61,11 @@
|
||||
<div class="minimap-viewport" :style="viewportStyles" />
|
||||
|
||||
<div
|
||||
class="absolute inset-0 touch-none"
|
||||
class="absolute inset-0"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
@pointerleave="handlePointerUp"
|
||||
@pointercancel="handlePointerCancel"
|
||||
@wheel="handleWheel"
|
||||
/>
|
||||
</div>
|
||||
@@ -106,7 +105,6 @@ const {
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handlePointerCancel,
|
||||
handleWheel,
|
||||
setMinimapRef
|
||||
} = useMinimap()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="minimap-panel mr-2 flex flex-col gap-2 bg-comfy-menu-bg p-3 text-sm shadow-interface"
|
||||
class="minimap-panel mr-2 flex flex-col gap-2 bg-interface-panel-surface p-3 text-sm shadow-interface"
|
||||
:style="panelStyles"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -244,7 +244,6 @@ export function useMinimap() {
|
||||
handlePointerDown: interaction.handlePointerDown,
|
||||
handlePointerMove: interaction.handlePointerMove,
|
||||
handlePointerUp: interaction.handlePointerUp,
|
||||
handlePointerCancel: interaction.handlePointerCancel,
|
||||
handleWheel: interaction.handleWheel,
|
||||
setMinimapRef,
|
||||
updateOption
|
||||
|
||||
@@ -35,10 +35,6 @@ export function useMinimapInteraction(
|
||||
const handlePointerDown = (e: PointerEvent) => {
|
||||
isDragging.value = true
|
||||
updateContainerRect()
|
||||
const target = e.currentTarget
|
||||
if (target instanceof HTMLElement) {
|
||||
target.setPointerCapture(e.pointerId)
|
||||
}
|
||||
handlePointerMove(e)
|
||||
}
|
||||
|
||||
@@ -57,23 +53,10 @@ export function useMinimapInteraction(
|
||||
centerViewOn(worldX, worldY)
|
||||
}
|
||||
|
||||
const releasePointer = (e?: PointerEvent) => {
|
||||
const handlePointerUp = () => {
|
||||
isDragging.value = false
|
||||
if (!e) return
|
||||
|
||||
const target = e.currentTarget
|
||||
if (
|
||||
target instanceof HTMLElement &&
|
||||
target.hasPointerCapture(e.pointerId)
|
||||
) {
|
||||
target.releasePointerCapture(e.pointerId)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerUp = releasePointer
|
||||
|
||||
const handlePointerCancel = releasePointer
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -119,7 +102,6 @@ export function useMinimapInteraction(
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handlePointerCancel,
|
||||
handleWheel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
:data-node-id="nodeData.id"
|
||||
:class="
|
||||
cn(
|
||||
'bg-component-node-background lg-node absolute',
|
||||
'bg-node-component-surface lg-node absolute',
|
||||
'h-min w-min contain-style contain-layout min-h-(--node-height) min-w-(--node-width)',
|
||||
'rounded-2xl touch-none flex flex-col',
|
||||
'border-1 border-solid border-component-node-border',
|
||||
'border-1 border-solid border-node-component-border',
|
||||
// hover (only when node should handle events)
|
||||
shouldHandleNodePointerEvents &&
|
||||
'hover:ring-7 ring-node-component-ring',
|
||||
@@ -23,8 +23,7 @@
|
||||
bypassed,
|
||||
'before:rounded-2xl before:pointer-events-none before:absolute before:inset-0':
|
||||
muted,
|
||||
'will-change-transform': isDragging,
|
||||
'ring-4 ring-primary-500 bg-primary-500/10': isDraggingOver
|
||||
'will-change-transform': isDragging
|
||||
},
|
||||
|
||||
shouldHandleNodePointerEvents
|
||||
@@ -37,16 +36,13 @@
|
||||
transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
||||
zIndex: zIndex,
|
||||
opacity: nodeOpacity,
|
||||
'--component-node-background': nodeBodyBackgroundColor
|
||||
'--node-component-surface': nodeBodyBackgroundColor
|
||||
},
|
||||
dragStyle
|
||||
]"
|
||||
v-bind="pointerHandlers"
|
||||
@wheel="handleWheel"
|
||||
@contextmenu="handleContextMenu"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<div class="flex flex-col justify-center items-center relative">
|
||||
<template v-if="isCollapsed">
|
||||
@@ -136,7 +132,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMouseInElement, whenever } from '@vueuse/core'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, inject, onErrorCaptured, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -387,7 +383,7 @@ const hasCustomContent = computed(() => {
|
||||
})
|
||||
|
||||
// Computed classes and conditions for better reusability
|
||||
const separatorClasses = 'bg-component-node-border h-px mx-0 w-full lod-toggle'
|
||||
const separatorClasses = 'bg-node-component-border h-px mx-0 w-full lod-toggle'
|
||||
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
|
||||
|
||||
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
|
||||
@@ -485,43 +481,4 @@ const nodeMedia = computed(() => {
|
||||
})
|
||||
|
||||
const nodeContainerRef = ref<HTMLDivElement>()
|
||||
|
||||
// Track mouse position relative to node container for drag and drop
|
||||
const { isOutside } = useMouseInElement(nodeContainerRef)
|
||||
|
||||
// Drag and drop support
|
||||
const isDraggingOver = ref(false)
|
||||
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
const node = lgraphNode.value
|
||||
if (!node || !node.onDragOver) {
|
||||
isDraggingOver.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// Call the litegraph node's onDragOver callback to check if files are valid
|
||||
const canDrop = node.onDragOver(event)
|
||||
isDraggingOver.value = canDrop
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
if (isOutside.value) {
|
||||
isDraggingOver.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = async (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
isDraggingOver.value = false
|
||||
|
||||
const node = lgraphNode.value
|
||||
if (!node || !node.onDragDrop) {
|
||||
return
|
||||
}
|
||||
|
||||
// Forward the drop event to the litegraph node's onDragDrop callback
|
||||
await node.onDragDrop(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:class="
|
||||
cn(
|
||||
'lg-node-header p-4 rounded-t-2xl w-full min-w-50',
|
||||
'text-node-component-header',
|
||||
'bg-node-component-header-surface text-node-component-header',
|
||||
collapsed && 'rounded-2xl'
|
||||
)
|
||||
"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div
|
||||
v-for="(widget, index) in processedWidgets"
|
||||
:key="`widget-${index}-${widget.name}`"
|
||||
class="lg-node-widget group flex items-stretch has-[.widget-expands]:flex-1"
|
||||
class="group flex items-stretch has-[.widget-expands]:flex-1"
|
||||
>
|
||||
<!-- Widget Input Slot Dot -->
|
||||
|
||||
|
||||
@@ -301,12 +301,14 @@ export function useSlotLinkInteraction({
|
||||
hoveredSlotKey = dragContext.lastPointerTargetSlotKey
|
||||
hoveredNodeId = dragContext.lastPointerTargetNodeId
|
||||
} else if (target instanceof HTMLElement) {
|
||||
const elWithSlot = target
|
||||
.closest('.lg-slot, .lg-node-widget')
|
||||
?.querySelector<HTMLElement>('[data-slot-key]')
|
||||
const elWithNode = target.closest<HTMLElement>('[data-node-id]')
|
||||
const elWithSlot = target.closest<HTMLElement>('[data-slot-key]')
|
||||
const elWithNode = elWithSlot
|
||||
? null
|
||||
: target.closest<HTMLElement>('[data-node-id]')
|
||||
hoveredSlotKey = elWithSlot?.dataset['slotKey'] ?? null
|
||||
hoveredNodeId = elWithNode?.dataset['nodeId'] ?? null
|
||||
hoveredNodeId = hoveredSlotKey
|
||||
? null
|
||||
: (elWithNode?.dataset['nodeId'] ?? null)
|
||||
dragContext.lastPointerEventTarget = target
|
||||
dragContext.lastPointerTargetSlotKey = hoveredSlotKey
|
||||
dragContext.lastPointerTargetNodeId = hoveredNodeId
|
||||
@@ -323,8 +325,10 @@ export function useSlotLinkInteraction({
|
||||
const graph = app.canvas?.graph ?? null
|
||||
const context = { adapter, graph, session: dragContext }
|
||||
const slotCandidate = resolveSlotTargetCandidate(target, context)
|
||||
const nodeCandidate = resolveNodeSurfaceSlotCandidate(target, context)
|
||||
candidate = slotCandidate?.compatible ? slotCandidate : nodeCandidate
|
||||
const nodeCandidate = slotCandidate
|
||||
? null
|
||||
: resolveNodeSurfaceSlotCandidate(target, context)
|
||||
candidate = slotCandidate ?? nodeCandidate
|
||||
dragContext.lastHoverSlotKey = hoveredSlotKey
|
||||
dragContext.lastHoverNodeId = hoveredNodeId
|
||||
|
||||
@@ -335,8 +339,7 @@ export function useSlotLinkInteraction({
|
||||
slotCandidate.layout.type === 'input'
|
||||
)
|
||||
setCompatibleForKey(key, !!slotCandidate.compatible)
|
||||
}
|
||||
if (nodeCandidate && !slotCandidate?.compatible) {
|
||||
} else if (nodeCandidate) {
|
||||
const key = getSlotKey(
|
||||
nodeCandidate.layout.nodeId,
|
||||
nodeCandidate.layout.index,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
size="small"
|
||||
:pt="{
|
||||
option: 'text-xs',
|
||||
dropdownIcon: 'text-component-node-foreground-secondary'
|
||||
dropdownIcon: 'text-button-icon'
|
||||
}"
|
||||
/>
|
||||
<Button
|
||||
@@ -97,7 +97,7 @@
|
||||
size="small"
|
||||
:pt="{
|
||||
option: 'text-xs',
|
||||
dropdownIcon: 'text-component-node-foreground-secondary'
|
||||
dropdownIcon: 'text-button-icon'
|
||||
}"
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -103,14 +103,10 @@ const inputNumberPt = useNumberWidgetButtonPt({
|
||||
@update:model-value="onChange"
|
||||
>
|
||||
<template #incrementicon>
|
||||
<span
|
||||
class="pi pi-plus text-sm text-component-node-foreground-secondary"
|
||||
/>
|
||||
<span class="pi pi-plus text-sm text-button-icon" />
|
||||
</template>
|
||||
<template #decrementicon>
|
||||
<span
|
||||
class="pi pi-minus text-sm text-component-node-foreground-secondary"
|
||||
/>
|
||||
<span class="pi pi-minus text-sm text-button-icon" />
|
||||
</template>
|
||||
</InputNumber>
|
||||
</div>
|
||||
@@ -120,7 +116,7 @@ const inputNumberPt = useNumberWidgetButtonPt({
|
||||
<style scoped>
|
||||
:deep(.p-inputnumber-input) {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--component-node-border);
|
||||
border: 1px solid var(--node-stroke);
|
||||
border-top: transparent;
|
||||
border-bottom: transparent;
|
||||
height: 1.625rem;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
display="chip"
|
||||
:pt="{
|
||||
option: 'text-xs',
|
||||
dropdownIcon: 'text-component-node-foreground-secondary'
|
||||
dropdownIcon: 'text-button-icon'
|
||||
}"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
|
||||
@@ -107,14 +107,10 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
const selectedButton = buttons[1] // 'banana'
|
||||
const unselectedButton = buttons[0] // 'apple'
|
||||
|
||||
expect(selectedButton.classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(selectedButton.classes()).toContain('text-primary')
|
||||
expect(unselectedButton.classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(unselectedButton.classes()).toContain('text-secondary')
|
||||
expect(selectedButton.classes()).toContain('bg-white')
|
||||
expect(selectedButton.classes()).toContain('text-neutral-900')
|
||||
expect(unselectedButton.classes()).not.toContain('bg-white')
|
||||
expect(unselectedButton.classes()).not.toContain('text-neutral-900')
|
||||
})
|
||||
|
||||
it('handles no selection gracefully', () => {
|
||||
@@ -124,10 +120,8 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
buttons.forEach((button) => {
|
||||
expect(button.classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(button.classes()).toContain('text-secondary')
|
||||
expect(button.classes()).not.toContain('bg-white')
|
||||
expect(button.classes()).not.toContain('text-neutral-900')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -140,19 +134,13 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
// Initially 'first' is selected
|
||||
let buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
|
||||
// Update to 'second'
|
||||
await wrapper.setProps({ modelValue: 'second' })
|
||||
buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[1].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[0].classes()).not.toContain('bg-white')
|
||||
expect(buttons[1].classes()).toContain('bg-white')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -234,9 +222,7 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
expect(buttons[2].text()).toBe('3')
|
||||
|
||||
// The selected button should be the one with '2'
|
||||
expect(buttons[1].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[1].classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('handles object options with label and value', () => {
|
||||
@@ -254,9 +240,7 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
expect(buttons[2].text()).toBe('Third Option')
|
||||
|
||||
// 'second' should be selected
|
||||
expect(buttons[1].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[1].classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('emits correct values for object options', async () => {
|
||||
@@ -293,9 +277,7 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(4)
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
) // Empty string is selected
|
||||
expect(buttons[0].classes()).toContain('bg-white') // Empty string is selected
|
||||
})
|
||||
|
||||
it('handles null/undefined in options', () => {
|
||||
@@ -310,9 +292,7 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(4)
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('handles very long option text', () => {
|
||||
@@ -333,9 +313,7 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(20)
|
||||
expect(buttons[4].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
) // option5 is at index 4
|
||||
expect(buttons[4].classes()).toContain('bg-white') // option5 is at index 4
|
||||
})
|
||||
|
||||
it('handles duplicate options', () => {
|
||||
@@ -346,12 +324,8 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(4)
|
||||
// Both 'duplicate' buttons should be highlighted (due to value matching)
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[2].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
expect(buttons[2].classes()).toContain('bg-white')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -380,9 +354,7 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
const buttons = wrapper.findAll('button')
|
||||
const unselectedButton = buttons[1] // 'option2'
|
||||
|
||||
expect(unselectedButton.classes()).toContain(
|
||||
'hover:bg-interface-menu-component-surface-hovered'
|
||||
)
|
||||
expect(unselectedButton.classes()).toContain('hover:bg-zinc-200/50')
|
||||
expect(unselectedButton.classes()).toContain('cursor-pointer')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
size="small"
|
||||
:pt="{
|
||||
option: 'text-xs',
|
||||
dropdownIcon: 'text-component-node-foreground-secondary'
|
||||
dropdownIcon: 'text-button-icon'
|
||||
}"
|
||||
data-capture-wheel="true"
|
||||
@update:model-value="onChange"
|
||||
|
||||
@@ -44,7 +44,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { localValue, onChange } = useWidgetValue({
|
||||
widget: props.widget,
|
||||
modelValue: () => props.modelValue,
|
||||
modelValue: props.modelValue,
|
||||
defaultValue: props.widget.options?.values?.[0] || '',
|
||||
emit
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:aria-label="widget.name"
|
||||
size="small"
|
||||
:pt="{
|
||||
dropdownIcon: 'text-component-node-foreground-secondary'
|
||||
dropdownIcon: 'text-button-icon'
|
||||
}"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
v-if="!hidden"
|
||||
:class="
|
||||
cn(
|
||||
'bg-component-node-widget-background box-border flex gap-4 items-center justify-start relative rounded-lg w-full h-16 px-4 py-0',
|
||||
'bg-zinc-500/10 dark-theme:bg-charcoal-600 box-border flex gap-4 items-center justify-start relative rounded-lg w-full h-16 px-4 py-0',
|
||||
{ hidden: hideWhenEmpty && !hasAudio }
|
||||
)
|
||||
"
|
||||
@@ -24,14 +24,17 @@
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
aria-label="Play/Pause"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-interface-menu-component-surface-hovered"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-black/10 dark-theme:hover:bg-white/10"
|
||||
@click="togglePlayPause"
|
||||
>
|
||||
<i
|
||||
v-if="!isPlaying"
|
||||
class="text-secondary icon-[lucide--play] size-4"
|
||||
class="icon-[lucide--play] size-4 text-smoke-600 dark-theme:text-smoke-800"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="icon-[lucide--pause] size-4 text-smoke-600 dark-theme:text-smoke-800"
|
||||
/>
|
||||
<i v-else class="text-secondary icon-[lucide--pause] size-4" />
|
||||
</div>
|
||||
|
||||
<!-- Time Display -->
|
||||
@@ -41,9 +44,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="relative h-0.5 flex-1 rounded-full bg-interface-stroke">
|
||||
<div
|
||||
class="relative h-0.5 flex-1 rounded-full bg-smoke-300 dark-theme:bg-ash-800"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 left-0 h-full rounded-full bg-button-icon transition-all"
|
||||
class="absolute top-0 left-0 h-full rounded-full bg-smoke-600 transition-all dark-theme:bg-white/50"
|
||||
:style="{ width: `${progressPercentage}%` }"
|
||||
/>
|
||||
<input
|
||||
@@ -65,18 +70,21 @@
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
aria-label="Volume"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-interface-menu-component-surface-hovered"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-black/10 dark-theme:hover:bg-white/10"
|
||||
@click="toggleMute"
|
||||
>
|
||||
<i
|
||||
v-if="showVolumeTwo"
|
||||
class="text-secondary icon-[lucide--volume-2] size-4"
|
||||
class="icon-[lucide--volume-2] size-4 text-smoke-600 dark-theme:text-smoke-800"
|
||||
/>
|
||||
<i
|
||||
v-else-if="showVolumeOne"
|
||||
class="text-secondary icon-[lucide--volume-1] size-4"
|
||||
class="icon-[lucide--volume-1] size-4 text-smoke-600 dark-theme:text-smoke-800"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="icon-[lucide--volume-x] size-4 text-smoke-600 dark-theme:text-smoke-800"
|
||||
/>
|
||||
<i v-else class="text-secondary icon-[lucide--volume-x] size-4" />
|
||||
</div>
|
||||
|
||||
<!-- Options Button -->
|
||||
@@ -86,10 +94,12 @@
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
aria-label="More Options"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-interface-menu-component-surface-hovered"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-black/10 dark-theme:hover:bg-white/10"
|
||||
@click="toggleOptionsMenu"
|
||||
>
|
||||
<i class="text-secondary icon-[lucide--more-vertical] size-4" />
|
||||
<i
|
||||
class="icon-[lucide--more-vertical] size-4 text-smoke-600 dark-theme:text-smoke-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -124,16 +124,10 @@ describe('FormSelectButton Core Component', () => {
|
||||
const wrapper = mountComponent('option2', options)
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[1].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[1].classes()).toContain('text-primary')
|
||||
expect(buttons[0].classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[2].classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[1].classes()).toContain('bg-white')
|
||||
expect(buttons[1].classes()).toContain('text-neutral-900')
|
||||
expect(buttons[0].classes()).not.toContain('bg-white')
|
||||
expect(buttons[2].classes()).not.toContain('bg-white')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -165,10 +159,8 @@ describe('FormSelectButton Core Component', () => {
|
||||
const wrapper = mountComponent('200', options)
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[1].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[1].classes()).toContain('text-primary')
|
||||
expect(buttons[1].classes()).toContain('bg-white')
|
||||
expect(buttons[1].classes()).toContain('text-neutral-900')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -209,15 +201,9 @@ describe('FormSelectButton Core Component', () => {
|
||||
const wrapper = mountComponent('md', options)
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[1].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
) // Medium
|
||||
expect(buttons[0].classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[2].classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[1].classes()).toContain('bg-white') // Medium
|
||||
expect(buttons[0].classes()).not.toContain('bg-white')
|
||||
expect(buttons[2].classes()).not.toContain('bg-white')
|
||||
})
|
||||
|
||||
it('handles objects without value field', () => {
|
||||
@@ -230,9 +216,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].text()).toBe('First')
|
||||
expect(buttons[1].text()).toBe('Second')
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('handles objects without label field', () => {
|
||||
@@ -269,12 +253,8 @@ describe('FormSelectButton Core Component', () => {
|
||||
const wrapper = mountComponent('first_id', options, { optionValue: 'id' })
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[1].classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
expect(buttons[1].classes()).not.toContain('bg-white')
|
||||
})
|
||||
|
||||
it('emits custom optionValue when clicked', async () => {
|
||||
@@ -321,9 +301,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
buttons.forEach((button) => {
|
||||
expect(button.classes()).not.toContain(
|
||||
'hover:bg-interface-menu-component-surface-hovered'
|
||||
)
|
||||
expect(button.classes()).not.toContain('hover:bg-zinc-200/50')
|
||||
expect(button.classes()).not.toContain('cursor-pointer')
|
||||
})
|
||||
})
|
||||
@@ -333,9 +311,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
const wrapper = mountComponent('option1', options, { disabled: true })
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
) // Selected styling disabled
|
||||
expect(buttons[0].classes()).not.toContain('bg-white') // Selected styling disabled
|
||||
expect(buttons[0].classes()).toContain('opacity-50')
|
||||
expect(buttons[0].classes()).toContain('text-secondary')
|
||||
})
|
||||
@@ -348,9 +324,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
buttons.forEach((button) => {
|
||||
expect(button.classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(button.classes()).not.toContain('bg-white')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -360,9 +334,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
buttons.forEach((button) => {
|
||||
expect(button.classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(button.classes()).not.toContain('bg-white')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -371,12 +343,8 @@ describe('FormSelectButton Core Component', () => {
|
||||
const wrapper = mountComponent('', options)
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
) // Empty string is selected
|
||||
expect(buttons[1].classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[0].classes()).toContain('bg-white') // Empty string is selected
|
||||
expect(buttons[1].classes()).not.toContain('bg-white')
|
||||
})
|
||||
|
||||
it('compares values as strings', () => {
|
||||
@@ -384,9 +352,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
const wrapper = mountComponent('1', options)
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
) // '1' matches number 1 as string
|
||||
expect(buttons[0].classes()).toContain('bg-white') // '1' matches number 1 as string
|
||||
})
|
||||
})
|
||||
|
||||
@@ -396,10 +362,8 @@ describe('FormSelectButton Core Component', () => {
|
||||
const wrapper = mountComponent('option1', options)
|
||||
|
||||
const selectedButton = wrapper.findAll('button')[0]
|
||||
expect(selectedButton.classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(selectedButton.classes()).toContain('text-primary')
|
||||
expect(selectedButton.classes()).toContain('bg-white')
|
||||
expect(selectedButton.classes()).toContain('text-neutral-900')
|
||||
})
|
||||
|
||||
it('applies unselected styling to inactive options', () => {
|
||||
@@ -416,9 +380,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
const wrapper = mountComponent('option1', options, { disabled: false })
|
||||
|
||||
const unselectedButton = wrapper.findAll('button')[1]
|
||||
expect(unselectedButton.classes()).toContain(
|
||||
'hover:bg-interface-menu-component-surface-hovered'
|
||||
)
|
||||
expect(unselectedButton.classes()).toContain('hover:bg-zinc-200/50')
|
||||
expect(unselectedButton.classes()).toContain('cursor-pointer')
|
||||
})
|
||||
})
|
||||
@@ -441,9 +403,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].text()).toBe('@#$%^&*()')
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('handles unicode characters in options', () => {
|
||||
@@ -452,9 +412,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].text()).toBe('🎨 Art')
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('handles duplicate option values', () => {
|
||||
@@ -462,15 +420,9 @@ describe('FormSelectButton Core Component', () => {
|
||||
const wrapper = mountComponent('duplicate', duplicateOptions)
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[2].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
) // Both duplicates selected
|
||||
expect(buttons[1].classes()).not.toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[0].classes()).toContain('bg-white')
|
||||
expect(buttons[2].classes()).toContain('bg-white') // Both duplicates selected
|
||||
expect(buttons[1].classes()).not.toContain('bg-white')
|
||||
})
|
||||
|
||||
it('handles mixed type options safely', () => {
|
||||
@@ -484,9 +436,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(4)
|
||||
expect(buttons[1].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
) // Number 123 as string
|
||||
expect(buttons[1].classes()).toContain('bg-white') // Number 123 as string
|
||||
})
|
||||
|
||||
it('handles objects with missing properties gracefully', () => {
|
||||
@@ -500,9 +450,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(4)
|
||||
expect(buttons[2].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
expect(buttons[2].classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('handles large number of options', () => {
|
||||
@@ -514,9 +462,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(50)
|
||||
expect(buttons[24].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
) // Option 25 at index 24
|
||||
expect(buttons[24].classes()).toContain('bg-white') // Option 25 at index 24
|
||||
})
|
||||
|
||||
it('fallback to index when all object properties are missing', () => {
|
||||
@@ -528,9 +474,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
expect(buttons).toHaveLength(2)
|
||||
expect(buttons[0].classes()).toContain(
|
||||
'bg-interface-menu-component-surface-selected'
|
||||
) // Falls back to index 0
|
||||
expect(buttons[0].classes()).toContain('bg-white') // Falls back to index 0
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -15,12 +15,15 @@
|
||||
'flex-1 h-6 px-5 py-[5px] rounded flex justify-center items-center gap-1 transition-all duration-150 ease-in-out',
|
||||
'bg-transparent border-none',
|
||||
'text-center text-xs font-normal',
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
||||
!disabled &&
|
||||
(isSelected(option)
|
||||
? 'bg-interface-menu-component-surface-selected'
|
||||
: 'hover:bg-interface-menu-component-surface-hovered'),
|
||||
isSelected(option) && !disabled ? 'text-primary' : 'text-secondary'
|
||||
{
|
||||
'bg-white': isSelected(option) && !disabled,
|
||||
'hover:bg-zinc-200/50': !isSelected(option) && !disabled,
|
||||
'opacity-50 cursor-not-allowed': disabled,
|
||||
'cursor-pointer': !disabled
|
||||
},
|
||||
isSelected(option) && !disabled
|
||||
? 'text-neutral-900'
|
||||
: 'text-secondary'
|
||||
)
|
||||
"
|
||||
:disabled="disabled"
|
||||
|
||||
@@ -34,7 +34,7 @@ const selectedItems = computed(() => {
|
||||
|
||||
const chevronClass = computed(() =>
|
||||
cn(
|
||||
'mr-2 size-4 transition-transform duration-200 flex-shrink-0 text-component-node-foreground-secondary',
|
||||
'mr-2 size-4 transition-transform duration-200 flex-shrink-0 text-button-icon',
|
||||
{
|
||||
'rotate-180': props.isOpen
|
||||
}
|
||||
@@ -42,15 +42,12 @@ const chevronClass = computed(() =>
|
||||
)
|
||||
|
||||
const theButtonStyle = computed(() =>
|
||||
cn(
|
||||
'border-0 bg-component-node-widget-background outline-none text-text-secondary',
|
||||
{
|
||||
'hover:bg-component-node-widget-background-hovered cursor-pointer':
|
||||
!props.disabled,
|
||||
'cursor-not-allowed': props.disabled,
|
||||
'text-text-primary': selectedItems.value.length > 0
|
||||
}
|
||||
)
|
||||
cn('bg-transparent border-0 outline-none text-text-secondary', {
|
||||
'hover:bg-node-component-widget-input-surface/30 cursor-pointer':
|
||||
!props.disabled,
|
||||
'cursor-not-allowed': props.disabled,
|
||||
'text-text-primary': selectedItems.value.length > 0
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const filterSelected = defineModel<OptionId>('filterSelected')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-secondary mb-4 flex gap-1 px-4">
|
||||
<div class="mb-4 flex gap-1 px-4 text-zinc-400">
|
||||
<div
|
||||
v-for="option in filterOptions"
|
||||
:key="option.id"
|
||||
@@ -19,10 +19,10 @@ const filterSelected = defineModel<OptionId>('filterSelected')
|
||||
cn(
|
||||
'px-4 py-2 rounded-md inline-flex justify-center items-center cursor-pointer select-none',
|
||||
'transition-all duration-150',
|
||||
'hover:text-base-foreground hover:bg-interface-menu-component-surface-hovered',
|
||||
'hover:text-base-foreground hover:bg-zinc-500/10',
|
||||
'active:scale-95',
|
||||
filterSelected === option.id
|
||||
? '!bg-interface-menu-component-surface-selected text-base-foreground'
|
||||
? '!bg-zinc-500/20 text-base-foreground'
|
||||
: 'bg-transparent'
|
||||
)
|
||||
"
|
||||
|
||||
@@ -60,9 +60,9 @@ function handleVideoLoad(event: Event) {
|
||||
'transition-all duration-150',
|
||||
{
|
||||
'flex-col text-center': layout === 'grid',
|
||||
'flex-row text-left max-h-16 bg-interface-menu-component-surface-hovered rounded-lg hover:scale-102 active:scale-98':
|
||||
'flex-row text-left max-h-16 bg-zinc-500/20 rounded-lg hover:scale-102 active:scale-98':
|
||||
layout === 'list',
|
||||
'flex-row text-left hover:bg-interface-menu-component-surface-hovered rounded-lg':
|
||||
'flex-row text-left hover:bg-zinc-500/20 rounded-lg':
|
||||
layout === 'list-small',
|
||||
// selection
|
||||
'ring-2 ring-blue-500': layout === 'list' && selected
|
||||
@@ -77,7 +77,7 @@ function handleVideoLoad(event: Event) {
|
||||
:class="
|
||||
cn(
|
||||
'relative',
|
||||
'w-full aspect-square overflow-hidden outline-1 outline-offset-[-1px] outline-interface-stroke',
|
||||
'w-full aspect-square overflow-hidden outline-1 outline-offset-[-1px] outline-zinc-300/10',
|
||||
'transition-all duration-150',
|
||||
{
|
||||
'min-w-16 max-w-16 rounded-l-lg': layout === 'list',
|
||||
@@ -142,7 +142,7 @@ function handleVideoLoad(event: Event) {
|
||||
{{ name }}
|
||||
</span>
|
||||
<!-- Meta Data -->
|
||||
<span class="text-secondary block text-xs">{{
|
||||
<span class="block text-xs text-slate-400">{{
|
||||
metadata || actualDimensions
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@ import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
export const WidgetInputBaseClass = cn([
|
||||
// Background
|
||||
'not-disabled:bg-component-node-widget-background',
|
||||
'not-disabled:text-component-node-foreground',
|
||||
'not-disabled:bg-node-component-widget-input-surface',
|
||||
'not-disabled:text-node-component-widget-input',
|
||||
// Outline
|
||||
'border-none',
|
||||
'outline outline-offset-[-1px] outline-component-node-border',
|
||||
'outline outline-offset-[-1px] outline-node-stroke',
|
||||
// Rounded
|
||||
'rounded-lg',
|
||||
// Hover
|
||||
'hover:bg-component-node-widget-background-hovered'
|
||||
'hover:bg-node-component-surface-hovered'
|
||||
])
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
assetItemSchema
|
||||
} from '@/platform/assets/schemas/assetSchema'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import { isComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
@@ -23,8 +22,6 @@ import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
import { addValueControlWidgets } from '@/scripts/widgets'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
|
||||
import { useRemoteWidget } from './useRemoteWidget'
|
||||
|
||||
@@ -35,20 +32,6 @@ const getDefaultValue = (inputSpec: ComboInputSpec) => {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Map node types to expected media types
|
||||
const NODE_MEDIA_TYPE_MAP: Record<string, 'image' | 'video' | 'audio'> = {
|
||||
LoadImage: 'image',
|
||||
LoadVideo: 'video',
|
||||
LoadAudio: 'audio'
|
||||
}
|
||||
|
||||
// Map node types to placeholder i18n keys
|
||||
const NODE_PLACEHOLDER_MAP: Record<string, string> = {
|
||||
LoadImage: 'widgets.uploadSelect.placeholderImage',
|
||||
LoadVideo: 'widgets.uploadSelect.placeholderVideo',
|
||||
LoadAudio: 'widgets.uploadSelect.placeholderAudio'
|
||||
}
|
||||
|
||||
const addMultiSelectWidget = (
|
||||
node: LGraphNode,
|
||||
inputSpec: ComboInputSpec
|
||||
@@ -72,168 +55,87 @@ const addMultiSelectWidget = (
|
||||
return widget
|
||||
}
|
||||
|
||||
const createAssetBrowserWidget = (
|
||||
node: LGraphNode,
|
||||
inputSpec: ComboInputSpec,
|
||||
defaultValue: string | undefined
|
||||
): IBaseWidget => {
|
||||
const currentValue = defaultValue
|
||||
const displayLabel = currentValue ?? t('widgets.selectModel')
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
|
||||
const widget = node.addWidget(
|
||||
'asset',
|
||||
inputSpec.name,
|
||||
displayLabel,
|
||||
async function (this: IBaseWidget) {
|
||||
if (!isAssetWidget(widget)) {
|
||||
throw new Error(`Expected asset widget but received ${widget.type}`)
|
||||
}
|
||||
await assetBrowserDialog.show({
|
||||
nodeType: node.comfyClass || '',
|
||||
inputName: inputSpec.name,
|
||||
currentValue: widget.value,
|
||||
onAssetSelected: (asset) => {
|
||||
const validatedAsset = assetItemSchema.safeParse(asset)
|
||||
|
||||
if (!validatedAsset.success) {
|
||||
console.error(
|
||||
'Invalid asset item:',
|
||||
validatedAsset.error.errors,
|
||||
'Received:',
|
||||
asset
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const filename = validatedAsset.data.user_metadata?.filename
|
||||
const validatedFilename = assetFilenameSchema.safeParse(filename)
|
||||
|
||||
if (!validatedFilename.success) {
|
||||
console.error(
|
||||
'Invalid asset filename:',
|
||||
validatedFilename.error.errors,
|
||||
'for asset:',
|
||||
validatedAsset.data.id
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const oldValue = widget.value
|
||||
this.value = validatedFilename.data
|
||||
node.onWidgetChanged?.(
|
||||
widget.name,
|
||||
validatedFilename.data,
|
||||
oldValue,
|
||||
widget
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
const createInputMappingWidget = (
|
||||
node: LGraphNode,
|
||||
inputSpec: ComboInputSpec,
|
||||
defaultValue: string | undefined
|
||||
): IBaseWidget => {
|
||||
const assetsStore = useAssetsStore()
|
||||
|
||||
const widget = node.addWidget(
|
||||
'combo',
|
||||
inputSpec.name,
|
||||
defaultValue ?? '',
|
||||
() => {},
|
||||
{
|
||||
values: [],
|
||||
getOptionLabel: (value?: string | null) => {
|
||||
if (!value) {
|
||||
const placeholderKey =
|
||||
NODE_PLACEHOLDER_MAP[node.comfyClass ?? ''] ??
|
||||
'widgets.uploadSelect.placeholder'
|
||||
return t(placeholderKey)
|
||||
}
|
||||
return assetsStore.getInputName(value)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (assetsStore.inputAssets.length === 0 && !assetsStore.inputLoading) {
|
||||
void assetsStore.updateInputs().then(() => {
|
||||
// edge for users using nodes with 0 prior inputs
|
||||
// force canvas refresh the first time they add an asset
|
||||
// so they see filenames instead of hashes.
|
||||
node.setDirtyCanvas(true, false)
|
||||
})
|
||||
}
|
||||
|
||||
const origOptions = widget.options
|
||||
widget.options = new Proxy(origOptions, {
|
||||
get(target, prop) {
|
||||
if (prop !== 'values') {
|
||||
return target[prop as keyof typeof target]
|
||||
}
|
||||
return assetsStore.inputAssets
|
||||
.filter(
|
||||
(asset) =>
|
||||
getMediaTypeFromFilename(asset.name) ===
|
||||
NODE_MEDIA_TYPE_MAP[node.comfyClass ?? '']
|
||||
)
|
||||
.map((asset) => asset.asset_hash)
|
||||
.filter((hash): hash is string => !!hash)
|
||||
}
|
||||
})
|
||||
|
||||
if (inputSpec.control_after_generate) {
|
||||
if (!isComboWidget(widget)) {
|
||||
throw new Error(`Expected combo widget but received ${widget.type}`)
|
||||
}
|
||||
widget.linkedWidgets = addValueControlWidgets(
|
||||
node,
|
||||
widget,
|
||||
undefined,
|
||||
undefined,
|
||||
transformInputSpecV2ToV1(inputSpec)
|
||||
)
|
||||
}
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
const addComboWidget = (
|
||||
node: LGraphNode,
|
||||
inputSpec: ComboInputSpec
|
||||
): IBaseWidget => {
|
||||
const defaultValue = getDefaultValue(inputSpec)
|
||||
const settingStore = useSettingStore()
|
||||
const isUsingAssetAPI = settingStore.get('Comfy.Assets.UseAssetAPI')
|
||||
const isEligible = assetService.isAssetBrowserEligible(
|
||||
node.comfyClass,
|
||||
inputSpec.name
|
||||
)
|
||||
|
||||
if (isCloud) {
|
||||
const settingStore = useSettingStore()
|
||||
const isUsingAssetAPI = settingStore.get('Comfy.Assets.UseAssetAPI')
|
||||
const isEligible = assetService.isAssetBrowserEligible(
|
||||
node.comfyClass,
|
||||
inputSpec.name
|
||||
if (isUsingAssetAPI && isEligible) {
|
||||
const currentValue = getDefaultValue(inputSpec)
|
||||
const displayLabel = currentValue ?? t('widgets.selectModel')
|
||||
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
|
||||
const widget = node.addWidget(
|
||||
'asset',
|
||||
inputSpec.name,
|
||||
displayLabel,
|
||||
async function (this: IBaseWidget) {
|
||||
if (!isAssetWidget(widget)) {
|
||||
throw new Error(`Expected asset widget but received ${widget.type}`)
|
||||
}
|
||||
await assetBrowserDialog.show({
|
||||
nodeType: node.comfyClass || '',
|
||||
inputName: inputSpec.name,
|
||||
currentValue: widget.value,
|
||||
onAssetSelected: (asset) => {
|
||||
const validatedAsset = assetItemSchema.safeParse(asset)
|
||||
|
||||
if (!validatedAsset.success) {
|
||||
console.error(
|
||||
'Invalid asset item:',
|
||||
validatedAsset.error.errors,
|
||||
'Received:',
|
||||
asset
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const filename = validatedAsset.data.user_metadata?.filename
|
||||
const validatedFilename = assetFilenameSchema.safeParse(filename)
|
||||
|
||||
if (!validatedFilename.success) {
|
||||
console.error(
|
||||
'Invalid asset filename:',
|
||||
validatedFilename.error.errors,
|
||||
'for asset:',
|
||||
validatedAsset.data.id
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const oldValue = widget.value
|
||||
this.value = validatedFilename.data
|
||||
node.onWidgetChanged?.(
|
||||
widget.name,
|
||||
validatedFilename.data,
|
||||
oldValue,
|
||||
widget
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (isUsingAssetAPI && isEligible) {
|
||||
return createAssetBrowserWidget(node, inputSpec, defaultValue)
|
||||
}
|
||||
|
||||
if (NODE_MEDIA_TYPE_MAP[node.comfyClass ?? '']) {
|
||||
return createInputMappingWidget(node, inputSpec, defaultValue)
|
||||
}
|
||||
return widget
|
||||
}
|
||||
|
||||
// Standard combo widget
|
||||
// Create normal combo widget
|
||||
const defaultValue = getDefaultValue(inputSpec)
|
||||
const comboOptions = inputSpec.options ?? []
|
||||
const widget = node.addWidget(
|
||||
'combo',
|
||||
inputSpec.name,
|
||||
defaultValue,
|
||||
() => {},
|
||||
{
|
||||
values: inputSpec.options ?? []
|
||||
values: comboOptions
|
||||
}
|
||||
)
|
||||
|
||||
@@ -241,7 +143,6 @@ const addComboWidget = (
|
||||
if (!isComboWidget(widget)) {
|
||||
throw new Error(`Expected combo widget but received ${widget.type}`)
|
||||
}
|
||||
|
||||
const remoteWidget = useRemoteWidget({
|
||||
remoteConfig: inputSpec.remote,
|
||||
defaultValue,
|
||||
@@ -265,7 +166,6 @@ const addComboWidget = (
|
||||
if (!isComboWidget(widget)) {
|
||||
throw new Error(`Expected combo widget but received ${widget.type}`)
|
||||
}
|
||||
|
||||
widget.linkedWidgets = addValueControlWidgets(
|
||||
node,
|
||||
widget,
|
||||
|
||||
@@ -78,13 +78,9 @@ export const useImageUploadWidget = () => {
|
||||
folder,
|
||||
onUploadComplete: (output) => {
|
||||
output.forEach((path) => addToComboValues(fileComboWidget, path))
|
||||
|
||||
// Create a NEW array to ensure Vue reactivity detects the change
|
||||
const newValue = allow_batch ? [...output] : output[0]
|
||||
|
||||
// @ts-expect-error litegraph combo value type does not support arrays yet
|
||||
fileComboWidget.value = newValue
|
||||
fileComboWidget.callback?.(newValue)
|
||||
fileComboWidget.value = output
|
||||
fileComboWidget.callback?.(output)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
demoteWidget,
|
||||
promoteRecommendedWidgets
|
||||
} from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
} from '@/renderer/graph/subgraph/proxyWidgetUtils'
|
||||
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type {
|
||||
@@ -3,7 +3,7 @@ import type { ProxyWidgetsProperty } from '@/core/schemas/proxyWidget'
|
||||
import {
|
||||
isProxyWidget,
|
||||
isDisconnectedWidget
|
||||
} from '@/core/graph/subgraph/proxyWidget'
|
||||
} from '@/renderer/graph/subgraph/proxyWidget'
|
||||
import { t } from '@/i18n'
|
||||
import type {
|
||||
IContextMenuValue,
|
||||
@@ -14,6 +14,7 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets.ts'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { registerWidgetPromotionHandlers } from '@/services/widgetPromotionHandlers'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
|
||||
type PartialNode = Pick<LGraphNode, 'title' | 'id' | 'type'>
|
||||
@@ -86,7 +87,7 @@ function getParentNodes(): SubgraphNode[] {
|
||||
)
|
||||
}
|
||||
|
||||
export function addWidgetPromotionOptions(
|
||||
function addWidgetPromotionOptions(
|
||||
options: (IContextMenuValue<unknown> | null)[],
|
||||
widget: IBaseWidget,
|
||||
node: LGraphNode
|
||||
@@ -111,7 +112,7 @@ export function addWidgetPromotionOptions(
|
||||
})
|
||||
}
|
||||
}
|
||||
export function tryToggleWidgetPromotion() {
|
||||
function tryToggleWidgetPromotion() {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
const [x, y] = canvas.graph_mouse
|
||||
const node = canvas.graph?.getNodeOnPos(x, y, canvas.visible_nodes)
|
||||
@@ -174,3 +175,8 @@ export function pruneDisconnected(subgraphNode: SubgraphNode) {
|
||||
.filter((w) => !isDisconnectedWidget(w))
|
||||
.map((w) => [w._overlay.nodeId, w._overlay.widgetName])
|
||||
}
|
||||
|
||||
registerWidgetPromotionHandlers({
|
||||
addWidgetPromotionOptions,
|
||||
tryToggleWidgetPromotion
|
||||
})
|
||||
103
src/router.ts
@@ -5,7 +5,6 @@ import {
|
||||
createWebHashHistory,
|
||||
createWebHistory
|
||||
} from 'vue-router'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
@@ -14,20 +13,8 @@ import { useUserStore } from '@/stores/userStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
|
||||
|
||||
import { cloudOnboardingRoutes } from './platform/cloud/onboarding/onboardingCloudRoutes'
|
||||
|
||||
const isFileProtocol = window.location.protocol === 'file:'
|
||||
|
||||
// Determine base path for the router
|
||||
// - Electron: always root
|
||||
// - Web: rely on Vite's BASE_URL (configured via vite.config `base`)
|
||||
function getBasePath(): string {
|
||||
if (isElectron()) return '/'
|
||||
// Vite injects BASE_URL at build/dev time; default to '/'
|
||||
return import.meta.env?.BASE_URL || '/'
|
||||
}
|
||||
|
||||
const basePath = getBasePath()
|
||||
const basePath = isElectron() ? '/' : window.location.pathname
|
||||
|
||||
const router = createRouter({
|
||||
history: isFileProtocol
|
||||
@@ -37,7 +24,6 @@ const router = createRouter({
|
||||
// we need this base path or assets will incorrectly resolve from 'http://localhost:7801/'
|
||||
createWebHistory(basePath),
|
||||
routes: [
|
||||
...(isCloud ? cloudOnboardingRoutes : []),
|
||||
{
|
||||
path: '/',
|
||||
component: LayoutDefault,
|
||||
@@ -47,7 +33,6 @@ const router = createRouter({
|
||||
name: 'GraphView',
|
||||
component: () => import('@/views/GraphView.vue'),
|
||||
beforeEnter: async (_to, _from, next) => {
|
||||
// Then check user store
|
||||
const userStore = useUserStore()
|
||||
await userStore.initialize()
|
||||
if (userStore.needsLogin) {
|
||||
@@ -76,27 +61,8 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
if (isCloud) {
|
||||
const PUBLIC_ROUTE_NAMES = new Set([
|
||||
'cloud-login',
|
||||
'cloud-signup',
|
||||
'cloud-forgot-password',
|
||||
'cloud-sorry-contact-support'
|
||||
])
|
||||
const PUBLIC_ROUTE_PATHS = new Set([
|
||||
'/cloud/login',
|
||||
'/cloud/signup',
|
||||
'/cloud/forgot-password',
|
||||
'/cloud/sorry-contact-support'
|
||||
])
|
||||
|
||||
function isPublicRoute(to: RouteLocationNormalized) {
|
||||
const name = String(to.name)
|
||||
if (PUBLIC_ROUTE_NAMES.has(name)) return true
|
||||
const path = to.path
|
||||
return PUBLIC_ROUTE_PATHS.has(path)
|
||||
}
|
||||
// Global authentication guard
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
router.beforeEach(async (_to, _from, next) => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
// Wait for Firebase auth to initialize
|
||||
@@ -113,69 +79,16 @@ if (isCloud) {
|
||||
|
||||
// Pass authenticated users
|
||||
const authHeader = await authStore.getAuthHeader()
|
||||
const isLoggedIn = !!authHeader
|
||||
|
||||
// Allow public routes
|
||||
if (isPublicRoute(to)) {
|
||||
if (authHeader) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// Special handling for user-check
|
||||
// These routes need auth but handle their own routing logic
|
||||
if (to.name === 'cloud-user-check') {
|
||||
if (to.meta.requiresAuth && !isLoggedIn) {
|
||||
return next({ name: 'cloud-login' })
|
||||
}
|
||||
return next()
|
||||
}
|
||||
// Show sign-in for unauthenticated users
|
||||
const dialogService = useDialogService()
|
||||
const loginSuccess = await dialogService.showSignInDialog()
|
||||
|
||||
// Prevent redirect loop when coming from user-check
|
||||
if (_from.name === 'cloud-user-check' && to.path === '/') {
|
||||
return next()
|
||||
}
|
||||
|
||||
// Check if route requires authentication
|
||||
if (to.meta.requiresAuth && !isLoggedIn) {
|
||||
return next({ name: 'cloud-login' })
|
||||
}
|
||||
|
||||
// Handle other protected routes
|
||||
if (!isLoggedIn) {
|
||||
// For Electron, use dialog
|
||||
if (isElectron()) {
|
||||
const dialogService = useDialogService()
|
||||
const loginSuccess = await dialogService.showSignInDialog()
|
||||
return loginSuccess ? next() : next(false)
|
||||
}
|
||||
|
||||
// For web, redirect to login
|
||||
return next({ name: 'cloud-login' })
|
||||
}
|
||||
|
||||
// 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 { getSurveyCompletedStatus } = await import(
|
||||
'@/platform/cloud/onboarding/auth'
|
||||
)
|
||||
try {
|
||||
// Check user's actual status
|
||||
const surveyCompleted = await getSurveyCompletedStatus()
|
||||
|
||||
// Survey is required for all users regardless of whitelist status
|
||||
if (!surveyCompleted) {
|
||||
return next({ name: 'cloud-survey' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check user status:', error)
|
||||
// On error, redirect to user-check as fallback
|
||||
return next({ name: 'cloud-user-check' })
|
||||
}
|
||||
}
|
||||
|
||||
// User is logged in and accessing protected route
|
||||
return next()
|
||||
if (loginSuccess) return next()
|
||||
return next(false)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -326,9 +326,14 @@ export class ComfyApi extends EventTarget {
|
||||
super()
|
||||
this.user = ''
|
||||
this.api_host = location.host
|
||||
this.api_base = isCloud
|
||||
? ''
|
||||
: location.pathname.split('/').slice(0, -1).join('/')
|
||||
const pathname = location.pathname
|
||||
const isCloudSpaRoute = isCloud && pathname.startsWith('/cloud/')
|
||||
if (isCloudSpaRoute) {
|
||||
this.api_base = ''
|
||||
} else {
|
||||
this.api_base = pathname.split('/').slice(0, -1).join('/')
|
||||
}
|
||||
console.log('Running on', this.api_host)
|
||||
this.initialClientId = sessionStorage.getItem('clientId')
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { reactive, unref } from 'vue'
|
||||
import { shallowRef } from 'vue'
|
||||
|
||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
|
||||
import { registerProxyWidgets } from '@/renderer/graph/subgraph/proxyWidget'
|
||||
import { st, t } from '@/i18n'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
||||
import {
|
||||
|
||||
@@ -14,13 +14,13 @@ import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
const THEME_PROPERTY_MAP = {
|
||||
NODE_BOX_OUTLINE_COLOR: 'component-node-border',
|
||||
NODE_DEFAULT_BGCOLOR: 'component-node-background',
|
||||
NODE_BOX_OUTLINE_COLOR: 'node-component-border',
|
||||
NODE_DEFAULT_BGCOLOR: 'node-component-surface',
|
||||
NODE_DEFAULT_BOXCOLOR: 'node-component-header-icon',
|
||||
NODE_DEFAULT_COLOR: 'node-component-header-surface',
|
||||
NODE_TITLE_COLOR: 'node-component-header',
|
||||
WIDGET_BGCOLOR: 'component-node-widget-background',
|
||||
WIDGET_TEXT_COLOR: 'component-node-foreground'
|
||||
WIDGET_BGCOLOR: 'node-component-widget-input-surface',
|
||||
WIDGET_TEXT_COLOR: 'node-component-widget-input'
|
||||
} as const satisfies Partial<Record<keyof Colors['litegraph_base'], string>>
|
||||
|
||||
export const useColorPaletteService = () => {
|
||||
|
||||
@@ -5,8 +5,7 @@ import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteG
|
||||
import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage'
|
||||
import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview'
|
||||
import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
|
||||
import { addWidgetPromotionOptions } from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
|
||||
import { showSubgraphNodeDialog } from '@/workbench/graph/subgraph/useSubgraphNodeDialog'
|
||||
import { st, t } from '@/i18n'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
@@ -58,6 +57,7 @@ import {
|
||||
import { getOrderedInputSpecs } from '@/utils/nodeDefOrderingUtil'
|
||||
|
||||
import { useExtensionService } from './extensionService'
|
||||
import { addWidgetPromotionOptions as invokeAddWidgetPromotionOptions } from './widgetPromotionHandlers'
|
||||
|
||||
export const CONFIG = Symbol()
|
||||
export const GET_CONFIG = Symbol()
|
||||
@@ -826,7 +826,7 @@ export const useLitegraphService = () => {
|
||||
const [x, y] = canvas.graph_mouse
|
||||
const overWidget = this.getWidgetOnPos(x, y, true)
|
||||
if (overWidget) {
|
||||
addWidgetPromotionOptions(options, overWidget, this)
|
||||
invokeAddWidgetPromotionOptions(options, overWidget, this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||