merge main into rh-test

This commit is contained in:
bymyself
2025-09-28 15:33:29 -07:00
parent 1c0f151d02
commit ff0c15b119
1317 changed files with 85439 additions and 18373 deletions

View File

@@ -0,0 +1,70 @@
<template>
<div class="w-full h-full flex flex-col rounded-lg p-6 justify-between">
<h1 class="font-inter font-semibold text-xl m-0 italic">
{{ t(`desktopDialogs.${id}.title`, title) }}
</h1>
<p class="whitespace-pre-wrap">
{{ t(`desktopDialogs.${id}.message`, message) }}
</p>
<div class="flex w-full gap-2">
<Button
v-for="button in buttons"
:key="button.label"
class="rounded-lg first:mr-auto"
:label="
t(
`desktopDialogs.${id}.buttons.${normalizeI18nKey(button.label)}`,
button.label
)
"
:severity="button.severity ?? 'secondary'"
@click="handleButtonClick(button)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useRoute } from 'vue-router'
import { type DialogAction, getDialog } from '@/constants/desktopDialogs'
import { t } from '@/i18n'
import { electronAPI } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
const route = useRoute()
const { id, title, message, buttons } = getDialog(route.params.dialogId)
const handleButtonClick = async (button: DialogAction) => {
await electronAPI().Dialog.clickButton(button.returnValue)
}
</script>
<style scoped>
@reference '../assets/css/style.css';
.p-button-secondary {
@apply text-white border-none bg-neutral-600;
}
.p-button-secondary:hover {
@apply bg-neutral-550;
}
.p-button-secondary:active {
@apply bg-neutral-500;
}
.p-button-danger {
@apply bg-coral-red-600;
}
.p-button-danger:hover {
@apply bg-coral-red-500;
}
.p-button-danger:active {
@apply bg-coral-red-400;
}
</style>

View File

@@ -1,11 +1,11 @@
<template>
<BaseViewTemplate dark>
<ProgressSpinner class="m-8 w-48 h-48" />
<StartupDisplay :title="$t('desktopStart.initialising')" />
</BaseViewTemplate>
</template>
<script setup lang="ts">
import ProgressSpinner from 'primevue/progressspinner'
import StartupDisplay from '@/components/common/StartupDisplay.vue'
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
</script>

View File

@@ -61,6 +61,8 @@ onUnmounted(() => electron.Validation.dispose())
</script>
<style scoped>
@reference '../assets/css/style.css';
.download-bg::before {
@apply m-0 absolute text-muted;
font-family: 'primeicons';

View File

@@ -1,7 +1,7 @@
<template>
<BaseViewTemplate>
<div
class="max-w-screen-sm flex flex-col gap-8 p-8 bg-[url('/assets/images/Git-Logo-White.svg')] bg-no-repeat bg-right-top bg-origin-padding"
class="max-w-(--breakpoint-sm) flex flex-col gap-8 p-8 bg-[url('/assets/images/Git-Logo-White.svg')] bg-no-repeat bg-top-right bg-origin-padding"
>
<!-- Header -->
<h1 class="mt-24 text-4xl font-bold text-red-500">

View File

@@ -33,6 +33,7 @@ import {
} from 'vue'
import { useI18n } from 'vue-i18n'
import { runWhenGlobalIdle } from '@/base/common/async'
import MenuHamburger from '@/components/MenuHamburger.vue'
import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue'
import GraphCanvas from '@/components/graph/GraphCanvas.vue'
@@ -42,11 +43,13 @@ import TopMenubar from '@/components/topbar/TopMenubar.vue'
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
import { useCoreCommands } from '@/composables/useCoreCommands'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useFrontendVersionMismatchWarning } from '@/composables/useFrontendVersionMismatchWarning'
import { useProgressFavicon } from '@/composables/useProgressFavicon'
import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'
import { i18n } from '@/i18n'
import { StatusWsMessageStatus } from '@/schemas/apiSchema'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useFrontendVersionMismatchWarning } from '@/platform/updates/common/useFrontendVersionMismatchWarning'
import { useVersionCompatibilityStore } from '@/platform/updates/common/versionCompatibilityStore'
import type { StatusWsMessageStatus } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { setupAutoQueueHandler } from '@/services/autoQueueService'
@@ -61,8 +64,6 @@ import {
useQueueStore
} from '@/stores/queueStore'
import { useServerConfigStore } from '@/stores/serverConfigStore'
import { useSettingStore } from '@/stores/settingStore'
import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
@@ -253,33 +254,30 @@ void nextTick(() => {
})
const onGraphReady = () => {
requestIdleCallback(
() => {
// Setting values now available after comfyApp.setup.
// Load keybindings.
wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)()
runWhenGlobalIdle(() => {
// Setting values now available after comfyApp.setup.
// Load keybindings.
wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)()
// Load server config
wrapWithErrorHandling(useServerConfigStore().loadServerConfig)(
SERVER_CONFIG_ITEMS,
settingStore.get('Comfy.Server.ServerConfigValues')
)
// Load server config
wrapWithErrorHandling(useServerConfigStore().loadServerConfig)(
SERVER_CONFIG_ITEMS,
settingStore.get('Comfy.Server.ServerConfigValues')
)
// Load model folders
void wrapWithErrorHandlingAsync(useModelStore().loadModelFolders)()
// Load model folders
void wrapWithErrorHandlingAsync(useModelStore().loadModelFolders)()
// Non-blocking load of node frequencies
void wrapWithErrorHandlingAsync(
useNodeFrequencyStore().loadNodeFrequencies
)()
// Non-blocking load of node frequencies
void wrapWithErrorHandlingAsync(
useNodeFrequencyStore().loadNodeFrequencies
)()
// Node defs now available after comfyApp.setup.
// Explicitly initialize nodeSearchService to avoid indexing delay when
// node search is triggered
useNodeDefStore().nodeSearchService.searchNode('')
},
{ timeout: 1000 }
)
// Node defs now available after comfyApp.setup.
// Explicitly initialize nodeSearchService to avoid indexing delay when
// node search is triggered
useNodeDefStore().nodeSearchService.searchNode('')
}, 1000)
}
</script>
@@ -343,7 +341,7 @@ const onGraphReady = () => {
grid-column: 2;
grid-row: 2;
position: relative;
overflow: hidden;
overflow: clip;
}
.comfyui-body-right {

View File

@@ -0,0 +1,423 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import { nextTick, provide } from 'vue'
import { createMemoryHistory, createRouter } from 'vue-router'
import InstallView from './InstallView.vue'
// Create a mock router for stories
const createMockRouter = () =>
createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: { template: '<div>Home</div>' } },
{
path: '/server-start',
component: { template: '<div>Server Start</div>' }
},
{
path: '/manual-configuration',
component: { template: '<div>Manual Configuration</div>' }
}
]
})
const meta: Meta<typeof InstallView> = {
title: 'Desktop/Views/InstallView',
component: InstallView,
parameters: {
layout: 'fullscreen',
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#0a0a0a' },
{ name: 'neutral-900', value: '#171717' },
{ name: 'neutral-950', value: '#0a0a0a' }
]
}
},
decorators: [
(story) => {
// Create router for this story
const router = createMockRouter()
// Mock electron API
;(window as any).electronAPI = {
getPlatform: () => 'darwin',
Config: {
getDetectedGpu: () => Promise.resolve('mps')
},
Events: {
trackEvent: (eventName: string, data?: any) => {
console.log('Track event:', eventName, data)
}
},
installComfyUI: (options: any) => {
console.log('Install ComfyUI with options:', options)
},
changeTheme: (theme: any) => {
console.log('Change theme:', theme)
},
getSystemPaths: () =>
Promise.resolve({
defaultInstallPath: '/Users/username/ComfyUI'
}),
validateInstallPath: () =>
Promise.resolve({
isValid: true,
exists: false,
canWrite: true,
freeSpace: 100000000000,
requiredSpace: 10000000000,
isNonDefaultDrive: false
}),
validateComfyUISource: () =>
Promise.resolve({
isValid: true
}),
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
}
return {
setup() {
// Provide router for all child components
provide('router', router)
return {
story
}
},
template: '<div style="width: 100vw; height: 100vh;"><story /></div>'
}
}
]
}
export default meta
type Story = StoryObj<typeof meta>
// Default story - start at GPU selection
export const GpuSelection: Story = {
render: () => ({
components: { InstallView },
setup() {
// The component will automatically start at step 1
return {}
},
template: '<InstallView />'
})
}
// Story showing the install location step
export const InstallLocation: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2
const buttons = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn) {
nextBtn.click()
}
},
template: '<InstallView />'
})
}
// Story showing the migration step (currently empty)
export const MigrationStep: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2
const buttons1 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn1) {
nextBtn1.click()
}
await nextTick()
// Click Next again to go to step 3
const buttons2 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn2) {
nextBtn2.click()
}
},
template: '<InstallView />'
})
}
// Story showing the desktop settings configuration
export const DesktopSettings: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2
const buttons1 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn1) {
nextBtn1.click()
}
await nextTick()
// Click Next again to go to step 3
const buttons2 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn2) {
nextBtn2.click()
}
await nextTick()
// Click Next again to go to step 4
const buttons3 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn3 = buttons3.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn3) {
nextBtn3.click()
}
},
template: '<InstallView />'
})
}
// Story with Windows platform (no Apple Metal option)
export const WindowsPlatform: Story = {
render: () => {
// Override the platform to Windows
;(window as any).electronAPI.getPlatform = () => 'win32'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('nvidia')
return {
components: { InstallView },
setup() {
return {}
},
template: '<InstallView />'
}
}
}
// Story with macOS platform (Apple Metal option)
export const MacOSPlatform: Story = {
name: 'macOS Platform',
render: () => {
// Override the platform to macOS
;(window as any).electronAPI.getPlatform = () => 'darwin'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('mps')
return {
components: { InstallView },
setup() {
return {}
},
template: '<InstallView />'
}
}
}
// Story with CPU selected
export const CpuSelected: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Find and click the CPU hardware option
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
// CPU is the button with "CPU" text
for (const button of hardwareButtons) {
if (button.textContent?.includes('CPU')) {
button.click()
break
}
}
},
template: '<InstallView />'
})
}
// Story with manual install selected
export const ManualInstall: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Find and click the Manual Install hardware option
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
// Manual Install is the button with "Manual Install" text
for (const button of hardwareButtons) {
if (button.textContent?.includes('Manual Install')) {
button.click()
break
}
}
},
template: '<InstallView />'
})
}
// Story with error state (invalid install path)
export const ErrorState: Story = {
render: () => {
// Override validation to return an error
;(window as any).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: false,
exists: false,
canWrite: false,
freeSpace: 100000000000,
requiredSpace: 10000000000,
isNonDefaultDrive: false,
error: 'Story mock: Example error state'
})
return {
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2 where error will be shown
const buttons = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn) {
nextBtn.click()
}
},
template: '<InstallView />'
}
}
}
// Story with warning state (non-default drive)
export const WarningState: Story = {
render: () => {
// Override validation to return a warning about non-default drive
;(window as any).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: true,
exists: false,
canWrite: true,
freeSpace: 500_000_000_000,
requiredSpace: 10_000_000_000,
isNonDefaultDrive: true
})
return {
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll('.hardware-option')
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2 where warning will be shown
const buttons = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn) {
nextBtn.click()
}
},
template: '<InstallView />'
}
}
}

View File

@@ -1,111 +1,54 @@
<template>
<BaseViewTemplate dark>
<!-- h-full to make sure the stepper does not layout shift between steps
as for each step the stepper height is different. Inherit the center element
placement from BaseViewTemplate would cause layout shift. -->
<Stepper
class="h-full p-8 2xl:p-16"
value="0"
@update:value="handleStepChange"
>
<StepList class="select-none">
<Step value="0">
{{ $t('install.gpu') }}
</Step>
<Step value="1" :disabled="noGpu">
{{ $t('install.installLocation') }}
</Step>
<Step value="2" :disabled="noGpu || hasError || highestStep < 1">
{{ $t('install.migration') }}
</Step>
<Step value="3" :disabled="noGpu || hasError || highestStep < 2">
{{ $t('install.desktopSettings') }}
</Step>
</StepList>
<StepPanels>
<StepPanel v-slot="{ activateCallback }" value="0">
<GpuPicker v-model:device="device" />
<div class="flex pt-6 justify-end">
<Button
:label="$t('g.next')"
icon="pi pi-arrow-right"
icon-pos="right"
:disabled="typeof device !== 'string'"
@click="activateCallback('1')"
<!-- Fixed height container with flexbox layout for proper content management -->
<div class="w-full h-full flex flex-col">
<Stepper
v-model:value="currentStep"
class="flex flex-col h-full"
@update:value="handleStepChange"
>
<!-- Main content area that grows to fill available space -->
<StepPanels
class="flex-1 overflow-auto"
:style="{ scrollbarGutter: 'stable' }"
>
<StepPanel value="1" class="flex">
<GpuPicker v-model:device="device" />
</StepPanel>
<StepPanel value="2">
<InstallLocationPicker
v-model:install-path="installPath"
v-model:path-error="pathError"
v-model:migration-source-path="migrationSourcePath"
v-model:migration-item-ids="migrationItemIds"
v-model:python-mirror="pythonMirror"
v-model:pypi-mirror="pypiMirror"
v-model:torch-mirror="torchMirror"
:device="device"
/>
</div>
</StepPanel>
<StepPanel v-slot="{ activateCallback }" value="1">
<InstallLocationPicker
v-model:installPath="installPath"
v-model:pathError="pathError"
/>
<div class="flex pt-6 justify-between">
<Button
:label="$t('g.back')"
severity="secondary"
icon="pi pi-arrow-left"
@click="activateCallback('0')"
</StepPanel>
<StepPanel value="3">
<DesktopSettingsConfiguration
v-model:auto-update="autoUpdate"
v-model:allow-metrics="allowMetrics"
/>
<Button
:label="$t('g.next')"
icon="pi pi-arrow-right"
icon-pos="right"
:disabled="pathError !== ''"
@click="activateCallback('2')"
/>
</div>
</StepPanel>
<StepPanel v-slot="{ activateCallback }" value="2">
<MigrationPicker
v-model:sourcePath="migrationSourcePath"
v-model:migrationItemIds="migrationItemIds"
/>
<div class="flex pt-6 justify-between">
<Button
:label="$t('g.back')"
severity="secondary"
icon="pi pi-arrow-left"
@click="activateCallback('1')"
/>
<Button
:label="$t('g.next')"
icon="pi pi-arrow-right"
icon-pos="right"
@click="activateCallback('3')"
/>
</div>
</StepPanel>
<StepPanel v-slot="{ activateCallback }" value="3">
<DesktopSettingsConfiguration
v-model:autoUpdate="autoUpdate"
v-model:allowMetrics="allowMetrics"
/>
<MirrorsConfiguration
v-model:pythonMirror="pythonMirror"
v-model:pypiMirror="pypiMirror"
v-model:torchMirror="torchMirror"
:device="device"
class="mt-6"
/>
<div class="flex mt-6 justify-between">
<Button
:label="$t('g.back')"
severity="secondary"
icon="pi pi-arrow-left"
@click="activateCallback('2')"
/>
<Button
:label="$t('g.install')"
icon="pi pi-check"
icon-pos="right"
:disabled="hasError"
@click="install()"
/>
</div>
</StepPanel>
</StepPanels>
</Stepper>
</StepPanel>
</StepPanels>
<!-- Install footer with navigation -->
<InstallFooter
class="w-full max-w-2xl my-6 mx-auto"
:current-step
:can-proceed
:disable-location-step="noGpu"
:disable-migration-step="noGpu || hasError || highestStep < 2"
:disable-settings-step="noGpu || hasError || highestStep < 3"
@previous="goToPreviousStep"
@next="goToNextStep"
@install="install"
/>
</Stepper>
</div>
</BaseViewTemplate>
</template>
@@ -114,9 +57,6 @@ import type {
InstallOptions,
TorchDeviceType
} from '@comfyorg/comfyui-electron-types'
import Button from 'primevue/button'
import Step from 'primevue/step'
import StepList from 'primevue/steplist'
import StepPanel from 'primevue/steppanel'
import StepPanels from 'primevue/steppanels'
import Stepper from 'primevue/stepper'
@@ -125,9 +65,8 @@ import { useRouter } from 'vue-router'
import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsConfiguration.vue'
import GpuPicker from '@/components/install/GpuPicker.vue'
import InstallFooter from '@/components/install/InstallFooter.vue'
import InstallLocationPicker from '@/components/install/InstallLocationPicker.vue'
import MigrationPicker from '@/components/install/MigrationPicker.vue'
import MirrorsConfiguration from '@/components/install/MirrorsConfiguration.vue'
import { electronAPI } from '@/utils/envUtil'
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
@@ -145,6 +84,9 @@ const pythonMirror = ref('')
const pypiMirror = ref('')
const torchMirror = ref('')
/** Current step in the stepper */
const currentStep = ref('1')
/** Forces each install step to be visited at least once. */
const highestStep = ref(0)
@@ -164,6 +106,40 @@ const setHighestStep = (value: string | number) => {
const hasError = computed(() => pathError.value !== '')
const noGpu = computed(() => typeof device.value !== 'string')
// Computed property to determine if user can proceed to next step
const regex = /^Insufficient space - minimum free space: \d+ GB$/
const canProceed = computed(() => {
switch (currentStep.value) {
case '1':
return typeof device.value === 'string'
case '2':
return pathError.value === '' || regex.test(pathError.value)
case '3':
return !hasError.value
default:
return false
}
})
// Navigation methods
const goToNextStep = () => {
const nextStep = (parseInt(currentStep.value) + 1).toString()
currentStep.value = nextStep
setHighestStep(nextStep)
electronAPI().Events.trackEvent('install_stepper_change', {
step: nextStep
})
}
const goToPreviousStep = () => {
const prevStep = (parseInt(currentStep.value) - 1).toString()
currentStep.value = prevStep
electronAPI().Events.trackEvent('install_stepper_change', {
step: prevStep
})
}
const electron = electronAPI()
const router = useRouter()
const install = async () => {
@@ -195,14 +171,40 @@ onMounted(async () => {
}
electronAPI().Events.trackEvent('install_stepper_change', {
step: '0',
step: currentStep.value,
gpu: detectedGpu
})
})
</script>
<style scoped>
@reference '../assets/css/style.css';
:deep(.p-steppanel) {
@apply mt-8 flex justify-center bg-transparent;
}
/* Remove default padding/margin from StepPanels to make scrollbar flush */
:deep(.p-steppanels) {
@apply p-0 m-0;
}
/* Ensure StepPanel content container has no top/bottom padding */
:deep(.p-steppanel-content) {
@apply p-0;
}
/* Custom overlay scrollbar for WebKit browsers (Electron, Chrome) */
:deep(.p-steppanels::-webkit-scrollbar) {
@apply w-4;
}
:deep(.p-steppanels::-webkit-scrollbar-track) {
@apply bg-transparent;
}
:deep(.p-steppanels::-webkit-scrollbar-thumb) {
@apply bg-white/20 rounded-lg border-[4px] border-transparent;
background-clip: content-box;
}
</style>

View File

@@ -3,7 +3,7 @@
<div
class="min-w-full min-h-full font-sans w-screen h-screen grid justify-around text-neutral-300 bg-neutral-900 dark-theme overflow-y-auto"
>
<div class="max-w-screen-sm w-screen m-8 relative">
<div class="max-w-(--breakpoint-sm) w-screen m-8 relative">
<!-- Header -->
<h1 class="backspan pi-wrench text-4xl font-bold">
{{ t('maintenance.title') }}
@@ -100,7 +100,7 @@ import TaskListPanel from '@/components/maintenance/TaskListPanel.vue'
import TerminalOutputDrawer from '@/components/maintenance/TerminalOutputDrawer.vue'
import { t } from '@/i18n'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import { MaintenanceFilter } from '@/types/desktop/maintenanceTypes'
import type { MaintenanceFilter } from '@/types/desktop/maintenanceTypes'
import { electronAPI } from '@/utils/envUtil'
import { useMinLoadingDurationRef } from '@/utils/refUtil'
@@ -178,6 +178,8 @@ onUnmounted(() => electron.Validation.dispose())
</script>
<style scoped>
@reference '../assets/css/style.css';
:deep(.p-tag) {
--p-tag-gap: 0.375rem;
}

View File

@@ -79,6 +79,8 @@ const continueToInstall = async () => {
</script>
<style scoped>
@reference '../assets/css/style.css';
.sad-container {
@apply grid items-center justify-evenly;
grid-template-columns: 25rem 1fr;

View File

@@ -1,75 +1,205 @@
<template>
<BaseViewTemplate dark class="flex-col">
<div class="flex flex-col w-full h-full items-center">
<h2 class="text-2xl font-bold">
{{ t(`serverStart.process.${status}`) }}
<span v-if="status === ProgressStatus.ERROR">
v{{ electronVersion }}
</span>
</h2>
<div
v-if="status === ProgressStatus.ERROR"
class="flex flex-col items-center gap-4"
>
<div class="flex items-center my-4 gap-2">
<Button
icon="pi pi-flag"
severity="secondary"
:label="t('serverStart.reportIssue')"
@click="reportIssue"
/>
<Button
icon="pi pi-file"
severity="secondary"
:label="t('serverStart.openLogs')"
@click="openLogs"
/>
<Button
icon="pi pi-wrench"
:label="t('serverStart.troubleshoot')"
@click="troubleshoot"
/>
<BaseViewTemplate dark>
<div class="relative min-h-screen">
<!-- Terminal Background Layer (always visible during loading) -->
<div v-if="!isError" class="fixed inset-0 overflow-hidden z-0">
<div class="h-full w-full">
<BaseTerminal @created="terminalCreated" />
</div>
</div>
<!-- Semi-transparent overlay -->
<div v-if="!isError" class="fixed inset-0 bg-neutral-900/80 z-5"></div>
<!-- Smooth radial gradient overlay -->
<div
v-if="!isError"
class="fixed inset-0 z-8"
style="
background: radial-gradient(
ellipse 800px 600px at center,
rgba(23, 23, 23, 0.95) 0%,
rgba(23, 23, 23, 0.93) 10%,
rgba(23, 23, 23, 0.9) 20%,
rgba(23, 23, 23, 0.85) 30%,
rgba(23, 23, 23, 0.75) 40%,
rgba(23, 23, 23, 0.6) 50%,
rgba(23, 23, 23, 0.4) 60%,
rgba(23, 23, 23, 0.2) 70%,
rgba(23, 23, 23, 0.1) 80%,
rgba(23, 23, 23, 0.05) 90%,
transparent 100%
);
"
></div>
<div class="relative z-10">
<!-- Main startup display using StartupDisplay component -->
<StartupDisplay
:title="displayTitle"
:status-text="displayStatusText"
:progress-percentage="installStageProgress"
:hide-progress="isError"
/>
<!-- Error Section (positioned at bottom) -->
<div
v-if="isError"
class="absolute bottom-20 left-0 right-0 flex flex-col items-center gap-4"
>
<div class="flex gap-4 justify-center">
<Button
icon="pi pi-flag"
:label="$t('serverStart.reportIssue')"
severity="secondary"
@click="reportIssue"
/>
<Button
icon="pi pi-file"
:label="$t('serverStart.openLogs')"
severity="secondary"
@click="openLogs"
/>
<Button
icon="pi pi-wrench"
:label="$t('serverStart.troubleshoot')"
@click="troubleshoot"
/>
</div>
<div class="text-center">
<button
v-if="!terminalVisible"
class="text-sm text-neutral-500 hover:text-neutral-300 transition-colors flex items-center gap-2 mx-auto"
@click="terminalVisible = true"
>
<i class="pi pi-search"></i>
{{ $t('serverStart.showTerminal') }}
</button>
</div>
</div>
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
<div
v-if="terminalVisible && isError"
class="absolute bottom-4 left-4 right-4 max-w-4xl mx-auto z-10"
>
<div
class="bg-neutral-900/95 rounded-lg p-4 border border-neutral-700 h-[300px]"
>
<BaseTerminal @created="terminalCreated" />
</div>
</div>
<Button
v-if="!terminalVisible"
icon="pi pi-search"
severity="secondary"
:label="t('serverStart.showTerminal')"
@click="terminalVisible = true"
/>
</div>
<BaseTerminal v-show="terminalVisible" @created="terminalCreated" />
</div>
</BaseViewTemplate>
</template>
<script setup lang="ts">
import { ProgressStatus } from '@comfyorg/comfyui-electron-types'
import { Terminal } from '@xterm/xterm'
import {
InstallStage,
type InstallStageInfo,
type InstallStageName,
ProgressStatus
} from '@comfyorg/comfyui-electron-types'
import type { Terminal } from '@xterm/xterm'
import Button from 'primevue/button'
import { Ref, onMounted, ref } from 'vue'
import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseTerminal from '@/components/bottomPanel/tabs/terminal/BaseTerminal.vue'
import StartupDisplay from '@/components/common/StartupDisplay.vue'
import type { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
import { electronAPI } from '@/utils/envUtil'
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
const electron = electronAPI()
const { t } = useI18n()
const electron = electronAPI()
const status = ref<ProgressStatus>(ProgressStatus.INITIAL_STATE)
const electronVersion = ref<string>('')
const terminalVisible = ref(false)
const installStage = ref<InstallStageName | null>(null)
const installStageMessage = ref<string>('')
const installStageProgress = ref<number | undefined>(undefined)
let xterm: Terminal | undefined
const terminalVisible = ref(true)
/**
* Handles installation stage updates from the desktop
*/
const updateInstallStage = (stageInfo: InstallStageInfo) => {
console.warn('[InstallStage.onUpdate] Received:', {
stage: stageInfo.stage,
progress: stageInfo.progress,
message: stageInfo.message,
error: stageInfo.error,
timestamp: stageInfo.timestamp,
fullInfo: stageInfo
})
installStage.value = stageInfo.stage
installStageMessage.value = stageInfo.message || ''
installStageProgress.value = stageInfo.progress
}
const currentStatusLabel = computed(() => {
// Use the message from the Electron API if available
if (installStageMessage.value) {
return installStageMessage.value
}
return t(`serverStart.process.${status.value}`)
})
const isError = computed(
() =>
status.value === ProgressStatus.ERROR ||
installStage.value === InstallStage.ERROR
)
const isInstallationStage = computed(() => {
const installationStages: InstallStageName[] = [
InstallStage.WELCOME_SCREEN,
InstallStage.INSTALL_OPTIONS_SELECTION,
InstallStage.CREATING_DIRECTORIES,
InstallStage.INITIALIZING_CONFIG,
InstallStage.PYTHON_ENVIRONMENT_SETUP,
InstallStage.INSTALLING_REQUIREMENTS,
InstallStage.INSTALLING_PYTORCH,
InstallStage.INSTALLING_COMFYUI_REQUIREMENTS,
InstallStage.INSTALLING_MANAGER_REQUIREMENTS,
InstallStage.MIGRATING_CUSTOM_NODES
]
return (
installStage.value !== null &&
installationStages.includes(installStage.value)
)
})
const displayTitle = computed(() => {
if (isError.value) {
return t('serverStart.errorMessage')
}
if (isInstallationStage.value) {
return t('serverStart.installation.title')
}
return t('serverStart.title')
})
const displayStatusText = computed(() => {
if (isError.value && electronVersion.value) {
return `v${electronVersion.value}`
}
return currentStatusLabel.value
})
const updateProgress = ({ status: newStatus }: { status: ProgressStatus }) => {
status.value = newStatus
// Make critical error screen more obvious.
if (newStatus === ProgressStatus.ERROR) terminalVisible.value = false
else xterm?.clear()
}
const terminalCreated = (
@@ -94,16 +224,30 @@ const reportIssue = () => {
}
const openLogs = () => electron.openLogsFolder()
let cleanupInstallStageListener: (() => void) | undefined
onMounted(async () => {
electron.sendReady()
electron.onProgressUpdate(updateProgress)
cleanupInstallStageListener =
electron.InstallStage.onUpdate(updateInstallStage)
const stageInfo = await electron.InstallStage.getCurrent()
updateInstallStage(stageInfo)
electronVersion.value = await electron.getElectronVersion()
})
onUnmounted(() => {
xterm?.dispose()
cleanupInstallStageListener?.()
})
</script>
<style scoped>
:deep(.xterm-helper-textarea) {
/* Hide this as it moves all over when uv is running */
display: none;
@reference '../assets/css/style.css';
/* Hide the xterm scrollbar completely */
:deep(.p-terminal) .xterm-viewport {
overflow: hidden !important;
}
</style>

View File

@@ -2,7 +2,7 @@
<BaseViewTemplate dark>
<main
id="comfy-user-selection"
class="min-w-84 relative rounded-lg bg-[var(--comfy-menu-bg)] p-5 px-10 shadow-lg"
class="min-w-84 relative rounded-lg bg-(--comfy-menu-bg) p-5 px-10 shadow-lg"
>
<h1 class="my-2.5 mb-7 font-normal">ComfyUI</h1>
<div class="flex w-full flex-col items-center">
@@ -50,7 +50,8 @@ import Select from 'primevue/select'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { User, useUserStore } from '@/stores/userStore'
import type { User } from '@/stores/userStore'
import { useUserStore } from '@/stores/userStore'
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
const userStore = useUserStore()

View File

@@ -1,21 +1,27 @@
<template>
<BaseViewTemplate dark>
<div class="flex flex-col items-center justify-center gap-8 p-8">
<!-- Header -->
<h1 class="animated-gradient-text text-glow select-none">
{{ $t('welcome.title') }}
</h1>
<!-- Get Started Button -->
<Button
:label="$t('welcome.getStarted')"
icon="pi pi-arrow-right"
icon-pos="right"
size="large"
rounded
class="p-4 text-lg fade-in-up"
@click="navigateTo('/install')"
/>
<div class="flex items-center justify-center min-h-screen">
<div class="grid grid-rows-2 gap-8">
<!-- Top container: Logo -->
<div class="flex items-end justify-center">
<img
src="/assets/images/comfy-brand-mark.svg"
:alt="$t('g.logoAlt')"
class="w-60"
/>
</div>
<!-- Bottom container: Title and button -->
<div class="flex flex-col items-center justify-center gap-4">
<Button
:label="$t('welcome.getStarted')"
class="px-8 mt-4 bg-brand-yellow hover:bg-brand-yellow/90 border-0 rounded-lg transition-colors"
:pt="{
label: { class: 'font-inter text-neutral-900 font-black' }
}"
@click="navigateTo('/install')"
/>
</div>
</div>
</div>
</BaseViewTemplate>
</template>
@@ -31,47 +37,3 @@ const navigateTo = async (path: string) => {
await router.push(path)
}
</script>
<style scoped>
.animated-gradient-text {
@apply font-bold;
font-size: clamp(2rem, 8vw, 4rem);
background: linear-gradient(to right, #12c2e9, #c471ed, #f64f59, #12c2e9);
background-size: 300% auto;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradient 8s linear infinite;
}
.text-glow {
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3));
}
@keyframes gradient {
0% {
background-position: 0% center;
}
100% {
background-position: 300% center;
}
}
.fade-in-up {
animation: fadeInUp 1.5s ease-out;
animation-fill-mode: both;
}
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -12,11 +12,9 @@
<div
v-show="isNativeWindow()"
ref="topMenuRef"
class="app-drag w-full h-[var(--comfy-topbar-height)]"
class="app-drag w-full h-(--comfy-topbar-height)"
/>
<div
class="flex-1 flex-grow w-full flex items-center justify-center overflow-auto"
>
<div class="grow w-full flex items-center justify-center overflow-auto">
<slot />
</div>
<slot name="footer"></slot>