Compare commits

..

1 Commits

Author SHA1 Message Date
Terry Jia
f1932631b6 export vue on Window 2025-05-13 22:04:29 -04:00
57 changed files with 550 additions and 4114 deletions

View File

@@ -25,7 +25,3 @@ ENABLE_MINIFY=true
# templates are served via the normal method from the server's python site
# packages.
DISABLE_TEMPLATES_PROXY=false
# If playwright tests are being run via vite dev server, Vue plugins will
# invalidate screenshots. When `true`, vite plugins will not be loaded.
DISABLE_VUE_PLUGINS=false

View File

@@ -1,72 +0,0 @@
name: Create Dev PyPI Package
on:
workflow_dispatch:
inputs:
devVersion:
description: 'Dev version'
required: true
type: number
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.current_version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Build project
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
USE_PROD_CONFIG: 'true'
run: |
npm ci
npm run build
npm run zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
with:
name: dist-files
path: |
dist/
dist.zip
publish_pypi:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download dist artifact
uses: actions/download-artifact@v4
with:
name: dist-files
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install build dependencies
run: python -m pip install build
- name: Setup pypi package
run: |
mkdir -p comfyui_frontend_package/comfyui_frontend_package/static/
cp -r dist/* comfyui_frontend_package/comfyui_frontend_package/static/
- name: Build pypi package
run: python -m build
working-directory: comfyui_frontend_package
env:
COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }}
- name: Publish pypi package
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist

135
README.md
View File

@@ -526,38 +526,6 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
## Development
### Prerequisites
- Node.js (v16 or later) and npm must be installed
- Git for version control
- A running ComfyUI backend instance
### Initial Setup
1. Clone the repository:
```bash
git clone https://github.com/Comfy-Org/ComfyUI_frontend.git
cd ComfyUI_frontend
```
2. Install dependencies:
```bash
npm install
```
3. Configure environment (optional):
Create a `.env` file in the project root based on the provided [.env.example](.env.example) file.
**Note about ports**: By default, the dev server expects the ComfyUI backend at `localhost:8188`. If your ComfyUI instance runs on a different port, update this in your `.env` file.
### Dev Server Configuration
To launch ComfyUI and have it connect to your development server:
```bash
python main.py --port 8188
```
### Tech Stack
- [Vue 3](https://vuejs.org/) with [TypeScript](https://www.typescriptlang.org/)
@@ -579,6 +547,7 @@ core extensions will be loaded.
- Start local ComfyUI backend at `localhost:8188`
- Run `npm run dev` to start the dev server
- Run `npm run dev:electron` to start the dev server with electron API mocked
#### Access dev server on touch devices
@@ -606,7 +575,7 @@ navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to
This project includes `.vscode/launch.json.default` and `.vscode/settings.json.default` files with recommended launch and workspace settings for editors that use the `.vscode` directory (e.g., VS Code, Cursor, etc.).
We've also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
Weve also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
### Unit Test
@@ -637,103 +606,3 @@ This will replace the litegraph package in this repo with the local litegraph re
### i18n
See [locales/README.md](src/locales/README.md) for details.
## Troubleshooting
> **Note**: For comprehensive troubleshooting and how-to guides, please refer to our [official documentation](https://docs.comfy.org/). This section covers only the most common issues related to frontend development.
> **Desktop Users**: For issues specific to the desktop application, please refer to the [ComfyUI desktop repository](https://github.com/Comfy-Org/desktop).
### Debugging Custom Node (Extension) Issues
If you're experiencing crashes, errors, or unexpected behavior with ComfyUI, it's often caused by custom nodes (extensions). Follow these steps to identify and resolve the issues:
#### Step 1: Verify if custom nodes are causing the problem
Run ComfyUI with the `--disable-all-custom-nodes` flag:
```bash
python main.py --disable-all-custom-nodes
```
If the issue disappears, a custom node is the culprit. Proceed to the next step.
#### Step 2: Identify the problematic custom node using binary search
Rather than disabling nodes one by one, use this more efficient approach:
1. Temporarily move half of your custom nodes out of the `custom_nodes` directory
```bash
# Create a temporary directory
# Linux/Mac
mkdir ~/custom_nodes_disabled
# Windows
mkdir %USERPROFILE%\custom_nodes_disabled
# Move half of your custom nodes (assuming you have node1 through node8)
# Linux/Mac
mv custom_nodes/node1 custom_nodes/node2 custom_nodes/node3 custom_nodes/node4 ~/custom_nodes_disabled/
# Windows
move custom_nodes\node1 custom_nodes\node2 custom_nodes\node3 custom_nodes\node4 %USERPROFILE%\custom_nodes_disabled\
```
2. Run ComfyUI again
- If the issue persists: The problem is in nodes 5-8 (the remaining half)
- If the issue disappears: The problem is in nodes 1-4 (the moved half)
3. Let's assume the issue disappeared, so the problem is in nodes 1-4. Move half of these for the next test:
```bash
# Move nodes 3-4 back to custom_nodes
# Linux/Mac
mv ~/custom_nodes_disabled/node3 ~/custom_nodes_disabled/node4 custom_nodes/
# Windows
move %USERPROFILE%\custom_nodes_disabled\node3 %USERPROFILE%\custom_nodes_disabled\node4 custom_nodes\
```
4. Run ComfyUI again
- If the issue reappears: The problem is in nodes 3-4
- If issue still gone: The problem is in nodes 1-2
5. Let's assume the issue reappeared, so the problem is in nodes 3-4. Test each one:
```bash
# Move node 3 back to disabled
# Linux/Mac
mv custom_nodes/node3 ~/custom_nodes_disabled/
# Windows
move custom_nodes\node3 %USERPROFILE%\custom_nodes_disabled\
```
6. Run ComfyUI again
- If the issue disappears: node3 is the problem
- If issue persists: node4 is the problem
7. Repeat until you identify the specific problematic node
#### Step 3: Update or replace the problematic node
Once identified:
1. Check for updates to the problematic custom node
2. Consider alternatives with similar functionality
3. Report the issue to the custom node developer with specific details
### Common Issues and Solutions
- **"Module not found" errors**: Usually indicates missing Python dependencies. Check the custom node's `requirements.txt` file for required packages and install them:
```bash
pip install -r custom_nodes/problematic_node/requirements.txt
```
- **Frontend or Templates Package Not Updated**: After updating ComfyUI via Git, ensure you update the frontend dependencies:
```bash
pip install -r requirements.txt
```
- **Can't Find Custom Node**: Make sure to disable node validation in ComfyUI settings.
- **Error Toast About Workflow Failing Validation**: Report the issue to the ComfyUI team. As a temporary workaround, disable workflow validation in settings.
- **Login Issues When Not on Localhost**: Normal login is only available when accessing from localhost. If you're running ComfyUI via LAN, another domain, or headless, you can use our API key feature to authenticate. The API key lets you log in normally through the UI. Generate an API key at [platform.comfy.org/login](https://platform.comfy.org/login) and use it in the API Key field in the login dialog or with the `--api-key` command line argument. Refer to our [API Key Integration Guide](https://docs.comfy.org/essentials/comfyui-server/api-key-integration#integration-of-api-key-to-use-comfyui-api-nodes) for complete setup instructions.

View File

@@ -275,6 +275,7 @@ export class ComfyPage {
localStorage.clear()
sessionStorage.clear()
localStorage.setItem('Comfy.userId', id)
localStorage.setItem('api-nodes-news-seen', 'true')
}, this.id)
}
await this.goto()

6
global.d.ts vendored
View File

@@ -16,3 +16,9 @@ interface Navigator {
visible: boolean
}
}
interface Window {
Vue: typeof import('vue')
PrimeVue: typeof import('primevue')
VueI18n: typeof import('vue-i18n')
}

1717
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.20.2",
"version": "1.20.0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -63,8 +63,6 @@
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.19",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-vue-devtools": "^7.7.6",
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0",
@@ -74,7 +72,7 @@
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.43",
"@comfyorg/litegraph": "^0.15.11",
"@comfyorg/litegraph": "^0.15.9",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",

View File

@@ -63,6 +63,15 @@ watchDebounced(
// Set initial position to bottom center
const setInitialPosition = () => {
if (x.value !== 0 || y.value !== 0) {
return
}
if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) {
x.value = storedPosition.value.x
y.value = storedPosition.value.y
captureLastDragState()
return
}
if (panelRef.value) {
const screenWidth = window.innerWidth
const screenHeight = window.innerHeight
@@ -73,25 +82,9 @@ const setInitialPosition = () => {
return
}
// Check if stored position exists and is within bounds
if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) {
// Ensure stored position is within screen bounds
x.value = clamp(storedPosition.value.x, 0, screenWidth - menuWidth)
y.value = clamp(storedPosition.value.y, 0, screenHeight - menuHeight)
captureLastDragState()
return
}
// If no stored position or current position, set to bottom center
if (x.value === 0 && y.value === 0) {
x.value = clamp((screenWidth - menuWidth) / 2, 0, screenWidth - menuWidth)
y.value = clamp(
screenHeight - menuHeight - 10,
0,
screenHeight - menuHeight
)
captureLastDragState()
}
x.value = (screenWidth - menuWidth) / 2
y.value = screenHeight - menuHeight - 10 // 10px margin from bottom
captureLastDragState()
}
}
onMounted(setInitialPosition)

View File

@@ -0,0 +1,74 @@
<template>
<div class="flex flex-col gap-12 p-2 w-96">
<img src="@/assets/images/api-nodes-news.webp" alt="API Nodes News" />
<div class="flex flex-col gap-2 justify-center items-center">
<div class="text-xl">
{{ $t('apiNodesNews.introducing') }}
<span class="text-amber-500">API NODES</span>
</div>
<div class="text-muted">{{ $t('apiNodesNews.subtitle') }}</div>
</div>
<div class="flex flex-col gap-4">
<div
v-for="(step, index) in steps"
:key="index"
class="grid grid-cols-[auto_1fr] gap-2 items-center"
>
<Tag class="w-8 h-8" :value="index + 1" rounded />
<div class="flex flex-col gap-2">
<div>{{ step.title }}</div>
<div v-if="step.subtitle" class="text-muted">
{{ step.subtitle }}
</div>
</div>
</div>
</div>
<div class="flex flex-row justify-between">
<Button label="Learn More" text @click="handleLearnMore" />
<Button label="Close" @click="onClose" />
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Tag from 'primevue/tag'
import { onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const steps: {
title: string
subtitle?: string
}[] = [
{
title: t('apiNodesNews.steps.step1.title'),
subtitle: t('apiNodesNews.steps.step1.subtitle')
},
{
title: t('apiNodesNews.steps.step2.title'),
subtitle: t('apiNodesNews.steps.step2.subtitle')
},
{
title: t('apiNodesNews.steps.step3.title')
},
{
title: t('apiNodesNews.steps.step4.title')
}
]
const { onClose } = defineProps<{
onClose: () => void
}>()
const handleLearnMore = () => {
window.open('https://blog.comfy.org/p/comfyui-native-api-nodes', '_blank')
}
onBeforeUnmount(() => {
localStorage.setItem('api-nodes-news-seen', 'true')
})
</script>

View File

@@ -12,8 +12,7 @@
<script setup lang="ts">
import type { LGraphNode } from '@comfyorg/litegraph'
import { whenever } from '@vueuse/core'
import { computed } from 'vue'
import { computed, watch } from 'vue'
import DomWidget from '@/components/graph/widgets/DomWidget.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
@@ -55,13 +54,18 @@ const updateWidgets = () => {
}
const canvasStore = useCanvasStore()
whenever(
watch(
() => canvasStore.canvas,
(canvas) =>
(canvas.onDrawForeground = useChainCallback(
canvas.onDrawForeground,
updateWidgets
)),
(lgCanvas) => {
if (!lgCanvas) return
lgCanvas.onDrawForeground = useChainCallback(
lgCanvas.onDrawForeground,
() => {
updateWidgets()
}
)
},
{ immediate: true }
)
</script>

View File

@@ -70,7 +70,7 @@ import { usePaste } from '@/composables/usePaste'
import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
import { CORE_SETTINGS } from '@/constants/coreSettings'
import { i18n, t } from '@/i18n'
import { i18n } from '@/i18n'
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
import { UnauthorizedError, api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
@@ -230,7 +230,7 @@ useEventListener(
() => {
toastStore.add({
severity: 'warn',
summary: t('toastMessages.nothingSelected'),
summary: 'No items selected',
life: 2000
})
},

View File

@@ -14,10 +14,12 @@
<script setup lang="ts">
import { createBounds } from '@comfyorg/litegraph'
import type { LGraphCanvas } from '@comfyorg/litegraph'
import { whenever } from '@vueuse/core'
import { ref, watch } from 'vue'
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useCanvasStore } from '@/stores/graphStore'
const canvasStore = useCanvasStore()
@@ -26,8 +28,8 @@ const { style, updatePosition } = useAbsolutePosition()
const visible = ref(false)
const showBorder = ref(false)
const positionSelectionOverlay = () => {
const { selectedItems } = canvasStore.getCanvas()
const positionSelectionOverlay = (canvas: LGraphCanvas) => {
const selectedItems = canvas.selectedItems
showBorder.value = selectedItems.size > 1
if (!selectedItems.size) {
@@ -46,18 +48,26 @@ const positionSelectionOverlay = () => {
}
// Register listener on canvas creation.
whenever(
() => canvasStore.getCanvas().state.selectionChanged,
() => {
requestAnimationFrame(() => {
positionSelectionOverlay()
canvasStore.getCanvas().state.selectionChanged = false
})
watch(
() => canvasStore.canvas as LGraphCanvas | null,
(canvas: LGraphCanvas | null) => {
if (!canvas) return
canvas.onSelectionChange = useChainCallback(
canvas.onSelectionChange,
// Wait for next frame as sometimes the selected items haven't been
// rendered yet, so the boundingRect is not available on them.
() => requestAnimationFrame(() => positionSelectionOverlay(canvas))
)
},
{ immediate: true }
)
canvasStore.getCanvas().ds.onChanged = positionSelectionOverlay
whenever(
() => canvasStore.getCanvas().ds.state,
() => positionSelectionOverlay(canvasStore.getCanvas()),
{ deep: true }
)
watch(
() => canvasStore.canvas?.state?.draggingItems,
@@ -67,10 +77,10 @@ watch(
// the correct position.
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/2656
if (draggingItems === false) {
requestAnimationFrame(() => {
setTimeout(() => {
visible.value = true
positionSelectionOverlay()
})
positionSelectionOverlay(canvasStore.getCanvas())
}, 100)
} else {
// Selection change update to visible state is delayed by a frame. Here
// we also delay a frame so that the order of events is correct when

View File

@@ -6,41 +6,96 @@
content: 'p-0 flex flex-row'
}"
>
<ExecuteButton />
<ColorPickerButton />
<BypassButton />
<PinButton />
<DeleteButton />
<RefreshButton />
<ExtensionCommandButton
<ExecuteButton v-show="nodeSelected" />
<ColorPickerButton v-show="nodeSelected || groupSelected" />
<Button
v-show="nodeSelected"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),
showDelay: 1000
}"
severity="secondary"
text
data-testid="bypass-button"
@click="
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
"
>
<template #icon>
<i-game-icons:detour />
</template>
</Button>
<Button
v-show="nodeSelected || groupSelected"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Pin.label'),
showDelay: 1000
}"
severity="secondary"
text
icon="pi pi-thumbtack"
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
/>
<Button
v-tooltip.top="{
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
showDelay: 1000
}"
severity="danger"
text
icon="pi pi-trash"
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
/>
<Button
v-show="isRefreshable"
severity="info"
text
icon="pi pi-refresh"
@click="refreshSelected"
/>
<Button
v-for="command in extensionToolboxCommands"
:key="command.id"
:command="command"
v-tooltip.top="{
value:
st(`commands.${normalizeI18nKey(command.id)}.label`, '') || undefined,
showDelay: 1000
}"
severity="secondary"
text
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
@click="() => commandStore.execute(command.id)"
/>
</Panel>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Panel from 'primevue/panel'
import { computed } from 'vue'
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
import { useRefreshableSelection } from '@/composables/useRefreshableSelection'
import { st, t } from '@/i18n'
import { useExtensionService } from '@/services/extensionService'
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
import { ComfyCommand, useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import BypassButton from './selectionToolbox/BypassButton.vue'
import DeleteButton from './selectionToolbox/DeleteButton.vue'
import ExtensionCommandButton from './selectionToolbox/ExtensionCommandButton.vue'
import PinButton from './selectionToolbox/PinButton.vue'
import RefreshButton from './selectionToolbox/RefreshButton.vue'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const extensionService = useExtensionService()
const { isRefreshable, refreshSelected } = useRefreshableSelection()
const nodeSelected = computed(() =>
canvasStore.selectedItems.some(isLGraphNode)
)
const groupSelected = computed(() =>
canvasStore.selectedItems.some(isLGraphGroup)
)
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
const extensionToolboxCommands = computed<ComfyCommand[]>(() => {
const commandIds = new Set<string>(
canvasStore.selectedItems
.map(
@@ -53,7 +108,7 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
)
return Array.from(commandIds)
.map((commandId) => commandStore.getCommand(commandId))
.filter((command): command is ComfyCommandImpl => command !== undefined)
.filter((command) => command !== undefined)
})
</script>

View File

@@ -1,31 +0,0 @@
<template>
<Button
v-show="canvasStore.nodeSelected"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),
showDelay: 1000
}"
severity="secondary"
text
data-testid="bypass-button"
@click="
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
"
>
<template #icon>
<i-game-icons:detour />
</template>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
const { t } = useI18n()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
</script>

View File

@@ -1,7 +1,6 @@
<template>
<div class="relative">
<Button
v-show="canvasStore.nodeSelected || canvasStore.groupSelected"
severity="secondary"
text
@click="() => (showColorPicker = !showColorPicker)"

View File

@@ -1,22 +0,0 @@
<template>
<Button
v-tooltip.top="{
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
showDelay: 1000
}"
severity="danger"
text
icon="pi pi-trash"
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
const { t } = useI18n()
const commandStore = useCommandStore()
</script>

View File

@@ -1,6 +1,5 @@
<template>
<Button
v-show="canvasStore.nodeSelected"
v-tooltip.top="{
value: isDisabled
? t('selectionToolbox.executeButton.disabledTooltip')

View File

@@ -1,27 +0,0 @@
<template>
<Button
v-tooltip.top="{
value:
st(`commands.${normalizeI18nKey(command.id)}.label`, '') || undefined,
showDelay: 1000
}"
severity="secondary"
text
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
@click="() => commandStore.execute(command.id)"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { st } from '@/i18n'
import { ComfyCommand, useCommandStore } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
defineProps<{
command: ComfyCommand
}>()
const commandStore = useCommandStore()
</script>

View File

@@ -1,25 +0,0 @@
<template>
<Button
v-show="canvasStore.nodeSelected || canvasStore.groupSelected"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_ToggleSelectedNodes_Pin.label'),
showDelay: 1000
}"
severity="secondary"
text
icon="pi pi-thumbtack"
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
const { t } = useI18n()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
</script>

View File

@@ -1,17 +0,0 @@
<template>
<Button
v-show="isRefreshable"
severity="info"
text
icon="pi pi-refresh"
@click="refreshSelected"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useRefreshableSelection } from '@/composables/useRefreshableSelection'
const { isRefreshable, refreshSelected } = useRefreshableSelection()
</script>

View File

@@ -1,135 +0,0 @@
<template>
<ScrollPanel
ref="scrollPanelRef"
class="w-full min-h-[400px] rounded-lg px-2 py-2 text-xs"
:pt="{ content: { id: 'chat-scroll-content' } }"
>
<div v-for="(item, i) in parsedHistory" :key="i" class="mb-4">
<!-- Prompt (user, right) -->
<span
:class="{
'opacity-40 pointer-events-none': editIndex !== null && i > editIndex
}"
>
<div class="flex justify-end mb-1">
<div
class="bg-gray-300 dark-theme:bg-gray-800 rounded-xl px-4 py-1 max-w-[80%] text-right"
>
<div class="break-words text-[12px]">{{ item.prompt }}</div>
</div>
</div>
<div class="flex justify-end mb-2 mr-1">
<CopyButton :text="item.prompt" />
<Button
v-tooltip="
editIndex === i ? $t('chatHistory.cancelEditTooltip') : null
"
text
rounded
class="!p-1 !h-4 !w-4 text-gray-400 hover:text-gray-600 dark-theme:hover:text-gray-200 transition"
pt:icon:class="!text-xs"
:icon="editIndex === i ? 'pi pi-times' : 'pi pi-pencil'"
:aria-label="
editIndex === i ? $t('chatHistory.cancelEdit') : $t('g.edit')
"
@click="editIndex === i ? handleCancelEdit() : handleEdit(i)"
/>
</div>
</span>
<!-- Response (LLM, left) -->
<ResponseBlurb
:text="item.response"
:class="{
'opacity-25 pointer-events-none': editIndex !== null && i >= editIndex
}"
>
<div v-html="nl2br(linkifyHtml(item.response))" />
</ResponseBlurb>
</div>
</ScrollPanel>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ScrollPanel from 'primevue/scrollpanel'
import { computed, nextTick, ref, watch } from 'vue'
import CopyButton from '@/components/graph/widgets/chatHistory/CopyButton.vue'
import ResponseBlurb from '@/components/graph/widgets/chatHistory/ResponseBlurb.vue'
import { ComponentWidget } from '@/scripts/domWidget'
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
const { widget, history = '[]' } = defineProps<{
widget?: ComponentWidget<string>
history: string
}>()
const editIndex = ref<number | null>(null)
const scrollPanelRef = ref<InstanceType<typeof ScrollPanel> | null>(null)
const parsedHistory = computed(() => JSON.parse(history || '[]'))
const findPromptInput = () =>
widget?.node.widgets?.find((w) => w.name === 'prompt')
let promptInput = findPromptInput()
const previousPromptInput = ref<string | null>(null)
const getPreviousResponseId = (index: number) =>
index > 0 ? parsedHistory.value[index - 1]?.response_id ?? '' : ''
const storePromptInput = () => {
promptInput ??= widget?.node.widgets?.find((w) => w.name === 'prompt')
if (!promptInput) return
previousPromptInput.value = String(promptInput.value)
}
const setPromptInput = (text: string, previousResponseId?: string | null) => {
promptInput ??= widget?.node.widgets?.find((w) => w.name === 'prompt')
if (!promptInput) return
if (previousResponseId !== null) {
promptInput.value = `<starting_point_id:${previousResponseId}>\n\n${text}`
} else {
promptInput.value = text
}
}
const handleEdit = (index: number) => {
if (!promptInput) return
editIndex.value = index
const prevResponseId = index === 0 ? 'start' : getPreviousResponseId(index)
const promptText = parsedHistory.value[index]?.prompt ?? ''
storePromptInput()
setPromptInput(promptText, prevResponseId)
}
const resetEditingState = () => {
editIndex.value = null
}
const handleCancelEdit = () => {
resetEditingState()
if (promptInput) {
promptInput.value = previousPromptInput.value ?? ''
}
}
const scrollChatToBottom = () => {
const content = document.getElementById('chat-scroll-content')
if (content) {
content.scrollTo({ top: content.scrollHeight, behavior: 'smooth' })
}
}
const onHistoryChanged = () => {
resetEditingState()
void nextTick(() => scrollChatToBottom())
}
watch(() => parsedHistory.value, onHistoryChanged, {
immediate: true,
deep: true
})
</script>

View File

@@ -11,7 +11,6 @@
v-if="isComponentWidget(widget)"
:model-value="widget.value"
:widget="widget"
v-bind="widget.props"
@update:model-value="emit('update:widgetValue', $event)"
/>
</div>

View File

@@ -1,36 +0,0 @@
<template>
<Button
v-tooltip="
copied ? $t('chatHistory.copiedTooltip') : $t('chatHistory.copyTooltip')
"
text
rounded
class="!p-1 !h-4 !w-6 text-gray-400 hover:text-gray-600 dark-theme:hover:text-gray-200 transition"
pt:icon:class="!text-xs"
:icon="copied ? 'pi pi-check' : 'pi pi-copy'"
:aria-label="
copied ? $t('chatHistory.copiedTooltip') : $t('chatHistory.copyTooltip')
"
@click="handleCopy"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { ref } from 'vue'
const { text } = defineProps<{
text: string
}>()
const copied = ref(false)
const handleCopy = async () => {
if (!text) return
await navigator.clipboard.writeText(text)
copied.value = true
setTimeout(() => {
copied.value = false
}, 1024)
}
</script>

View File

@@ -1,22 +0,0 @@
<template>
<span>
<div class="flex justify-start mb-1">
<div class="rounded-xl px-4 py-1 max-w-[80%]">
<div class="break-words text-[12px]">
<slot />
</div>
</div>
</div>
<div class="flex justify-start ml-1">
<CopyButton :text="text" />
</div>
</span>
</template>
<script setup lang="ts">
import CopyButton from '@/components/graph/widgets/chatHistory/CopyButton.vue'
defineProps<{
text: string
}>()
</script>

View File

@@ -52,9 +52,6 @@ const eventConfig = {
showPreviewChange: (value: boolean) => emit('showPreviewChange', value),
backgroundImageChange: (value: string) =>
emit('backgroundImageChange', value),
backgroundImageLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.loadingBackgroundImage')),
backgroundImageLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
upDirectionChange: (value: string) => emit('upDirectionChange', value),
edgeThresholdChange: (value: number) => emit('edgeThresholdChange', value),
modelLoadingStart: () =>
@@ -78,7 +75,7 @@ const eventConfig = {
watchEffect(async () => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
const rawLoad3d = toRaw(load3d.value)
rawLoad3d.setBackgroundColor(props.backgroundColor)
rawLoad3d.toggleGrid(props.showGrid)
@@ -87,25 +84,15 @@ watchEffect(async () => {
rawLoad3d.toggleCamera(props.cameraType)
rawLoad3d.togglePreview(props.showPreview)
await rawLoad3d.setBackgroundImage(props.backgroundImage)
rawLoad3d.setUpDirection(props.upDirection)
}
})
watch(
() => props.upDirection,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
rawLoad3d.setUpDirection(newValue)
}
}
)
watch(
() => props.materialMode,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value) as Load3d
const rawLoad3d = toRaw(load3d.value)
rawLoad3d.setMaterialMode(newValue)
}
@@ -115,9 +102,10 @@ watch(
watch(
() => props.edgeThreshold,
(newValue) => {
if (load3d.value && newValue) {
const rawLoad3d = toRaw(load3d.value) as Load3d
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value)
// @ts-expect-error fixme ts strict error
rawLoad3d.setEdgeThreshold(newValue)
}
}

View File

@@ -1,310 +0,0 @@
# Composables
This directory contains Vue composables for the ComfyUI frontend application. Composables are reusable pieces of logic that encapsulate stateful functionality and can be shared across components.
## Table of Contents
- [Overview](#overview)
- [Composable Architecture](#composable-architecture)
- [Composable Categories](#composable-categories)
- [Usage Guidelines](#usage-guidelines)
- [VueUse Library](#vueuse-library)
- [Development Guidelines](#development-guidelines)
- [Common Patterns](#common-patterns)
## Overview
Vue composables are a core part of Vue 3's Composition API and provide a way to extract and reuse stateful logic between multiple components. In ComfyUI, composables are used to encapsulate behaviors like:
- State management
- DOM interactions
- Feature-specific functionality
- UI behaviors
- Data fetching
Composables enable a more modular and functional approach to building components, allowing for better code reuse and separation of concerns. They help keep your component code cleaner by extracting complex logic into separate, reusable functions.
As described in the [Vue.js documentation](https://vuejs.org/guide/reusability/composables.html), composables are:
> Functions that leverage Vue's Composition API to encapsulate and reuse stateful logic.
## Composable Architecture
The composable architecture in ComfyUI follows these principles:
1. **Single Responsibility**: Each composable should focus on a specific concern
2. **Composition**: Composables can use other composables
3. **Reactivity**: Composables leverage Vue's reactivity system
4. **Reusability**: Composables are designed to be used across multiple components
The following diagram shows how composables fit into the application architecture:
```
┌─────────────────────────────────────────────────────────┐
│ Vue Components │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Component A │ │ Component B │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
└────────────┼───────────────────┼────────────────────────┘
│ │
▼ ▼
┌────────────┴───────────────────┴────────────────────────┐
│ Composables │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ useFeatureA │ │ useFeatureB │ │ useFeatureC │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
└─────────┼────────────────┼────────────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌─────────┴────────────────┴────────────────┴─────────────┐
│ Services & Stores │
└─────────────────────────────────────────────────────────┘
```
## Composable Categories
ComfyUI's composables are organized into several categories:
### Auth
Composables for authentication and user management:
- `useCurrentUser` - Provides access to the current user information
- `useFirebaseAuthActions` - Handles Firebase authentication operations
### Element
Composables for DOM and element interactions:
- `useAbsolutePosition` - Handles element positioning
- `useDomClipping` - Manages clipping of DOM elements
- `useResponsiveCollapse` - Manages responsive collapsing of elements
### Node
Composables for node-specific functionality:
- `useNodeBadge` - Handles node badge display and interaction
- `useNodeImage` - Manages node image preview
- `useNodeDragAndDrop` - Handles drag and drop for nodes
- `useNodeChatHistory` - Manages chat history for nodes
### Settings
Composables for settings management:
- `useSettingSearch` - Provides search functionality for settings
- `useSettingUI` - Manages settings UI interactions
### Sidebar
Composables for sidebar functionality:
- `useNodeLibrarySidebarTab` - Manages the node library sidebar tab
- `useQueueSidebarTab` - Manages the queue sidebar tab
- `useWorkflowsSidebarTab` - Manages the workflows sidebar tab
### Widgets
Composables for widget functionality:
- `useBooleanWidget` - Manages boolean widget interactions
- `useComboWidget` - Manages combo box widget interactions
- `useFloatWidget` - Manages float input widget interactions
- `useImagePreviewWidget` - Manages image preview widget
## Usage Guidelines
When using composables in components, follow these guidelines:
1. **Import and call** composables at the top level of the `setup` function
2. **Destructure returned values** to use in your component
3. **Respect reactivity** by not destructuring reactive objects
4. **Handle cleanup** by using `onUnmounted` when necessary
5. **Use VueUse** for common functionality instead of writing from scratch
Example usage:
```vue
<template>
<div
:class="{ 'dragging': isDragging }"
@mousedown="startDrag"
@mouseup="endDrag"
>
<img v-if="imageUrl" :src="imageUrl" alt="Node preview" />
</div>
</template>
<script setup lang="ts">
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop';
import { useNodeImage } from '@/composables/node/useNodeImage';
// Use composables at the top level
const { isDragging, startDrag, endDrag } = useNodeDragAndDrop();
const { imageUrl, loadImage } = useNodeImage();
// Use returned values in your component
</script>
```
## VueUse Library
ComfyUI leverages the [VueUse](https://vueuse.org/) library, which provides a collection of essential Vue Composition API utilities. Instead of implementing common functionality from scratch, prefer using VueUse composables for:
- DOM event handling (`useEventListener`, `useMouseInElement`)
- Element measurements (`useElementBounding`, `useElementSize`)
- Asynchronous operations (`useAsyncState`, `useFetch`)
- Animation and timing (`useTransition`, `useTimeout`, `useInterval`)
- Browser APIs (`useLocalStorage`, `useClipboard`)
- Sensors (`useDeviceMotion`, `useDeviceOrientation`)
- State management (`createGlobalState`, `useStorage`)
- ...and [more](https://vueuse.org/functions.html)
Examples:
```js
// Instead of manually adding/removing event listeners
import { useEventListener } from '@vueuse/core'
useEventListener(window, 'resize', handleResize)
// Instead of manually tracking element measurements
import { useElementBounding } from '@vueuse/core'
const { width, height, top, left } = useElementBounding(elementRef)
// Instead of manual async state management
import { useAsyncState } from '@vueuse/core'
const { state, isReady, isLoading } = useAsyncState(
fetch('https://api.example.com/data').then(r => r.json()),
{ data: [] }
)
```
For a complete list of available functions, see the [VueUse documentation](https://vueuse.org/functions.html).
## Development Guidelines
When creating or modifying composables, follow these best practices:
1. **Name with `use` prefix**: All composables should start with "use"
2. **Return an object**: Composables should return an object with named properties/methods
3. **Handle cleanup**: Use `onUnmounted` to clean up resources
4. **Document parameters and return values**: Add JSDoc comments
5. **Test composables**: Write unit tests for composable functionality
6. **Use VueUse**: Leverage VueUse composables instead of reimplementing common functionality
7. **Implement proper cleanup**: Cancel debounced functions, pending requests, and clear maps
8. **Use watchDebounced/watchThrottled**: For performance-sensitive reactive operations
### Composable Template
Here's a template for creating a new composable:
```typescript
import { ref, computed, onMounted, onUnmounted } from 'vue';
/**
* Composable for [functionality description]
* @param options Configuration options
* @returns Object containing state and methods
*/
export function useExample(options = {}) {
// State
const state = ref({
// Initial state
});
// Computed values
const derivedValue = computed(() => {
// Compute from state
return state.value.someProperty;
});
// Methods
function doSomething() {
// Implementation
}
// Lifecycle hooks
onMounted(() => {
// Setup
});
onUnmounted(() => {
// Cleanup
});
// Return exposed state and methods
return {
state,
derivedValue,
doSomething
};
}
```
## Common Patterns
Composables in ComfyUI frequently use these patterns:
### State Management
```typescript
export function useState() {
const count = ref(0);
function increment() {
count.value++;
}
return {
count,
increment
};
}
```
### Event Handling with VueUse
```typescript
import { useEventListener } from '@vueuse/core';
export function useKeyPress(key) {
const isPressed = ref(false);
useEventListener('keydown', (e) => {
if (e.key === key) {
isPressed.value = true;
}
});
useEventListener('keyup', (e) => {
if (e.key === key) {
isPressed.value = false;
}
});
return { isPressed };
}
```
### Fetch & Load with VueUse
```typescript
import { useAsyncState } from '@vueuse/core';
export function useFetchData(url) {
const { state: data, isLoading, error, execute: refresh } = useAsyncState(
async () => {
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch data');
return response.json();
},
null,
{ immediate: true }
);
return { data, isLoading, error, refresh };
}
```
For more information on Vue composables, refer to the [Vue.js Composition API documentation](https://vuejs.org/guide/reusability/composables.html) and the [VueUse documentation](https://vueuse.org/).

View File

@@ -1,396 +0,0 @@
import { ApiNodeCostRecord } from '@/types/apiNodeTypes'
/**
* API Node cost data sourced from pricing database
*
* GENERATED FILE - DO NOT EDIT DIRECTLY
* Generated from Price Run Range for each API Node CSV
*/
export const apiNodeCosts: ApiNodeCostRecord = {
FluxProCannyNode: {
vendor: 'Unknown',
nodeName: 'Flux 1: Canny Control Image',
pricingParams: '-',
pricePerRunRange: '$0.05',
displayPrice: '$0.05/Run'
},
FluxProDepthNode: {
vendor: 'Unknown',
nodeName: 'Flux 1: Depth Control Image',
pricingParams: '-',
pricePerRunRange: '$0.05',
displayPrice: '$0.05/Run'
},
FluxProExpandNode: {
vendor: 'Unknown',
nodeName: 'Flux 1: Expand Image',
pricingParams: '-',
pricePerRunRange: '$0.05',
rateDocumentation: 'https://docs.bfl.ml/pricing/',
displayPrice: '$0.05/Run'
},
FluxProFillNode: {
vendor: 'Unknown',
nodeName: 'Flux 1: Fill Image',
pricingParams: '-',
pricePerRunRange: '$0.05',
displayPrice: '$0.05/Run'
},
FluxProUltraImageNode: {
vendor: 'Unknown',
nodeName: 'Flux 1.1: [pro] Ultra Image',
pricingParams: '-',
pricePerRunRange: '$0.06',
displayPrice: '$0.06/Run'
},
IdeogramV1: {
vendor: 'Unknown',
nodeName: 'Ideogram V1',
pricingParams: '-',
pricePerRunRange: '$0.06',
rateDocumentation: 'https://about.ideogram.ai/api-pricing',
displayPrice: '$0.06/Run'
},
IdeogramV2: {
vendor: 'Unknown',
nodeName: 'Ideogram V2',
pricingParams: '-',
pricePerRunRange: '$0.08',
displayPrice: '$0.08/Run'
},
IdeogramV3: {
vendor: 'Unknown',
nodeName: 'Ideogram V3',
pricingParams: 'rendering_speed',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
KlingCameraControlI2VNode: {
vendor: 'Unknown',
nodeName: 'Kling Image to Video (Camera Control)',
pricingParams: '-',
pricePerRunRange: '$0.49',
displayPrice: '$0.49/Run'
},
KlingCameraControlT2VNode: {
vendor: 'Unknown',
nodeName: 'Kling Text to Video (Camera Control)',
pricingParams: '-',
pricePerRunRange: '$0.14',
displayPrice: '$0.14/Run'
},
KlingDualCharacterVideoEffectNode: {
vendor: 'Unknown',
nodeName: 'Kling Dual Character Video Effects',
pricingParams: 'Priced the same as t2v based on mode, model, and duration.',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
KlingImage2VideoNode: {
vendor: 'Unknown',
nodeName: 'Kling Image to Video',
pricingParams: 'Same as Text to Video',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
KlingImageGenerationNode: {
vendor: 'Unknown',
nodeName: 'Kling Image Generation',
pricingParams: 'modality | model',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
KlingLipSyncAudioToVideoNode: {
vendor: 'Unknown',
nodeName: 'Kling Lip Sync Video with Audio',
pricingParams: 'duration of input video',
pricePerRunRange: '$0.07',
displayPrice: '$0.07/Run'
},
KlingLipSyncTextToVideoNode: {
vendor: 'Unknown',
nodeName: 'Kling Lip Sync Video with Text',
pricingParams: 'duration of input video',
pricePerRunRange: '$0.07',
displayPrice: '$0.07/Run'
},
KlingSingleImageVideoEffectNode: {
vendor: 'Unknown',
nodeName: 'Kling Video Effects',
pricingParams: 'effect_scene',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
KlingStartEndFrameNode: {
vendor: 'Unknown',
nodeName: 'Kling Start-End Frame to Video',
pricingParams: 'Same as text to video',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
KlingTextToVideoNode: {
vendor: 'Unknown',
nodeName: 'Kling Text to Video',
pricingParams: 'model | duration | mode',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
KlingVideoExtendNode: {
vendor: 'Unknown',
nodeName: 'Kling Video Extend',
pricingParams: '-',
pricePerRunRange: '$0.28',
displayPrice: '$0.28/Run'
},
KlingVirtualTryOnNode: {
vendor: 'Unknown',
nodeName: 'Kling Virtual Try On',
pricingParams: '-',
pricePerRunRange: '$0.07',
displayPrice: '$0.07/Run'
},
LumaImageToVideoNode: {
vendor: 'Unknown',
nodeName: 'Luma Image to Video',
pricingParams: 'Same as Text to Video',
pricePerRunRange: 'dynamic',
rateDocumentation: 'https://lumalabs.ai/api/pricing',
displayPrice: 'Variable pricing'
},
LumaVideoNode: {
vendor: 'Unknown',
nodeName: 'Luma Text to Video',
pricingParams: 'model | resolution | duration',
pricePerRunRange: 'dynamic',
rateDocumentation: 'https://lumalabs.ai/api/pricing',
displayPrice: 'Variable pricing'
},
MinimaxImageToVideoNode: {
vendor: 'Unknown',
nodeName: 'MiniMax Image to Video',
pricingParams: '-',
pricePerRunRange: '$0.43',
rateDocumentation: 'https://www.minimax.io/price',
displayPrice: '$0.43/Run'
},
MinimaxTextToVideoNode: {
vendor: 'Unknown',
nodeName: 'MiniMax Text to Video',
pricingParams: '-',
pricePerRunRange: '$0.43',
rateDocumentation: 'https://www.minimax.io/price',
displayPrice: '$0.43/Run'
},
OpenAIDalle2: {
vendor: 'Unknown',
nodeName: 'dall-e-2',
pricingParams: 'size',
pricePerRunRange: 'dynamic',
rateDocumentation: 'https://platform.openai.com/docs/pricing',
displayPrice: 'Variable pricing'
},
OpenAIDalle3: {
vendor: 'Unknown',
nodeName: 'dall-e-3',
pricingParams: '1024×1024 | hd',
pricePerRunRange: '$0.08',
displayPrice: '$0.08/Run'
},
OpenAIGPTImage1: {
vendor: 'Unknown',
nodeName: 'gpt-image-1',
pricingParams: 'medium',
pricePerRunRange: '$[0.046 - 0.07]',
displayPrice: '$0.07/Run'
},
PikaImageToVideoNode2_2: {
vendor: 'Unknown',
nodeName: 'Pika Image to Video',
pricingParams: 'duration | resolution',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
PikaScenesV2_2: {
vendor: 'Unknown',
nodeName: 'Pika Scenes (Video Image Composition)',
pricingParams: 'duration | resolution',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
PikaStartEndFrameNode2_2: {
vendor: 'Unknown',
nodeName: 'Pika Start and End Frame to Video',
pricingParams: 'duration | resolution',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
PikaTextToVideoNode2_2: {
vendor: 'Unknown',
nodeName: 'Pika Text to Video',
pricingParams: 'duration | resolution',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
Pikadditions: {
vendor: 'Unknown',
nodeName: 'Pikadditions (Video Object Insertion)',
pricingParams: '-',
pricePerRunRange: '$0.3',
displayPrice: '$0.3/Run'
},
Pikaffects: {
vendor: 'Unknown',
nodeName: 'Pikaffects (Video Effects)',
pricingParams: '-',
pricePerRunRange: '0.45',
displayPrice: '$0.45/Run'
},
Pikaswaps: {
vendor: 'Unknown',
nodeName: 'Pika Swaps (Video Object Replacement)',
pricingParams: '-',
pricePerRunRange: '$0.3',
displayPrice: '$0.3/Run'
},
PixverseImageToVideoNode: {
vendor: 'Unknown',
nodeName: 'PixVerse Image to Video',
pricingParams: 'same as text to video',
pricePerRunRange: '$0.9',
displayPrice: '$0.9/Run'
},
PixverseTextToVideoNode: {
vendor: 'Unknown',
nodeName: 'PixVerse Text to Video',
pricingParams: 'duration | quality | motion_mode',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
PixverseTransitionVideoNode: {
vendor: 'Unknown',
nodeName: 'PixVerse Transition Video',
pricingParams: 'same as text to video',
pricePerRunRange: '$0.9',
displayPrice: '$0.9/Run'
},
RecraftCrispUpscaleNode: {
vendor: 'Unknown',
nodeName: 'Recraft Crisp Upscale Image',
pricingParams: '-',
pricePerRunRange: '$0.004',
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
displayPrice: '$0.004/Run'
},
RecraftImageInpaintingNode: {
vendor: 'Unknown',
nodeName: 'Recraft Image Inpainting',
pricingParams: 'n',
pricePerRunRange: '$$0.04 x n',
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
displayPrice: '$$0.04 x n/Run'
},
RecraftImageToImageNode: {
vendor: 'Unknown',
nodeName: 'Recraft Image to Image',
pricingParams: 'n',
pricePerRunRange: '$0.04 x n',
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
displayPrice: '$0.04 x n/Run'
},
RecraftRemoveBackgroundNode: {
vendor: 'Unknown',
nodeName: 'Recraft Remove Background',
pricingParams: '-',
pricePerRunRange: '$0.01',
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
displayPrice: '$0.01/Run'
},
RecraftReplaceBackgroundNode: {
vendor: 'Unknown',
nodeName: 'Recraft Replace Background',
pricingParams: 'n',
pricePerRunRange: '$0.04',
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
displayPrice: '$0.04/Run'
},
RecraftTextToImageNode: {
vendor: 'Unknown',
nodeName: 'Recraft Text to Image',
pricingParams: 'model | n',
pricePerRunRange: '$0.04 x n',
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
displayPrice: '$0.04 x n/Run'
},
RecraftTextToVectorNode: {
vendor: 'Unknown',
nodeName: 'Recraft Text to Vector',
pricingParams: 'model | n',
pricePerRunRange: '$0.08 x n',
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
displayPrice: '$0.08 x n/Run'
},
RecraftVectorizeImageNode: {
vendor: 'Unknown',
nodeName: 'Recraft Vectorize Image',
pricingParams: '-',
pricePerRunRange: '$0.01',
rateDocumentation: 'https://www.recraft.ai/docs#pricing',
displayPrice: '$0.01/Run'
},
StabilityStableImageSD_3_5Node: {
vendor: 'Unknown',
nodeName: 'Stability AI Stable Diffusion 3.5 Image',
pricingParams: 'model',
pricePerRunRange: 'dynamic',
displayPrice: 'Variable pricing'
},
StabilityStableImageUltraNode: {
vendor: 'Unknown',
nodeName: 'Stability AI Stable Image Ultra',
pricingParams: '-',
pricePerRunRange: '$0.08',
displayPrice: '$0.08/Run'
},
StabilityUpscaleConservativeNode: {
vendor: 'Unknown',
nodeName: 'Stability AI Upscale Conservative',
pricingParams: '-',
pricePerRunRange: '$0.25',
displayPrice: '$0.25/Run'
},
StabilityUpscaleCreativeNode: {
vendor: 'Unknown',
nodeName: 'Stability AI Upscale Creative',
pricingParams: '-',
pricePerRunRange: '$0.25',
displayPrice: '$0.25/Run'
},
StabilityUpscaleFastNode: {
vendor: 'Unknown',
nodeName: 'Stability AI Upscale Fast',
pricingParams: '-',
pricePerRunRange: '$0.01',
displayPrice: '$0.01/Run'
},
VeoVideoGenerationNode: {
vendor: 'Unknown',
nodeName: 'Google Veo2 Video Generation',
pricingParams: '10s',
pricePerRunRange: '$5.0',
rateDocumentation:
'https://cloud.google.com/vertex-ai/generative-ai/pricing',
displayPrice: '$5.0/Run'
}
}
/**
* Get the display price for a node
* Returns a default value if the node isn't found
*/
export function getNodeDisplayPrice(
nodeName: string,
defaultPrice = '0.02/Run (approx)'
): string {
return apiNodeCosts[nodeName]?.displayPrice || defaultPrice
}

View File

@@ -13,8 +13,6 @@ import { useSettingStore } from '@/stores/settingStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { NodeBadgeMode } from '@/types/nodeSource'
import { useNodePricing } from './useNodePricing'
/**
* Add LGraphBadge to LGraphNode based on settings.
*
@@ -59,8 +57,6 @@ export const useNodeBadge = () => {
}
onMounted(() => {
const nodePricing = useNodePricing()
extensionStore.registerExtension({
name: 'Comfy.NodeBadge',
nodeCreated(node: LGraphNode) {
@@ -99,12 +95,9 @@ export const useNodeBadge = () => {
node.badges.push(() => badge.value)
if (node.constructor.nodeData?.api_node) {
// Get price from our mapping service
const price = nodePricing.getNodePriceDisplay(node)
const creditsBadge = computed(() => {
return new LGraphBadge({
text: price ?? '',
text: '',
iconOptions: {
unicode: '\ue96b',
fontFamily: 'PrimeIcons',
@@ -115,7 +108,9 @@ export const useNodeBadge = () => {
fgColor:
colorPaletteStore.completedActivePalette.colors.litegraph_base
.BADGE_FG_COLOR,
bgColor: '#8D6932'
bgColor:
colorPaletteStore.completedActivePalette.colors.litegraph_base
.BADGE_BG_COLOR
})
})

View File

@@ -1,60 +0,0 @@
import { LGraphNode } from '@comfyorg/litegraph'
import type ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue'
import { useChatHistoryWidget } from '@/composables/widgets/useChatHistoryWidget'
const CHAT_HISTORY_WIDGET_NAME = '$$node-chat-history'
/**
* Composable for handling node text previews
*/
export function useNodeChatHistory(
options: {
minHeight?: number
props?: Omit<InstanceType<typeof ChatHistoryWidget>['$props'], 'widget'>
} = {}
) {
const chatHistoryWidget = useChatHistoryWidget(options)
const findChatHistoryWidget = (node: LGraphNode) =>
node.widgets?.find((w) => w.name === CHAT_HISTORY_WIDGET_NAME)
const addChatHistoryWidget = (node: LGraphNode) =>
chatHistoryWidget(node, {
name: CHAT_HISTORY_WIDGET_NAME,
type: 'chatHistory'
})
/**
* Shows chat history for a node
* @param node The graph node to show the chat history for
*/
function showChatHistory(node: LGraphNode) {
if (!findChatHistoryWidget(node)) {
addChatHistoryWidget(node)
}
node.setDirtyCanvas?.(true)
}
/**
* Removes chat history from a node
* @param node The graph node to remove the chat history from
*/
function removeChatHistory(node: LGraphNode) {
if (!node.widgets) return
const widgetIdx = node.widgets.findIndex(
(w) => w.name === CHAT_HISTORY_WIDGET_NAME
)
if (widgetIdx > -1) {
node.widgets[widgetIdx].onRemove?.()
node.widgets.splice(widgetIdx, 1)
}
}
return {
showChatHistory,
removeChatHistory
}
}

View File

@@ -1,103 +0,0 @@
import type { LGraphNode } from '@comfyorg/litegraph'
// Direct mapping of node names to prices
export const NODE_PRICES: Record<string, string> = {
// OpenAI models
OpenAIDalle2: '$0.02/Run',
OpenAIDalle3: '$0.08/Run',
OpenAIGPTImage1: '$0.07/Run',
// Ideogram models
IdeogramV1: '$0.06/Run',
IdeogramV2: '$0.08/Run',
IdeogramV3: 'Variable pricing',
// Minimax models
MinimaxTextToVideoNode: '$0.43/Run',
MinimaxImageToVideoNode: '$0.43/Run',
// Google Veo
VeoVideoGenerationNode: '$5.0/Run',
// Kling models
KlingTextToVideoNode: 'Variable pricing',
KlingImage2VideoNode: 'Variable pricing',
KlingCameraControlI2VNode: '$0.49/Run',
KlingCameraControlT2VNode: '$0.14/Run',
KlingStartEndFrameNode: 'Variable pricing',
KlingVideoExtendNode: '$0.28/Run',
KlingLipSyncAudioToVideoNode: '$0.07/Run',
KlingLipSyncTextToVideoNode: '$0.07/Run',
KlingVirtualTryOnNode: '$0.07/Run',
KlingImageGenerationNode: 'Variable pricing',
KlingSingleImageVideoEffectNode: 'Variable pricing',
KlingDualCharacterVideoEffectNode: 'Variable pricing',
// Flux Pro models
FluxProUltraImageNode: '$0.06/Run',
FluxProExpandNode: '$0.05/Run',
FluxProFillNode: '$0.05/Run',
FluxProCannyNode: '$0.05/Run',
FluxProDepthNode: '$0.05/Run',
// Luma models
LumaVideoNode: 'Variable pricing',
LumaImageToVideoNode: 'Variable pricing',
LumaImageNode: 'Variable pricing',
LumaImageModifyNode: 'Variable pricing',
// Recraft models
RecraftTextToImageNode: '$0.04/Run',
RecraftImageToImageNode: '$0.04/Run',
RecraftImageInpaintingNode: '$0.04/Run',
RecraftTextToVectorNode: '$0.08/Run',
RecraftVectorizeImageNode: '$0.01/Run',
RecraftRemoveBackgroundNode: '$0.01/Run',
RecraftReplaceBackgroundNode: '$0.04/Run',
RecraftCrispUpscaleNode: '$0.004/Run',
RecraftCreativeUpscaleNode: '$0.004/Run',
// Pixverse models
PixverseTextToVideoNode: '$0.9/Run',
PixverseImageToVideoNode: '$0.9/Run',
PixverseTransitionVideoNode: '$0.9/Run',
// Stability models
StabilityStableImageUltraNode: '$0.08/Run',
StabilityStableImageSD_3_5Node: 'Variable pricing',
StabilityUpscaleConservativeNode: '$0.25/Run',
StabilityUpscaleCreativeNode: '$0.25/Run',
StabilityUpscaleFastNode: '$0.01/Run',
// Pika models
PikaImageToVideoNode2_2: 'Variable pricing',
PikaTextToVideoNode2_2: 'Variable pricing',
PikaScenesV2_2: 'Variable pricing',
PikaStartEndFrameNode2_2: 'Variable pricing',
Pikadditions: '$0.3/Run',
Pikaswaps: '$0.3/Run',
Pikaffects: '$0.45/Run'
}
/**
* Simple utility function to get the price for a node
* Returns a formatted price string or default value if the node isn't found
*/
export function getNodePrice(
node: LGraphNode,
defaultPrice = '0.02/Run (approx)'
): string {
if (!node.constructor.nodeData?.api_node) {
return ''
}
return NODE_PRICES[node.constructor.name] || defaultPrice
}
/**
* Composable to get node pricing information for API nodes
*/
export const useNodePricing = () => {
/**
* Get the price display for a node
*/
const getNodePriceDisplay = (node: LGraphNode): string => {
if (!node.constructor.nodeData?.api_node) {
return ''
}
return NODE_PRICES[node.constructor.name] || '0.02/Run (approx)'
}
return {
getNodePriceDisplay
}
}

View File

@@ -1,43 +0,0 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import { ref } from 'vue'
import ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
const PADDING = 16
export const useChatHistoryWidget = (
options: {
props?: Omit<InstanceType<typeof ChatHistoryWidget>['$props'], 'widget'>
} = {}
) => {
const widgetConstructor: ComfyWidgetConstructorV2 = (
node: LGraphNode,
inputSpec: InputSpec
) => {
const widgetValue = ref<string>('')
const widget = new ComponentWidgetImpl<
string | object,
InstanceType<typeof ChatHistoryWidget>['$props']
>({
node,
name: inputSpec.name,
component: ChatHistoryWidget,
props: options.props,
inputSpec,
options: {
getValue: () => widgetValue.value,
setValue: (value: string | object) => {
widgetValue.value = typeof value === 'string' ? value : String(value)
},
getMinHeight: () => 400 + PADDING
}
})
addWidget(node, widget)
return widget
}
return widgetConstructor
}

View File

@@ -8,11 +8,7 @@ import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
const PADDING = 16
export const useTextPreviewWidget = (
options: {
minHeight?: number
} = {}
) => {
export const useTextPreviewWidget = () => {
const widgetConstructor: ComfyWidgetConstructorV2 = (
node: LGraphNode,
inputSpec: InputSpec
@@ -28,7 +24,7 @@ export const useTextPreviewWidget = (
setValue: (value: string | object) => {
widgetValue.value = typeof value === 'string' ? value : String(value)
},
getMinHeight: () => options.minHeight ?? 42 + PADDING
getMinHeight: () => 42 + PADDING
}
})
addWidget(node, widget)

View File

@@ -1,63 +0,0 @@
const apiNodeNames = [
'IdeogramV1',
'IdeogramV2',
'IdeogramV3',
'MinimaxTextToVideoNode',
'MinimaxImageToVideoNode',
'VeoVideoGenerationNode',
'KlingCameraControls',
'KlingTextToVideoNode',
'KlingImage2VideoNode',
'KlingCameraControlI2VNode',
'KlingCameraControlT2VNode',
'KlingStartEndFrameNode',
'KlingVideoExtendNode',
'KlingLipSyncAudioToVideoNode',
'KlingLipSyncTextToVideoNode',
'KlingVirtualTryOnNode',
'KlingImageGenerationNode',
'KlingSingleImageVideoEffectNode',
'KlingDualCharacterVideoEffectNode',
'FluxProUltraImageNode',
'FluxProExpandNode',
'FluxProFillNode',
'FluxProCannyNode',
'FluxProDepthNode',
'LumaImageNode',
'LumaImageModifyNode',
'LumaVideoNode',
'LumaImageToVideoNode',
'LumaReferenceNode',
'LumaConceptsNode',
'RecraftTextToImageNode',
'RecraftImageToImageNode',
'RecraftImageInpaintingNode',
'RecraftTextToVectorNode',
'RecraftVectorizeImageNode',
'RecraftRemoveBackgroundNode',
'RecraftReplaceBackgroundNode',
'RecraftCrispUpscaleNode',
'RecraftCreativeUpscaleNode',
'RecraftStyleV3RealisticImage',
'RecraftStyleV3DigitalIllustration',
'RecraftStyleV3LogoRaster',
'RecraftStyleV3InfiniteStyleLibrary',
'RecraftColorRGB',
'RecraftControls',
'PixverseTextToVideoNode',
'PixverseImageToVideoNode',
'PixverseTransitionVideoNode',
'PixverseTemplateNode',
'StabilityStableImageUltraNode',
'StabilityStableImageSD_3_5Node',
'StabilityUpscaleConservativeNode',
'StabilityUpscaleCreativeNode',
'StabilityUpscaleFastNode',
'PikaImageToVideoNode2_2',
'PikaTextToVideoNode2_2',
'PikaScenesV2_2',
'Pikadditions',
'Pikaswaps',
'Pikaffects',
'PikaStartEndFrameNode2_2'
]

View File

@@ -1,139 +0,0 @@
# Core Extensions
This directory contains the core extensions that provide essential functionality to the ComfyUI frontend.
## Table of Contents
- [Overview](#overview)
- [Extension Architecture](#extension-architecture)
- [Core Extensions](#core-extensions)
- [Extension Development](#extension-development)
- [Extension Hooks](#extension-hooks)
- [Further Reading](#further-reading)
## Overview
Extensions in ComfyUI are modular JavaScript modules that extend and enhance the functionality of the frontend. The extensions in this directory are considered "core" as they provide fundamental features that are built into ComfyUI by default.
## Extension Architecture
ComfyUI's extension system follows these key principles:
1. **Registration-based:** Extensions must register themselves with the application using `app.registerExtension()`
2. **Hook-driven:** Extensions interact with the system through predefined hooks
3. **Non-intrusive:** Extensions should avoid directly modifying core objects where possible
## Core Extensions List
The core extensions include:
| Extension | Description |
|-----------|-------------|
| clipspace.ts | Implements the Clipspace feature for temporary image storage |
| dynamicPrompts.ts | Provides dynamic prompt generation capabilities |
| groupNode.ts | Implements the group node functionality to organize workflows |
| load3d.ts | Supports 3D model loading and visualization |
| maskeditor.ts | Implements the mask editor for image masking operations |
| noteNode.ts | Adds note nodes for documentation within workflows |
| rerouteNode.ts | Implements reroute nodes for cleaner workflow connections |
| uploadImage.ts | Handles image upload functionality |
| webcamCapture.ts | Provides webcam capture capabilities |
| widgetInputs.ts | Implements various widget input types |
## Extension Development
When developing or modifying extensions, follow these best practices:
1. **Use provided hooks** rather than directly modifying core application objects
2. **Maintain compatibility** with other extensions
3. **Follow naming conventions** for both extension names and settings
4. **Properly document** extension hooks and functionality
5. **Test with other extensions** to ensure no conflicts
### Extension Registration
Extensions are registered using the `app.registerExtension()` method:
```javascript
app.registerExtension({
name: "MyExtension",
// Hook implementations
async init() {
// Implementation
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Implementation
}
// Other hooks as needed
});
```
## Extension Hooks
ComfyUI extensions can implement various hooks that are called at specific points in the application lifecycle:
### Hook Execution Sequence
#### Web Page Load
```
init
addCustomNodeDefs
getCustomWidgets
beforeRegisterNodeDef [repeated multiple times]
registerCustomNodes
beforeConfigureGraph
nodeCreated
loadedGraphNode
afterConfigureGraph
setup
```
#### Loading Workflow
```
beforeConfigureGraph
beforeRegisterNodeDef [zero, one, or multiple times]
nodeCreated [repeated multiple times]
loadedGraphNode [repeated multiple times]
afterConfigureGraph
```
#### Adding New Node
```
nodeCreated
```
### Key Hooks
| Hook | Description |
|------|-------------|
| `init` | Called after canvas creation but before nodes are added |
| `setup` | Called after the application is fully set up and running |
| `addCustomNodeDefs` | Called before nodes are registered with the graph |
| `getCustomWidgets` | Allows extensions to add custom widgets |
| `beforeRegisterNodeDef` | Allows extensions to modify nodes before registration |
| `registerCustomNodes` | Allows extensions to register additional nodes |
| `loadedGraphNode` | Called when a node is reloaded onto the graph |
| `nodeCreated` | Called after a node's constructor |
| `beforeConfigureGraph` | Called before a graph is configured |
| `afterConfigureGraph` | Called after a graph is configured |
| `getSelectionToolboxCommands` | Allows extensions to add commands to the selection toolbox |
For the complete list of available hooks and detailed descriptions, see the [ComfyExtension interface in comfy.ts](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/types/comfy.ts).
## Further Reading
For more detailed information about ComfyUI's extension system, refer to the official documentation:
- [JavaScript Extension Overview](https://docs.comfy.org/custom-nodes/js/javascript_overview)
- [JavaScript Hooks](https://docs.comfy.org/custom-nodes/js/javascript_hooks)
- [JavaScript Objects and Hijacking](https://docs.comfy.org/custom-nodes/js/javascript_objects_and_hijacking)
- [JavaScript Settings](https://docs.comfy.org/custom-nodes/js/javascript_settings)
- [JavaScript Examples](https://docs.comfy.org/custom-nodes/js/javascript_examples)
Also, check the main [README.md](https://github.com/Comfy-Org/ComfyUI_frontend#developer-apis) section on Developer APIs for the latest information on extension APIs and features.

View File

@@ -82,8 +82,6 @@ export class SceneManager implements SceneManagerInterface {
}
async setBackgroundImage(uploadPath: string): Promise<void> {
this.eventManager.emitEvent('backgroundImageLoadingStart', null)
if (uploadPath === '') {
this.removeBackgroundImage()
return
@@ -125,9 +123,7 @@ export class SceneManager implements SceneManagerInterface {
)
this.eventManager.emitEvent('backgroundImageChange', uploadPath)
this.eventManager.emitEvent('backgroundImageLoadingEnd', null)
} catch (error) {
this.eventManager.emitEvent('backgroundImageLoadingEnd', null)
console.error('Error loading background image:', error)
}
}
@@ -143,7 +139,6 @@ export class SceneManager implements SceneManagerInterface {
this.backgroundTexture.dispose()
this.backgroundTexture = null
}
this.eventManager.emitEvent('backgroundImageLoadingEnd', null)
}
updateBackgroundSize(

View File

@@ -114,9 +114,7 @@
"learnMore": "Learn more",
"amount": "Amount",
"unknownError": "Unknown error",
"title": "Title",
"edit": "Edit",
"copy": "Copy"
"title": "Title"
},
"manager": {
"title": "Custom Nodes Manager",
@@ -1262,8 +1260,7 @@
"failedToAccessBillingPortal": "Failed to access billing portal: {error}",
"failedToPurchaseCredits": "Failed to purchase credits: {error}",
"unauthorizedDomain": "Your domain {domain} is not authorized to use this service. Please contact {email} to add your domain to the whitelist.",
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.",
"nothingSelected": "Nothing selected"
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option."
},
"auth": {
"apiKey": {
@@ -1384,17 +1381,30 @@
"notSet": "Not set",
"updatePassword": "Update Password"
},
"apiNodesNews": {
"introducing": "Introducing",
"subtitle": "Access all the popular paid models natively in ComfyUI",
"steps": {
"step1": {
"title": "Login/Create an account:",
"subtitle": "Settings > User > Login"
},
"step2": {
"title": "Purchase credits:",
"subtitle": "Settings > Credits > Buy Credits"
},
"step3": {
"title": "Locate new API Nodes under 'API Node' section and add to the canvas"
},
"step4": {
"title": "Run!"
}
}
},
"selectionToolbox": {
"executeButton": {
"tooltip": "Execute to selected output nodes (Highlighted with orange border)",
"disabledTooltip": "No output nodes selected"
}
},
"chatHistory": {
"cancelEdit": "Cancel",
"editTooltip": "Edit message",
"cancelEditTooltip": "Cancel edit",
"copiedTooltip": "Copied",
"copyTooltip": "Copy message to clipboard"
}
}

View File

@@ -4,6 +4,26 @@
"title": "Nodo(s) de API",
"totalCost": "Costo total"
},
"apiNodesNews": {
"introducing": "Presentamos",
"steps": {
"step1": {
"subtitle": "Configuración > Usuario > Iniciar sesión",
"title": "Inicia sesión/Crea una cuenta:"
},
"step2": {
"subtitle": "Configuración > Créditos > Comprar créditos",
"title": "Compra créditos:"
},
"step3": {
"title": "Ubica los nuevos nodos API en la sección 'API Node' y agrégalos al lienzo"
},
"step4": {
"title": "¡Ejecuta!"
}
},
"subtitle": "Todos los modelos externos ahora disponibles en ComfyUI"
},
"apiNodesSignInDialog": {
"message": "Este flujo de trabajo contiene nodos de API, que requieren que inicies sesión en tu cuenta para poder ejecutar.",
"title": "Se requiere iniciar sesión para usar los nodos de API"
@@ -82,13 +102,6 @@
"title": "Crea una cuenta"
}
},
"chatHistory": {
"cancelEdit": "Cancelar",
"cancelEditTooltip": "Cancelar edición",
"copiedTooltip": "Copiado",
"copyTooltip": "Copiar mensaje al portapapeles",
"editTooltip": "Editar mensaje"
},
"clipboard": {
"errorMessage": "Error al copiar al portapapeles",
"errorNotSupported": "API del portapapeles no soportada en su navegador",
@@ -264,7 +277,6 @@
"continue": "Continuar",
"control_after_generate": "control después de generar",
"control_before_generate": "control antes de generar",
"copy": "Copiar",
"copyToClipboard": "Copiar al portapapeles",
"currentUser": "Usuario actual",
"customize": "Personalizar",
@@ -276,7 +288,6 @@
"disableAll": "Deshabilitar todo",
"disabling": "Deshabilitando",
"download": "Descargar",
"edit": "Editar",
"empty": "Vacío",
"enableAll": "Habilitar todo",
"enabled": "Habilitado",
@@ -1343,7 +1354,6 @@
"no3dSceneToExport": "No hay escena 3D para exportar",
"noTemplatesToExport": "No hay plantillas para exportar",
"nodeDefinitionsUpdated": "Definiciones de nodos actualizadas",
"nothingSelected": "Nada seleccionado",
"nothingToGroup": "Nada para agrupar",
"nothingToQueue": "Nada para poner en cola",
"pendingTasksDeleted": "Tareas pendientes eliminadas",

View File

@@ -4,6 +4,26 @@
"title": "Nœud(s) API",
"totalCost": "Coût total"
},
"apiNodesNews": {
"introducing": "Présentation",
"steps": {
"step1": {
"subtitle": "Paramètres > Utilisateur > Connexion",
"title": "Connectez-vous / Créez un compte :"
},
"step2": {
"subtitle": "Paramètres > Crédits > Acheter des crédits",
"title": "Achetez des crédits :"
},
"step3": {
"title": "Trouvez les nouveaux nœuds API dans la section 'API Node' et ajoutez-les à la toile"
},
"step4": {
"title": "Lancez !"
}
},
"subtitle": "Tous les modèles externes sont désormais disponibles dans ComfyUI"
},
"apiNodesSignInDialog": {
"message": "Ce flux de travail contient des nœuds API, qui nécessitent que vous soyez connecté à votre compte pour pouvoir fonctionner.",
"title": "Connexion requise pour utiliser les nœuds API"
@@ -82,13 +102,6 @@
"title": "Créer un compte"
}
},
"chatHistory": {
"cancelEdit": "Annuler",
"cancelEditTooltip": "Annuler la modification",
"copiedTooltip": "Copié",
"copyTooltip": "Copier le message dans le presse-papiers",
"editTooltip": "Modifier le message"
},
"clipboard": {
"errorMessage": "Échec de la copie dans le presse-papiers",
"errorNotSupported": "L'API du presse-papiers n'est pas prise en charge par votre navigateur",
@@ -264,7 +277,6 @@
"continue": "Continuer",
"control_after_generate": "contrôle après génération",
"control_before_generate": "contrôle avant génération",
"copy": "Copier",
"copyToClipboard": "Copier dans le presse-papiers",
"currentUser": "Utilisateur actuel",
"customize": "Personnaliser",
@@ -276,7 +288,6 @@
"disableAll": "Désactiver tout",
"disabling": "Désactivation",
"download": "Télécharger",
"edit": "Modifier",
"empty": "Vide",
"enableAll": "Activer tout",
"enabled": "Activé",
@@ -1343,7 +1354,6 @@
"no3dSceneToExport": "Aucune scène 3D à exporter",
"noTemplatesToExport": "Aucun modèle à exporter",
"nodeDefinitionsUpdated": "Définitions de nœuds mises à jour",
"nothingSelected": "Aucune sélection",
"nothingToGroup": "Rien à regrouper",
"nothingToQueue": "Rien à ajouter à la file dattente",
"pendingTasksDeleted": "Tâches en attente supprimées",

View File

@@ -4,6 +4,26 @@
"title": "APIード",
"totalCost": "合計コスト"
},
"apiNodesNews": {
"introducing": "紹介",
"steps": {
"step1": {
"subtitle": "設定 > ユーザー > ログイン",
"title": "ログイン/アカウント作成:"
},
"step2": {
"subtitle": "設定 > クレジット > クレジットを購入",
"title": "クレジットを購入:"
},
"step3": {
"title": "「APIード」セクションで新しいAPIードを見つけてキャンバスに追加"
},
"step4": {
"title": "実行!"
}
},
"subtitle": "すべての外部モデルがComfyUIで利用可能になりました"
},
"apiNodesSignInDialog": {
"message": "このワークフローにはAPIードが含まれており、実行するためにはアカウントにサインインする必要があります。",
"title": "APIードを使用するためにはサインインが必要です"
@@ -82,13 +102,6 @@
"title": "アカウントを作成する"
}
},
"chatHistory": {
"cancelEdit": "キャンセル",
"cancelEditTooltip": "編集をキャンセル",
"copiedTooltip": "コピーしました",
"copyTooltip": "メッセージをクリップボードにコピー",
"editTooltip": "メッセージを編集"
},
"clipboard": {
"errorMessage": "クリップボードへのコピーに失敗しました",
"errorNotSupported": "お使いのブラウザではクリップボードAPIがサポートされていません",
@@ -264,7 +277,6 @@
"continue": "続ける",
"control_after_generate": "生成後の制御",
"control_before_generate": "生成前の制御",
"copy": "コピー",
"copyToClipboard": "クリップボードにコピー",
"currentUser": "現在のユーザー",
"customize": "カスタマイズ",
@@ -276,7 +288,6 @@
"disableAll": "すべて無効にする",
"disabling": "無効化",
"download": "ダウンロード",
"edit": "編集",
"empty": "空",
"enableAll": "すべて有効にする",
"enabled": "有効",
@@ -1343,7 +1354,6 @@
"no3dSceneToExport": "エクスポートする3Dシーンがありません",
"noTemplatesToExport": "エクスポートするテンプレートがありません",
"nodeDefinitionsUpdated": "ノード定義が更新されました",
"nothingSelected": "選択されていません",
"nothingToGroup": "グループ化するものがありません",
"nothingToQueue": "キューに追加する項目がありません",
"pendingTasksDeleted": "保留中のタスクが削除されました",

View File

@@ -4,6 +4,26 @@
"title": "API 노드(들)",
"totalCost": "총 비용"
},
"apiNodesNews": {
"introducing": "소개합니다",
"steps": {
"step1": {
"subtitle": "설정 > 사용자 > 로그인",
"title": "로그인/계정 생성:"
},
"step2": {
"subtitle": "설정 > 크레딧 > 크레딧 구매",
"title": "크레딧 구매:"
},
"step3": {
"title": "'API Node' 섹션에서 새로운 API 노드를 찾아 캔버스에 추가하세요"
},
"step4": {
"title": "실행!"
}
},
"subtitle": "모든 외부 모델이 이제 ComfyUI에서 사용 가능합니다"
},
"apiNodesSignInDialog": {
"message": "이 워크플로우에는 API 노드가 포함되어 있으며, 실행하려면 계정에 로그인해야 합니다.",
"title": "API 노드 사용에 필요한 로그인"
@@ -82,13 +102,6 @@
"title": "계정 생성"
}
},
"chatHistory": {
"cancelEdit": "취소",
"cancelEditTooltip": "편집 취소",
"copiedTooltip": "복사됨",
"copyTooltip": "메시지를 클립보드에 복사",
"editTooltip": "메시지 편집"
},
"clipboard": {
"errorMessage": "클립보드에 복사하지 못했습니다",
"errorNotSupported": "브라우저가 클립보드 API를 지원하지 않습니다.",
@@ -264,7 +277,6 @@
"continue": "계속",
"control_after_generate": "생성 후 제어",
"control_before_generate": "생성 전 제어",
"copy": "복사",
"copyToClipboard": "클립보드에 복사",
"currentUser": "현재 사용자",
"customize": "사용자 정의",
@@ -276,7 +288,6 @@
"disableAll": "모두 비활성화",
"disabling": "비활성화 중",
"download": "다운로드",
"edit": "편집",
"empty": "비어 있음",
"enableAll": "모두 활성화",
"enabled": "활성화됨",
@@ -1343,7 +1354,6 @@
"no3dSceneToExport": "내보낼 3D 장면이 없습니다",
"noTemplatesToExport": "내보낼 템플릿이 없습니다",
"nodeDefinitionsUpdated": "노드 정의가 업데이트되었습니다",
"nothingSelected": "선택된 항목이 없습니다",
"nothingToGroup": "그룹화할 항목이 없습니다",
"nothingToQueue": "대기열에 추가할 항목이 없습니다",
"pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다",

View File

@@ -4,6 +4,26 @@
"title": "API Node(s)",
"totalCost": "Общая стоимость"
},
"apiNodesNews": {
"introducing": "Представляем",
"steps": {
"step1": {
"subtitle": "Настройки > Пользователь > Войти",
"title": "Войти/Создать аккаунт:"
},
"step2": {
"subtitle": "Настройки > Кредиты > Купить кредиты",
"title": "Купить кредиты:"
},
"step3": {
"title": "Найдите новые API-узлы в разделе 'API Node' и добавьте их на холст"
},
"step4": {
"title": "Запустить!"
}
},
"subtitle": "Все внешние модели теперь доступны в ComfyUI"
},
"apiNodesSignInDialog": {
"message": "Этот рабочий процесс содержит API Nodes, которые требуют входа в вашу учетную запись для выполнения.",
"title": "Требуется вход для использования API Nodes"
@@ -82,13 +102,6 @@
"title": "Создать аккаунт"
}
},
"chatHistory": {
"cancelEdit": "Отмена",
"cancelEditTooltip": "Отменить редактирование",
"copiedTooltip": "Скопировано",
"copyTooltip": "Скопировать сообщение в буфер",
"editTooltip": "Редактировать сообщение"
},
"clipboard": {
"errorMessage": "Не удалось скопировать в буфер обмена",
"errorNotSupported": "API буфера обмена не поддерживается в вашем браузере",
@@ -264,7 +277,6 @@
"continue": "Продолжить",
"control_after_generate": "управление после генерации",
"control_before_generate": "управление до генерации",
"copy": "Копировать",
"copyToClipboard": "Скопировать в буфер обмена",
"currentUser": "Текущий пользователь",
"customize": "Настроить",
@@ -276,7 +288,6 @@
"disableAll": "Отключить все",
"disabling": "Отключение",
"download": "Скачать",
"edit": "Редактировать",
"empty": "Пусто",
"enableAll": "Включить все",
"enabled": "Включено",
@@ -1343,7 +1354,6 @@
"no3dSceneToExport": "Нет 3D сцены для экспорта",
"noTemplatesToExport": "Нет шаблонов для экспорта",
"nodeDefinitionsUpdated": "Определения узлов обновлены",
"nothingSelected": "Ничего не выбрано",
"nothingToGroup": "Нечего группировать",
"nothingToQueue": "Нет заданий в очереди",
"pendingTasksDeleted": "Ожидающие задачи удалены",

View File

@@ -4,6 +4,26 @@
"title": "API节点",
"totalCost": "总成本"
},
"apiNodesNews": {
"introducing": "介绍",
"steps": {
"step1": {
"subtitle": "设置 > 用户 > 登录",
"title": "登录/创建账户:"
},
"step2": {
"subtitle": "设置 > 积分 > 购买积分",
"title": "购买积分:"
},
"step3": {
"title": "在“API 节点”部分找到新的 API 节点并添加到画布"
},
"step4": {
"title": "运行!"
}
},
"subtitle": "所有外部模型现已在 ComfyUI 中可用"
},
"apiNodesSignInDialog": {
"message": "此工作流包含API节点需要您登录账户才能运行。",
"title": "使用API节点需要登录"
@@ -82,13 +102,6 @@
"title": "创建一个账户"
}
},
"chatHistory": {
"cancelEdit": "取消",
"cancelEditTooltip": "取消编辑",
"copiedTooltip": "已复制",
"copyTooltip": "复制消息到剪贴板",
"editTooltip": "编辑消息"
},
"clipboard": {
"errorMessage": "复制到剪贴板失败",
"errorNotSupported": "您的浏览器不支持剪贴板API",
@@ -264,7 +277,6 @@
"continue": "继续",
"control_after_generate": "生成后控制",
"control_before_generate": "生成前控制",
"copy": "复制",
"copyToClipboard": "复制到剪贴板",
"currentUser": "当前用户",
"customize": "自定义",
@@ -276,7 +288,6 @@
"disableAll": "禁用全部",
"disabling": "禁用中",
"download": "下载",
"edit": "编辑",
"empty": "空",
"enableAll": "启用全部",
"enabled": "已启用",
@@ -1343,7 +1354,6 @@
"no3dSceneToExport": "没有3D场景可以导出",
"noTemplatesToExport": "没有模板可以导出",
"nodeDefinitionsUpdated": "节点定义已更新",
"nothingSelected": "未选择任何内容",
"nothingToGroup": "没有可分组的内容",
"nothingToQueue": "没有可加入队列的内容",
"pendingTasksDeleted": "待处理任务已删除",

View File

@@ -5,11 +5,14 @@ import * as Sentry from '@sentry/vue'
import { initializeApp } from 'firebase/app'
import { createPinia } from 'pinia'
import 'primeicons/primeicons.css'
import * as pv from 'primevue'
import PrimeVue from 'primevue/config'
import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip'
import { createApp } from 'vue'
import * as Vue from 'vue'
import * as vueI18n from 'vue-i18n'
import { VueFire, VueFireAuth } from 'vuefire'
import '@/assets/css/style.css'
@@ -68,3 +71,7 @@ app
modules: [VueFireAuth()]
})
.mount('#vue-app')
window.Vue = Vue
window.PrimeVue = pv
window.VueI18n = vueI18n

View File

@@ -87,12 +87,6 @@ const zProgressTextWsMessage = z.object({
text: z.string()
})
const zDisplayComponentWsMessage = z.object({
node_id: zNodeId,
component: z.enum(['ChatHistoryWidget']),
props: z.record(z.string(), z.any()).optional()
})
const zTerminalSize = z.object({
cols: z.number(),
row: z.number()
@@ -126,9 +120,6 @@ export type ExecutionInterruptedWsMessage = z.infer<
export type ExecutionErrorWsMessage = z.infer<typeof zExecutionErrorWsMessage>
export type LogsWsMessage = z.infer<typeof zLogsWsMessage>
export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
export type DisplayComponentWsMessage = z.infer<
typeof zDisplayComponentWsMessage
>
// End of ws messages
const zPromptInputItem = z.object({

View File

@@ -1,7 +1,6 @@
import axios from 'axios'
import type {
DisplayComponentWsMessage,
EmbeddingsResponse,
ExecutedWsMessage,
ExecutingWsMessage,
@@ -104,7 +103,6 @@ interface BackendApiCalls {
/** Binary preview/progress data */
b_preview: Blob
progress_text: ProgressTextWsMessage
display_component: DisplayComponentWsMessage
}
/** Dictionary of all api calls */

View File

@@ -143,20 +143,13 @@ export class ComfyApp {
_nodeOutputs: Record<string, any>
nodePreviewImages: Record<string, string[]>
// @ts-expect-error fixme ts strict error
#graph: LGraph
get graph() {
return this.#graph
}
graph: LGraph
// @ts-expect-error fixme ts strict error
canvas: LGraphCanvas
dragOverNode: LGraphNode | null = null
// @ts-expect-error fixme ts strict error
canvasEl: HTMLCanvasElement
#configuringGraphLevel: number = 0
get configuringGraph() {
return this.#configuringGraphLevel > 0
}
configuringGraph: boolean = false
// @ts-expect-error fixme ts strict error
ctx: CanvasRenderingContext2D
bodyTop: HTMLElement
@@ -699,16 +692,17 @@ export class ComfyApp {
api.init()
}
/** Flag that the graph is configuring to prevent nodes from running checks while its still loading */
#addConfigureHandler() {
const app = this
const configure = LGraph.prototype.configure
LGraph.prototype.configure = function (...args) {
app.#configuringGraphLevel++
// Flag that the graph is configuring to prevent nodes from running checks while its still loading
LGraph.prototype.configure = function () {
app.configuringGraph = true
try {
return configure.apply(this, args)
// @ts-expect-error fixme ts strict error
return configure.apply(this, arguments)
} finally {
app.#configuringGraphLevel--
app.configuringGraph = false
}
}
}
@@ -762,13 +756,14 @@ export class ComfyApp {
this.#addConfigureHandler()
this.#addApiUpdateHandlers()
this.#graph = new LGraph()
this.graph = new LGraph()
this.#addAfterConfigureHandler()
this.canvas = new LGraphCanvas(canvasEl, this.graph)
// Make canvas states reactive so we can observe changes on them.
this.canvas.state = reactive(this.canvas.state)
this.canvas.ds.state = reactive(this.canvas.ds.state)
// @ts-expect-error fixme ts strict error
this.ctx = canvasEl.getContext('2d')
@@ -1095,7 +1090,6 @@ export class ComfyApp {
title: t('errorDialog.loadWorkflowTitle'),
reportType: 'loadWorkflowError'
})
console.error(error)
return
}
for (const node of this.graph.nodes) {
@@ -1235,7 +1229,6 @@ export class ComfyApp {
title: t('errorDialog.promptExecutionError'),
reportType: 'promptExecutionError'
})
console.error(error)
if (error instanceof PromptExecutionError) {
executionStore.lastNodeErrors = error.response.node_errors ?? null

View File

@@ -47,13 +47,10 @@ export interface DOMWidget<T extends HTMLElement, V extends object | string>
/**
* A DOM widget that wraps a Vue component as a litegraph widget.
*/
export interface ComponentWidget<
V extends object | string,
P = Record<string, unknown>
> extends BaseDOMWidget<V> {
export interface ComponentWidget<V extends object | string>
extends BaseDOMWidget<V> {
readonly component: Component
readonly inputSpec: InputSpec
readonly props?: P
}
export interface DOMWidgetOptions<V extends object | string>
@@ -220,23 +217,18 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
}
}
export class ComponentWidgetImpl<
V extends object | string,
P = Record<string, unknown>
>
export class ComponentWidgetImpl<V extends object | string>
extends BaseDOMWidgetImpl<V>
implements ComponentWidget<V, P>
implements ComponentWidget<V>
{
readonly component: Component
readonly inputSpec: InputSpec
readonly props?: P
constructor(obj: {
node: LGraphNode
name: string
component: Component
inputSpec: InputSpec
props?: P
options: DOMWidgetOptions<V>
}) {
super({
@@ -245,7 +237,6 @@ export class ComponentWidgetImpl<
})
this.component = obj.component
this.inputSpec = obj.inputSpec
this.props = obj.props
}
override computeLayoutSize() {

View File

@@ -1,257 +0,0 @@
# Services
This directory contains the service layer for the ComfyUI frontend application. Services encapsulate application logic and functionality into organized, reusable modules.
## Table of Contents
- [Overview](#overview)
- [Service Architecture](#service-architecture)
- [Core Services](#core-services)
- [Service Development Guidelines](#service-development-guidelines)
- [Common Design Patterns](#common-design-patterns)
## Overview
Services in ComfyUI provide organized modules that implement the application's functionality and logic. They handle operations such as API communication, workflow management, user settings, and other essential features.
The term "business logic" in this context refers to the code that implements the core functionality and behavior of the application - the rules, processes, and operations that make ComfyUI work as expected, separate from the UI display code.
Services help organize related functionality into cohesive units, making the codebase more maintainable and testable. By centralizing related operations in services, the application achieves better separation of concerns, with UI components focusing on presentation and services handling functional operations.
## Service Architecture
The service layer in ComfyUI follows these architectural principles:
1. **Domain-driven**: Each service focuses on a specific domain of the application
2. **Stateless when possible**: Services generally avoid maintaining internal state
3. **Reusable**: Services can be used across multiple components
4. **Testable**: Services are designed for easy unit testing
5. **Isolated**: Services have clear boundaries and dependencies
While services can interact with both UI components and stores (centralized state), they primarily focus on implementing functionality rather than managing state. The following diagram illustrates how services fit into the application architecture:
```
┌─────────────────────────────────────────────────────────┐
│ UI Components │
└────────────────────────────┬────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Composables │
└────────────────────────────┬────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Services │
│ │
│ (Application Functionality) │
└────────────────────────────┬────────────────────────────┘
┌───────────┴───────────┐
▼ ▼
┌───────────────────────────┐ ┌─────────────────────────┐
│ Stores │ │ External APIs │
│ (Centralized State) │ │ │
└───────────────────────────┘ └─────────────────────────┘
```
## Core Services
The core services include:
| Service | Description |
|---------|-------------|
| algoliaSearchService.ts | Implements search functionality using Algolia |
| autoQueueService.ts | Manages automatic queue execution |
| colorPaletteService.ts | Handles color palette management and customization |
| comfyManagerService.ts | Manages ComfyUI application packages and updates |
| comfyRegistryService.ts | Handles registration and discovery of ComfyUI extensions |
| dialogService.ts | Provides dialog and modal management |
| extensionService.ts | Manages extension registration and lifecycle |
| keybindingService.ts | Handles keyboard shortcuts and keybindings |
| litegraphService.ts | Provides utilities for working with the LiteGraph library |
| load3dService.ts | Manages 3D model loading and visualization |
| nodeSearchService.ts | Implements node search functionality |
| workflowService.ts | Handles workflow operations (save, load, execute) |
## Service Development Guidelines
In ComfyUI, services can be implemented using two approaches:
### 1. Class-based Services
For complex services with state management and multiple methods, class-based services are used:
```typescript
export class NodeSearchService {
// Service state
private readonly nodeFuseSearch: FuseSearch<ComfyNodeDefImpl>
private readonly filters: Record<string, FuseFilter<ComfyNodeDefImpl, string>>
constructor(data: ComfyNodeDefImpl[]) {
// Initialize state
this.nodeFuseSearch = new FuseSearch(data, { /* options */ })
// Setup filters
this.filters = {
inputType: new FuseFilter<ComfyNodeDefImpl, string>(/* options */),
category: new FuseFilter<ComfyNodeDefImpl, string>(/* options */)
}
}
public searchNode(query: string, filters: FuseFilterWithValue[] = []): ComfyNodeDefImpl[] {
// Implementation
return results
}
}
```
### 2. Composable-style Services
For simpler services or those that need to integrate with Vue's reactivity system, we prefer using composable-style services:
```typescript
export function useNodeSearchService(initialData: ComfyNodeDefImpl[]) {
// State (reactive if needed)
const data = ref(initialData)
// Search functionality
function searchNodes(query: string) {
// Implementation
return results
}
// Additional methods
function refreshData(newData: ComfyNodeDefImpl[]) {
data.value = newData
}
// Return public API
return {
searchNodes,
refreshData
}
}
```
When deciding between these approaches, consider:
1. **Stateful vs. Stateless**: For stateful services, classes often provide clearer encapsulation
2. **Reactivity needs**: If the service needs to be reactive, composable-style services integrate better with Vue's reactivity system
3. **Complexity**: For complex services with many methods and internal state, classes can provide better organization
4. **Testing**: Both approaches can be tested effectively, but composables may be simpler to test with Vue Test Utils
### Service Template
Here's a template for creating a new composable-style service:
```typescript
/**
* Service for managing [domain/functionality]
*/
export function useExampleService() {
// Private state/functionality
const cache = new Map()
/**
* Description of what this method does
* @param param1 Description of parameter
* @returns Description of return value
*/
async function performOperation(param1: string) {
try {
// Implementation
return result
} catch (error) {
// Error handling
console.error(`Operation failed: ${error.message}`)
throw error
}
}
// Return public API
return {
performOperation
}
}
```
## Common Design Patterns
Services in ComfyUI frequently use the following design patterns:
### Caching and Request Deduplication
```typescript
export function useCachedService() {
const cache = new Map()
const pendingRequests = new Map()
async function fetchData(key: string) {
// Check cache first
if (cache.has(key)) return cache.get(key)
// Check if request is already in progress
if (pendingRequests.has(key)) {
return pendingRequests.get(key)
}
// Perform new request
const requestPromise = fetch(`/api/${key}`)
.then(response => response.json())
.then(data => {
cache.set(key, data)
pendingRequests.delete(key)
return data
})
pendingRequests.set(key, requestPromise)
return requestPromise
}
return { fetchData }
}
```
### Factory Pattern
```typescript
export function useNodeFactory() {
function createNode(type: string, config: Record<string, any>) {
// Create node based on type and configuration
switch (type) {
case 'basic':
return { /* basic node implementation */ }
case 'complex':
return { /* complex node implementation */ }
default:
throw new Error(`Unknown node type: ${type}`)
}
}
return { createNode }
}
```
### Facade Pattern
```typescript
export function useWorkflowService(
apiService,
graphService,
storageService
) {
// Provides a simple interface to complex subsystems
async function saveWorkflow(name: string) {
const graphData = graphService.serializeGraph()
const storagePath = await storageService.getPath(name)
return apiService.saveData(storagePath, graphData)
}
return { saveWorkflow }
}
```
For more detailed information about the service layer pattern and its applications, refer to:
- [Service Layer Pattern](https://en.wikipedia.org/wiki/Service_layer_pattern)
- [Service-Orientation](https://en.wikipedia.org/wiki/Service-orientation)

View File

@@ -1,3 +1,4 @@
import ApiNodesNewsContent from '@/components/dialog/content/ApiNodesNewsContent.vue'
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
@@ -379,6 +380,32 @@ export const useDialogService = () => {
})
}
/**
* Shows a dialog for the API nodes news.
* TODO: Remove the news dialog on next major feature release.
*/
function showApiNodesNewsDialog() {
if (localStorage.getItem('api-nodes-news-seen') === 'true') {
return
}
return dialogStore.showDialog({
key: 'api-nodes-news',
component: ApiNodesNewsContent,
props: {
dismissableMask: true,
onClose: () => {
dialogStore.closeDialog({ key: 'api-nodes-news' })
localStorage.setItem('api-nodes-news-seen', 'true')
}
},
dialogComponentProps: {
closable: false,
position: 'bottomright'
}
})
}
return {
showLoadWorkflowWarning,
showMissingModelsWarning,
@@ -394,6 +421,7 @@ export const useDialogService = () => {
showSignInDialog,
showTopUpCreditsDialog,
showUpdatePasswordDialog,
showApiNodesNewsDialog,
prompt,
confirm
}

View File

@@ -1,356 +0,0 @@
# Stores
This directory contains Pinia stores for the ComfyUI frontend application. Stores provide centralized state management for the application.
## Table of Contents
- [Overview](#overview)
- [Store Architecture](#store-architecture)
- [Core Stores](#core-stores)
- [Store Development Guidelines](#store-development-guidelines)
- [Common Patterns](#common-patterns)
- [Testing Stores](#testing-stores)
## Overview
Stores in ComfyUI use [Pinia](https://pinia.vuejs.org/), Vue's official state management library. Each store is responsible for managing a specific domain of the application state, such as user data, workflow information, graph state, and UI configuration.
Stores provide a way to maintain global application state that can be accessed from any component, regardless of where those components are in the component hierarchy. This solves the problem of "prop drilling" (passing data down through multiple levels of components) and allows components that aren't directly related to share and modify the same state.
For example, without global state:
```
App
┌──────────┴──────────┐
│ │
HeaderBar Canvas
│ │
│ │
UserMenu NodeProperties
```
In this structure, if the `UserMenu` component needs to update something that affects `NodeProperties`, the data would need to be passed up to `App` and then down again, through all intermediate components.
With Pinia stores, components can directly access and update the shared state:
```
┌─────────────────┐
│ │
│ Pinia Stores │
│ │
└───────┬─────────┘
│ Accessed by
┌──────────────────────────┐
│ │
│ Components │
│ │
└──────────────────────────┘
```
## Store Architecture
The store architecture in ComfyUI follows these principles:
1. **Domain-driven**: Each store focuses on a specific domain of the application
2. **Single source of truth**: Stores serve as the definitive source for specific data
3. **Composition**: Stores can interact with other stores when needed
4. **Actions for logic**: Business logic is encapsulated in store actions
5. **Getters for derived state**: Computed values are exposed via getters
The following diagram illustrates the store architecture and data flow:
```
┌─────────────────────────────────────────────────────────┐
│ Vue Components │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Component A │ │ Component B │ │
│ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │
└───────────┼────────────────────────────┼────────────────┘
│ │
│ ┌───────────────┐ │
└────►│ Composables │◄─────┘
└───────┬───────┘
┌─────────────────────────┼─────────────────────────────┐
│ Pinia Stores │ │
│ │ │
│ ┌───────────────────▼───────────────────────┐ │
│ │ Actions │ │
│ └───────────────────┬───────────────────────┘ │
│ │ │
│ ┌───────────────────▼───────────────────────┐ │
│ │ State │ │
│ └───────────────────┬───────────────────────┘ │
│ │ │
│ ┌───────────────────▼───────────────────────┐ │
│ │ Getters │ │
│ └───────────────────┬───────────────────────┘ │
│ │ │
└─────────────────────────┼─────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ External Services │
│ (API, localStorage, WebSocket, etc.) │
└─────────────────────────────────────────────────────────┘
```
## Core Stores
The core stores include:
| Store | Description |
|-------|-------------|
| aboutPanelStore.ts | Manages the About panel state and badges |
| apiKeyAuthStore.ts | Handles API key authentication |
| comfyManagerStore.ts | Manages ComfyUI application state |
| comfyRegistryStore.ts | Handles extensions registry |
| commandStore.ts | Manages commands and command execution |
| dialogStore.ts | Controls dialog/modal display and state |
| domWidgetStore.ts | Manages DOM widget state |
| executionStore.ts | Tracks workflow execution state |
| extensionStore.ts | Manages extension registration and state |
| firebaseAuthStore.ts | Handles Firebase authentication |
| graphStore.ts | Manages the graph canvas state |
| imagePreviewStore.ts | Controls image preview functionality |
| keybindingStore.ts | Manages keyboard shortcuts |
| menuItemStore.ts | Handles menu items and their state |
| modelStore.ts | Manages AI models information |
| nodeDefStore.ts | Manages node definitions |
| queueStore.ts | Handles the execution queue |
| settingStore.ts | Manages application settings |
| userStore.ts | Manages user data and preferences |
| workflowStore.ts | Handles workflow data and operations |
| workspace/* | Stores related to the workspace UI |
## Store Development Guidelines
When developing or modifying stores, follow these best practices:
1. **Define clear purpose**: Each store should have a specific responsibility
2. **Use actions for async operations**: Encapsulate asynchronous logic in actions
3. **Keep stores focused**: Each store should manage related state
4. **Document public API**: Add comments for state properties, actions, and getters
5. **Use getters for derived state**: Compute derived values using getters
6. **Test store functionality**: Write unit tests for stores
### Store Template
Here's a template for creating a new Pinia store, following the setup style used in ComfyUI:
```typescript
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useExampleStore = defineStore('example', () => {
// State
const items = ref([])
const isLoading = ref(false)
const error = ref(null)
// Getters
const itemCount = computed(() => items.value.length)
const hasError = computed(() => error.value !== null)
// Actions
function addItem(item) {
items.value.push(item)
}
async function fetchItems() {
isLoading.value = true
error.value = null
try {
const response = await fetch('/api/items')
const data = await response.json()
items.value = data
} catch (err) {
error.value = err.message
} finally {
isLoading.value = false
}
}
// Expose state, getters, and actions
return {
// State
items,
isLoading,
error,
// Getters
itemCount,
hasError,
// Actions
addItem,
fetchItems
}
})
```
## Common Patterns
Stores in ComfyUI frequently use these patterns:
### API Integration
```typescript
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { api } from '@/scripts/api'
export const useDataStore = defineStore('data', () => {
const data = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchData() {
loading.value = true
try {
const result = await api.getData()
data.value = result
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
fetchData
}
})
```
### Store Composition
```typescript
import { defineStore, storeToRefs } from 'pinia'
import { computed, ref, watch } from 'vue'
import { useOtherStore } from './otherStore'
export const useComposedStore = defineStore('composed', () => {
const otherStore = useOtherStore()
const { someData } = storeToRefs(otherStore)
// Local state
const localState = ref(0)
// Computed value based on other store
const derivedValue = computed(() => {
return computeFromOtherData(someData.value, localState.value)
})
// Action that uses another store
async function complexAction() {
await otherStore.someAction()
localState.value += 1
}
return {
localState,
derivedValue,
complexAction
}
})
```
### Persistent State
```typescript
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
export const usePreferencesStore = defineStore('preferences', () => {
// Load from localStorage if available
const theme = ref(localStorage.getItem('theme') || 'light')
const fontSize = ref(parseInt(localStorage.getItem('fontSize') || '14'))
// Save to localStorage when changed
watch(theme, (newTheme) => {
localStorage.setItem('theme', newTheme)
})
watch(fontSize, (newSize) => {
localStorage.setItem('fontSize', newSize.toString())
})
function setTheme(newTheme) {
theme.value = newTheme
}
return {
theme,
fontSize,
setTheme
}
})
```
## Testing Stores
Stores should be tested to ensure they behave as expected. Here's an example of how to test a store:
```typescript
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { api } from '@/scripts/api'
import { useExampleStore } from '@/stores/exampleStore'
// Mock API dependencies
vi.mock('@/scripts/api', () => ({
api: {
getData: vi.fn()
}
}))
describe('useExampleStore', () => {
let store: ReturnType<typeof useExampleStore>
beforeEach(() => {
// Create a fresh pinia instance and make it active
setActivePinia(createPinia())
store = useExampleStore()
// Clear all mocks
vi.clearAllMocks()
})
it('should initialize with default state', () => {
expect(store.items).toEqual([])
expect(store.isLoading).toBe(false)
expect(store.error).toBeNull()
})
it('should add an item', () => {
store.addItem('test')
expect(store.items).toEqual(['test'])
expect(store.itemCount).toBe(1)
})
it('should fetch items', async () => {
// Setup mock response
vi.mocked(api.getData).mockResolvedValue(['item1', 'item2'])
// Call the action
await store.fetchItems()
// Verify state changes
expect(store.isLoading).toBe(false)
expect(store.items).toEqual(['item1', 'item2'])
expect(store.error).toBeNull()
})
})
```
For more information on Pinia, refer to the [Pinia documentation](https://pinia.vuejs.org/introduction.html).

View File

@@ -1,11 +1,8 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import type ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue'
import { useNodeChatHistory } from '@/composables/node/useNodeChatHistory'
import { useNodeProgressText } from '@/composables/node/useNodeProgressText'
import type {
DisplayComponentWsMessage,
ExecutedWsMessage,
ExecutionCachedWsMessage,
ExecutionErrorWsMessage,
@@ -110,10 +107,6 @@ export const useExecutionStore = defineStore('execution', () => {
)
}
api.addEventListener('progress_text', handleProgressText as EventListener)
api.addEventListener(
'display_component',
handleDisplayComponent as EventListener
)
function unbindExecutionEvents() {
api.removeEventListener(
@@ -202,21 +195,6 @@ export const useExecutionStore = defineStore('execution', () => {
useNodeProgressText().showTextPreview(node, text)
}
function handleDisplayComponent(e: CustomEvent<DisplayComponentWsMessage>) {
const { node_id, component, props = {} } = e.detail
const node = app.graph.getNodeById(node_id)
if (!node) return
if (component === 'ChatHistoryWidget') {
useNodeChatHistory({
props: props as Omit<
InstanceType<typeof ChatHistoryWidget>['$props'],
'widget'
>
}).showChatHistory(node)
}
}
function storePrompt({
nodes,
id,

View File

@@ -1,9 +1,7 @@
import type { LGraphCanvas, LGraphGroup, LGraphNode } from '@comfyorg/litegraph'
import type { Positionable } from '@comfyorg/litegraph/dist/interfaces'
import { defineStore } from 'pinia'
import { type Raw, computed, markRaw, ref, shallowRef } from 'vue'
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
import { type Raw, markRaw, ref, shallowRef } from 'vue'
export const useTitleEditorStore = defineStore('titleEditor', () => {
const titleEditorTarget = shallowRef<LGraphNode | LGraphGroup | null>(null)
@@ -29,9 +27,6 @@ export const useCanvasStore = defineStore('canvas', () => {
selectedItems.value = items.map((item) => markRaw(item))
}
const nodeSelected = computed(() => selectedItems.value.some(isLGraphNode))
const groupSelected = computed(() => selectedItems.value.some(isLGraphGroup))
const getCanvas = () => {
if (!canvas.value) throw new Error('getCanvas: canvas is null')
return canvas.value
@@ -40,8 +35,6 @@ export const useCanvasStore = defineStore('canvas', () => {
return {
canvas,
selectedItems,
nodeSelected,
groupSelected,
updateSelectedItems,
getCanvas
}

View File

@@ -2,14 +2,3 @@ export interface ApiNodeCost {
name: string
cost: number
}
export interface ApiNodeCostData {
vendor: string
nodeName: string
pricingParams: string
pricePerRunRange: string
displayPrice: string
rateDocumentation?: string
}
export type ApiNodeCostRecord = Record<string, ApiNodeCostData>

View File

@@ -158,7 +158,7 @@ export const graphToPrompt = async (
if (link) {
if (parent?.updateLink) {
// Subgraph node / groupNode callback; deprecated, should be replaced
// groupNode
link = parent.updateLink(link)
}
if (link) {

View File

@@ -42,6 +42,7 @@ import { StatusWsMessageStatus } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { setupAutoQueueHandler } from '@/services/autoQueueService'
import { useDialogService } from '@/services/dialogService'
import { useKeybindingService } from '@/services/keybindingService'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
@@ -243,6 +244,8 @@ const onGraphReady = () => {
// Explicitly initialize nodeSearchService to avoid indexing delay when
// node search is triggered
useNodeDefStore().nodeSearchService.searchNode('')
useDialogService().showApiNodesNewsDialog()
},
{ timeout: 1000 }
)

View File

@@ -271,7 +271,7 @@ describe('useWorkflowStore', () => {
// Set up initial bookmark
expect(workflow.path).toBe('workflows/dir/test.json')
await bookmarkStore.setBookmarked(workflow.path, true)
bookmarkStore.setBookmarked(workflow.path, true)
expect(bookmarkStore.isBookmarked(workflow.path)).toBe(true)
// Mock super.rename
@@ -351,7 +351,7 @@ describe('useWorkflowStore', () => {
vi.spyOn(workflow, 'delete').mockResolvedValue()
// Bookmark the workflow
await bookmarkStore.setBookmarked(workflow.path, true)
bookmarkStore.setBookmarked(workflow.path, true)
expect(bookmarkStore.isBookmarked(workflow.path)).toBe(true)
// Delete the workflow

View File

@@ -4,15 +4,9 @@ import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'
import vueDevTools from 'vite-plugin-vue-devtools'
import type { UserConfigExport } from 'vitest/config'
import {
addElementVnodeExportPlugin,
comfyAPIPlugin,
generateImportMapPlugin
} from './build/plugins'
import { comfyAPIPlugin } from './build/plugins'
dotenv.config()
@@ -21,7 +15,6 @@ const SHOULD_MINIFY = process.env.ENABLE_MINIFY === 'true'
// vite dev server will listen on all addresses, including LAN and public addresses
const VITE_REMOTE_DEV = process.env.VITE_REMOTE_DEV === 'true'
const DISABLE_TEMPLATES_PROXY = process.env.DISABLE_TEMPLATES_PROXY === 'true'
const DISABLE_VUE_PLUGINS = process.env.DISABLE_VUE_PLUGINS === 'true'
const DEV_SERVER_COMFYUI_URL =
process.env.DEV_SERVER_COMFYUI_URL || 'http://127.0.0.1:8188'
@@ -72,16 +65,8 @@ export default defineConfig({
},
plugins: [
...(!DISABLE_VUE_PLUGINS
? [vueDevTools(), vue(), createHtmlPlugin({})]
: [vue()]),
vue(),
comfyAPIPlugin(IS_DEV),
generateImportMapPlugin([
{ name: 'vue', pattern: /[\\/]node_modules[\\/]vue[\\/]/ },
{ name: 'primevue', pattern: /[\\/]node_modules[\\/]primevue[\\/]/ },
{ name: 'vue-i18n', pattern: /[\\/]node_modules[\\/]vue-i18n[\\/]/ }
]),
addElementVnodeExportPlugin(),
Icons({
compiler: 'vue3'