Compare commits
3 Commits
v1.20.2
...
model_file
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d61221ad3 | ||
|
|
fec5dbcf70 | ||
|
|
c72ba664ee |
@@ -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
|
||||
|
||||
2
.github/workflows/test-ui.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_devtools'
|
||||
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
||||
ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
|
||||
ref: '9d2421fd3208a310e4d0f71fca2ea0c985759c33'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"id": "5635564e-189f-49e4-9b25-6b7634bcd595",
|
||||
"revision": 0,
|
||||
"last_node_id": 78,
|
||||
"last_link_id": 53,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 78,
|
||||
"type": "DevToolsNodeWithV2ComboInput",
|
||||
"pos": [1320, 904],
|
||||
"size": [270.3199157714844, 58],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "COMBO",
|
||||
"type": "COMBO",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithV2ComboInput"
|
||||
},
|
||||
"widgets_values": ["A"]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.19.7"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
124
browser_tests/desktop/fixtures/electron.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
type ElectronFixtureOptions = {
|
||||
registerDefaults?: {
|
||||
downloadManager?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
type MockFunction = {
|
||||
calls: unknown[][]
|
||||
called: () => Promise<void>
|
||||
handler?: (args: unknown[]) => unknown
|
||||
}
|
||||
|
||||
export type MockElectronAPI = {
|
||||
setup: (method: string, handler: (args: unknown[]) => unknown) => MockFunction
|
||||
}
|
||||
|
||||
export const electronFixture = base.extend<{
|
||||
electronAPI: MockElectronAPI
|
||||
electronOptions: ElectronFixtureOptions
|
||||
}>({
|
||||
electronOptions: [
|
||||
{
|
||||
registerDefaults: {
|
||||
downloadManager: true
|
||||
}
|
||||
},
|
||||
{ option: true }
|
||||
],
|
||||
|
||||
electronAPI: [
|
||||
async ({ page, electronOptions }, use) => {
|
||||
const mocks = new Map<string, MockFunction>()
|
||||
|
||||
await page.exposeFunction(
|
||||
'__handleMockCall',
|
||||
async (method: string, args: unknown[]) => {
|
||||
const mock = mocks.get(method)
|
||||
|
||||
if (electronOptions.registerDefaults?.downloadManager) {
|
||||
if (method === 'DownloadManager.getAllDownloads') {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
if (!mock) return null
|
||||
mock.calls.push(args)
|
||||
return mock.handler ? mock.handler(args) : null
|
||||
}
|
||||
)
|
||||
|
||||
const createMockFunction = (
|
||||
method: string,
|
||||
handler: (args: unknown[]) => unknown
|
||||
): MockFunction => {
|
||||
let resolveNextCall: (() => void) | null = null
|
||||
|
||||
const mockFn: MockFunction = {
|
||||
calls: [],
|
||||
async called() {
|
||||
if (this.calls.length > 0) return
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
resolveNextCall = resolve
|
||||
})
|
||||
},
|
||||
handler: (args: unknown[]) => {
|
||||
const result = handler(args)
|
||||
resolveNextCall?.()
|
||||
resolveNextCall = null
|
||||
return result
|
||||
}
|
||||
}
|
||||
mocks.set(method, mockFn)
|
||||
|
||||
// Add the method to the window.electronAPI object
|
||||
page.evaluate((methodName) => {
|
||||
const w = window as typeof window & {
|
||||
electronAPI: Record<string, any>
|
||||
}
|
||||
|
||||
w.electronAPI[methodName] = async (...args: unknown[]) => {
|
||||
return window['__handleMockCall'](methodName, args)
|
||||
}
|
||||
}, method)
|
||||
|
||||
return mockFn
|
||||
}
|
||||
|
||||
const testAPI: MockElectronAPI = {
|
||||
setup(method, handler) {
|
||||
console.log('adding handler for', method)
|
||||
return createMockFunction(method, handler)
|
||||
}
|
||||
}
|
||||
|
||||
await page.addInitScript(async () => {
|
||||
const getProxy = (...path: string[]) => {
|
||||
return new Proxy(() => {}, {
|
||||
// Handle the proxy itself being called as a function
|
||||
apply: async (target, thisArg, argArray) => {
|
||||
return window['__handleMockCall'](path.join('.'), argArray)
|
||||
},
|
||||
// Handle property access
|
||||
get: (target, prop: string) => {
|
||||
return getProxy(...path, prop)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const w = window as typeof window & {
|
||||
electronAPI: any
|
||||
}
|
||||
|
||||
w.electronAPI = getProxy()
|
||||
console.log('registered electron api')
|
||||
})
|
||||
|
||||
await use(testAPI)
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
})
|
||||
172
browser_tests/desktop/importModel.spec.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { ComfyPage, comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { MockElectronAPI, electronFixture } from './fixtures/electron'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, electronFixture)
|
||||
|
||||
comfyPageFixture.describe('Import Model (web)', () => {
|
||||
comfyPageFixture(
|
||||
'Import dialog does not show when electron api is not available',
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.dragAndDropExternalResource({
|
||||
fileName: 'test.bin',
|
||||
buffer: Buffer.from('')
|
||||
})
|
||||
|
||||
// Normal unable to find workflow in file error
|
||||
await expect(
|
||||
comfyPage.page.locator('.p-toast-message.p-toast-message-warn')
|
||||
).toHaveCount(1)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Import Model (electron)', () => {
|
||||
const dropFile = async (
|
||||
comfyPage: ComfyPage,
|
||||
electronAPI: MockElectronAPI,
|
||||
fileName: string,
|
||||
metadata: string
|
||||
) => {
|
||||
const getFilePathMock = electronAPI.setup('getFilePath', () =>
|
||||
Promise.resolve('some/file/path/' + fileName)
|
||||
)
|
||||
|
||||
let buffer: Buffer | undefined
|
||||
if (metadata) {
|
||||
const contentBuffer = Buffer.from(metadata, 'utf-8')
|
||||
|
||||
const headerSizeBuffer = Buffer.alloc(8)
|
||||
headerSizeBuffer.writeBigUInt64LE(BigInt(contentBuffer.length))
|
||||
|
||||
buffer = Buffer.concat([headerSizeBuffer, contentBuffer])
|
||||
}
|
||||
|
||||
await comfyPage.dragAndDropExternalResource({
|
||||
fileName,
|
||||
buffer
|
||||
})
|
||||
await getFilePathMock.called()
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('.p-toast-message.p-toast-message-warn')
|
||||
).toHaveCount(0)
|
||||
await expect(comfyPage.importModelDialog.rootEl).toBeVisible()
|
||||
}
|
||||
|
||||
test('Can show import file dialog by dropping file onto the app', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(comfyPage, electronAPI, 'test.bin', '{}')
|
||||
})
|
||||
|
||||
test('Can autodetect checkpoint model type from modelspec', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
__metadata__: {
|
||||
'modelspec.sai_model_spec': 'test',
|
||||
'modelspec.architecture': 'stable-diffusion-v1'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'checkpoints'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can autodetect lora model type from modelspec', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
__metadata__: {
|
||||
'modelspec.sai_model_spec': 'test',
|
||||
'modelspec.architecture': 'Flux.1-AE/lora'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'loras'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can autodetect checkpoint model type from header keys', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
'model.diffusion_model.input_blocks.0.0.bias': {}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'checkpoints'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can autodetect lora model type from header keys', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
'lora_unet_down_blocks_0_attentions_0_proj_in.alpha': {}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'loras'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can import file', async ({ comfyPage, electronAPI }) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'checkpoint_modelspec.safetensors',
|
||||
'{}'
|
||||
)
|
||||
|
||||
const importModelMock = electronAPI.setup(
|
||||
'importModel',
|
||||
() => new Promise((resolve) => setTimeout(resolve, 100))
|
||||
)
|
||||
|
||||
// Model type is required so select one
|
||||
await expect(comfyPage.importModelDialog.importButton).toBeDisabled()
|
||||
await comfyPage.importModelDialog.modelTypeInput.fill('checkpoints')
|
||||
await expect(comfyPage.importModelDialog.importButton).toBeEnabled()
|
||||
|
||||
// Click import, ensure API is called
|
||||
await comfyPage.importModelDialog.importButton.click()
|
||||
await importModelMock.called()
|
||||
|
||||
// Toast should be shown and dialog closes
|
||||
await expect(
|
||||
comfyPage.page.locator('.p-toast-message.p-toast-message-success')
|
||||
).toHaveCount(1)
|
||||
|
||||
await expect(comfyPage.importModelDialog.rootEl).toBeHidden()
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import { ComfyActionbar } from '../helpers/actionbar'
|
||||
import { ComfyTemplates } from '../helpers/templates'
|
||||
import { ComfyMouse } from './ComfyMouse'
|
||||
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { ImportModelDialog } from './components/ImportModelDialog'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
NodeLibrarySidebarTab,
|
||||
@@ -133,9 +134,6 @@ export class ComfyPage {
|
||||
// Inputs
|
||||
public readonly workflowUploadInput: Locator
|
||||
|
||||
// Toasts
|
||||
public readonly visibleToasts: Locator
|
||||
|
||||
// Components
|
||||
public readonly searchBox: ComfyNodeSearchBox
|
||||
public readonly menu: ComfyMenu
|
||||
@@ -143,6 +141,7 @@ export class ComfyPage {
|
||||
public readonly templates: ComfyTemplates
|
||||
public readonly settingDialog: SettingDialog
|
||||
public readonly confirmDialog: ConfirmDialog
|
||||
public readonly importModelDialog: ImportModelDialog
|
||||
|
||||
/** Worker index to test user ID */
|
||||
public readonly userIds: string[] = []
|
||||
@@ -162,14 +161,13 @@ export class ComfyPage {
|
||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
this.visibleToasts = page.locator('.p-toast-message:visible')
|
||||
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
this.menu = new ComfyMenu(page)
|
||||
this.actionbar = new ComfyActionbar(page)
|
||||
this.templates = new ComfyTemplates(page)
|
||||
this.settingDialog = new SettingDialog(page)
|
||||
this.confirmDialog = new ConfirmDialog(page)
|
||||
this.importModelDialog = new ImportModelDialog(page)
|
||||
}
|
||||
|
||||
convertLeafToContent(structure: FolderStructure): FolderStructure {
|
||||
@@ -275,6 +273,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()
|
||||
@@ -401,30 +400,6 @@ export class ComfyPage {
|
||||
await this.nextFrame()
|
||||
}
|
||||
|
||||
async deleteWorkflow(
|
||||
workflowName: string,
|
||||
whenMissing: 'ignoreMissing' | 'throwIfMissing' = 'ignoreMissing'
|
||||
) {
|
||||
// Open workflows tab
|
||||
const { workflowsTab } = this.menu
|
||||
await workflowsTab.open()
|
||||
|
||||
// Action to take if workflow missing
|
||||
if (whenMissing === 'ignoreMissing') {
|
||||
const workflows = await workflowsTab.getTopLevelSavedWorkflowNames()
|
||||
if (!workflows.includes(workflowName)) return
|
||||
}
|
||||
|
||||
// Delete workflow
|
||||
await workflowsTab.getPersistedItem(workflowName).click({ button: 'right' })
|
||||
await this.clickContextMenuItem('Delete')
|
||||
await this.confirmDialog.delete.click()
|
||||
|
||||
// Clear toast & close tab
|
||||
await this.closeToasts(1)
|
||||
await workflowsTab.close()
|
||||
}
|
||||
|
||||
async resetView() {
|
||||
if (await this.resetViewButton.isVisible()) {
|
||||
await this.resetViewButton.click()
|
||||
@@ -441,20 +416,7 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
async getVisibleToastCount() {
|
||||
return await this.visibleToasts.count()
|
||||
}
|
||||
|
||||
async closeToasts(requireCount = 0) {
|
||||
if (requireCount) await expect(this.visibleToasts).toHaveCount(requireCount)
|
||||
|
||||
// Clear all toasts
|
||||
const toastCloseButtons = await this.page
|
||||
.locator('.p-toast-close-button')
|
||||
.all()
|
||||
for (const button of toastCloseButtons) {
|
||||
await button.click()
|
||||
}
|
||||
await expect(this.visibleToasts).toHaveCount(0)
|
||||
return await this.page.locator('.p-toast-message:visible').count()
|
||||
}
|
||||
|
||||
async clickTextEncodeNode1() {
|
||||
@@ -510,6 +472,7 @@ export class ComfyPage {
|
||||
fileName?: string
|
||||
url?: string
|
||||
dropPosition?: Position
|
||||
buffer?: Buffer
|
||||
} = {}
|
||||
) {
|
||||
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
|
||||
@@ -528,7 +491,7 @@ export class ComfyPage {
|
||||
// Dropping a file from the filesystem
|
||||
if (fileName) {
|
||||
const filePath = this.assetPath(fileName)
|
||||
const buffer = fs.readFileSync(filePath)
|
||||
const buffer = options.buffer ?? fs.readFileSync(filePath)
|
||||
|
||||
const getFileType = (fileName: string) => {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
|
||||
17
browser_tests/fixtures/components/ImportModelDialog.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Page } from '@playwright/test'
|
||||
|
||||
export class ImportModelDialog {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
get rootEl() {
|
||||
return this.page.locator('div[aria-labelledby="global-import-model"]')
|
||||
}
|
||||
|
||||
get modelTypeInput() {
|
||||
return this.rootEl.locator('#model-type')
|
||||
}
|
||||
|
||||
get importButton() {
|
||||
return this.rootEl.getByLabel('Import')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { type ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
@@ -689,42 +689,3 @@ test.describe('Load duplicate workflow', () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Viewport settings', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar')
|
||||
|
||||
await comfyPage.setupWorkflowsDirectory({})
|
||||
})
|
||||
|
||||
test('Keeps viewport settings when changing tabs', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
// Screenshot the canvas element
|
||||
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-a.png')
|
||||
|
||||
// Save workflow as a new file, then zoom out before screen shot
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')
|
||||
await comfyMouse.move(comfyPage.emptySpace)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await comfyMouse.wheel(0, 60)
|
||||
}
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-b.png')
|
||||
|
||||
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
|
||||
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
|
||||
|
||||
// Go back to Workflow A
|
||||
await tabA.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-a.png')
|
||||
|
||||
// And back to Workflow B
|
||||
await tabB.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-b.png')
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 67 KiB |
@@ -53,26 +53,6 @@ test.describe('Combo text widget', () => {
|
||||
const refreshedComboValues = await getComboValues()
|
||||
expect(refreshedComboValues).not.toEqual(initialComboValues)
|
||||
})
|
||||
|
||||
test('Should refresh combo values of nodes with v2 combo input spec', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('node_with_v2_combo_input')
|
||||
// click canvas to focus
|
||||
await comfyPage.page.mouse.click(400, 300)
|
||||
// press R to trigger refresh
|
||||
await comfyPage.page.keyboard.press('r')
|
||||
// wait for nodes' widgets to be updated
|
||||
await comfyPage.page.mouse.click(400, 300)
|
||||
await comfyPage.nextFrame()
|
||||
// get the combo widget's values
|
||||
const comboValues = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].graph.nodes
|
||||
.find((node) => node.title === 'Node With V2 Combo Input')
|
||||
.widgets.find((widget) => widget.name === 'combo_input').options.values
|
||||
})
|
||||
expect(comboValues).toEqual(['A', 'B'])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Boolean widget', () => {
|
||||
|
||||
1717
package-lock.json
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.20.2",
|
||||
"version": "1.19.7",
|
||||
"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.8",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 647 B |
|
Before Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 700 B |
|
Before Width: | Height: | Size: 702 B |
|
Before Width: | Height: | Size: 705 B |
|
Before Width: | Height: | Size: 708 B |
|
Before Width: | Height: | Size: 705 B |
16
src/App.vue
@@ -16,8 +16,10 @@ import { computed, onMounted } from 'vue'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import { useDialogService } from './services/dialogService'
|
||||
import { electronAPI, isElectron } from './utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
@@ -46,6 +48,20 @@ onMounted(() => {
|
||||
|
||||
if (isElectron()) {
|
||||
document.addEventListener('contextmenu', showContextMenu)
|
||||
|
||||
// Handle file drops to import models via electron
|
||||
api.addEventListener('unhandledFileDrop', async (e) => {
|
||||
e.preventDefault() // Prevent unable to find workflow in file error
|
||||
|
||||
const filePath = await electronAPI()['getFilePath'](e.detail.file)
|
||||
|
||||
if (filePath) {
|
||||
useDialogService().showImportModelDialog({
|
||||
path: filePath,
|
||||
file: e.detail.file
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, reactive } from 'vue'
|
||||
|
||||
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
||||
import BrowserTabTitle from '@/components/BrowserTabTitle.vue'
|
||||
|
||||
// Mock the execution store
|
||||
const executionStore = reactive({
|
||||
@@ -30,8 +31,11 @@ vi.mock('@/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => workflowStore
|
||||
}))
|
||||
|
||||
describe('useBrowserTabTitle', () => {
|
||||
describe('BrowserTabTitle.vue', () => {
|
||||
let wrapper: ReturnType<typeof mount> | null
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = null
|
||||
// reset execution store
|
||||
executionStore.isIdle = true
|
||||
executionStore.executionProgress = 0
|
||||
@@ -46,8 +50,12 @@ describe('useBrowserTabTitle', () => {
|
||||
document.title = ''
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
})
|
||||
|
||||
it('sets default title when idle and no workflow', () => {
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
expect(document.title).toBe('ComfyUI')
|
||||
})
|
||||
|
||||
@@ -58,7 +66,7 @@ describe('useBrowserTabTitle', () => {
|
||||
isModified: false,
|
||||
isPersisted: true
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('myFlow - ComfyUI')
|
||||
})
|
||||
@@ -70,21 +78,19 @@ describe('useBrowserTabTitle', () => {
|
||||
isModified: true,
|
||||
isPersisted: true
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('*myFlow - ComfyUI')
|
||||
})
|
||||
|
||||
// Fails when run together with other tests. Suspect to be caused by leaked
|
||||
// state from previous tests.
|
||||
it.skip('disables workflow title when menu disabled', async () => {
|
||||
it('disables workflow title when menu disabled', async () => {
|
||||
;(settingStore.get as any).mockReturnValue('Disabled')
|
||||
workflowStore.activeWorkflow = {
|
||||
filename: 'myFlow',
|
||||
isModified: false,
|
||||
isPersisted: true
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('ComfyUI')
|
||||
})
|
||||
@@ -92,7 +98,7 @@ describe('useBrowserTabTitle', () => {
|
||||
it('shows execution progress when not idle without workflow', async () => {
|
||||
executionStore.isIdle = false
|
||||
executionStore.executionProgress = 0.3
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[30%]ComfyUI')
|
||||
})
|
||||
@@ -102,7 +108,7 @@ describe('useBrowserTabTitle', () => {
|
||||
executionStore.executionProgress = 0.4
|
||||
executionStore.executingNodeProgress = 0.5
|
||||
executionStore.executingNode = { type: 'Foo' }
|
||||
useBrowserTabTitle()
|
||||
wrapper = mount(BrowserTabTitle)
|
||||
await nextTick()
|
||||
expect(document.title).toBe('[40%][50%] Foo')
|
||||
})
|
||||
58
src/components/BrowserTabTitle.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- This component does not render anything visible. -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
const DEFAULT_TITLE = 'ComfyUI'
|
||||
const TITLE_SUFFIX = ' - ComfyUI'
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const executionText = computed(() =>
|
||||
executionStore.isIdle
|
||||
? ''
|
||||
: `[${Math.round(executionStore.executionProgress * 100)}%]`
|
||||
)
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const newMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const isUnsavedText = computed(() =>
|
||||
workflowStore.activeWorkflow?.isModified ||
|
||||
!workflowStore.activeWorkflow?.isPersisted
|
||||
? ' *'
|
||||
: ''
|
||||
)
|
||||
const workflowNameText = computed(() => {
|
||||
const workflowName = workflowStore.activeWorkflow?.filename
|
||||
return workflowName
|
||||
? isUnsavedText.value + workflowName + TITLE_SUFFIX
|
||||
: DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
executionText.value +
|
||||
(newMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)
|
||||
)
|
||||
|
||||
const title = computed(() => nodeExecutionTitle.value || workflowTitle.value)
|
||||
useTitle(title)
|
||||
</script>
|
||||
@@ -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)
|
||||
|
||||
@@ -32,8 +32,6 @@ describe('TreeExplorerTreeNode', () => {
|
||||
handleRename: () => {}
|
||||
} as RenderedTreeExplorerNode
|
||||
|
||||
const mockHandleEditLabel = vi.fn()
|
||||
|
||||
beforeAll(() => {
|
||||
// Create a Vue app instance for PrimeVuePrimeVue
|
||||
const app = createApp({})
|
||||
@@ -50,10 +48,7 @@ describe('TreeExplorerTreeNode', () => {
|
||||
props: { node: mockNode },
|
||||
global: {
|
||||
components: { EditableText, Badge },
|
||||
plugins: [createTestingPinia(), i18n],
|
||||
provide: {
|
||||
[InjectKeyHandleEditLabelFunction]: mockHandleEditLabel
|
||||
}
|
||||
plugins: [createTestingPinia(), i18n]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,10 +72,7 @@ describe('TreeExplorerTreeNode', () => {
|
||||
},
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue],
|
||||
provide: {
|
||||
[InjectKeyHandleEditLabelFunction]: mockHandleEditLabel
|
||||
}
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -70,12 +70,11 @@ const state = computed<GridState>(() => {
|
||||
const fromCol = fromRow * cols.value
|
||||
const toCol = toRow * cols.value
|
||||
const remainingCol = items.length - toCol
|
||||
const hasMoreToRender = remainingCol >= 0
|
||||
|
||||
return {
|
||||
start: clamp(fromCol, 0, items?.length),
|
||||
end: clamp(toCol, fromCol, items?.length),
|
||||
isNearEnd: hasMoreToRender && remainingCol <= cols.value * bufferRows
|
||||
isNearEnd: remainingCol <= cols.value * bufferRows
|
||||
}
|
||||
})
|
||||
const renderedItems = computed(() =>
|
||||
|
||||
74
src/components/dialog/content/ApiNodesNewsContent.vue
Normal 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>
|
||||
123
src/components/dialog/content/ImportModelDialogContent.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="px-4 py-2 h-full gap-2 flex flex-col">
|
||||
<h2 class="text-4xl font-normal my-0">
|
||||
{{ t('importModelDialog.title') }}
|
||||
</h2>
|
||||
<span class="text-muted">{{ path }}</span>
|
||||
<div class="flex flex-col gap-2 mt-4">
|
||||
<IftaLabel>
|
||||
<Select
|
||||
v-model="selectedType"
|
||||
:options="modelFolders"
|
||||
editable
|
||||
filter
|
||||
labelId="model-type"
|
||||
:disabled="importing"
|
||||
/>
|
||||
<label for="model-type">Type</label>
|
||||
</IftaLabel>
|
||||
</div>
|
||||
<Message severity="error" v-if="importError">{{ importError }}</Message>
|
||||
</div>
|
||||
<footer>
|
||||
<div class="flex justify-between gap-2 p-4">
|
||||
<SelectButton
|
||||
v-model="selectedImportMode"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:options="importModes"
|
||||
:disabled="importing"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
label="Cancel"
|
||||
severity="secondary"
|
||||
@click="dialogStore.closeDialog()"
|
||||
:disabled="importing"
|
||||
></Button>
|
||||
<Button
|
||||
type="button"
|
||||
label="Import"
|
||||
@click="importModel()"
|
||||
:icon="importIcon"
|
||||
:loading="importing"
|
||||
:disabled="!selectedType"
|
||||
></Button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import IftaLabel from 'primevue/iftalabel'
|
||||
import Message from 'primevue/message'
|
||||
import Select from 'primevue/select'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { guessModelType } from '@/utils/safetensorsUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
const { path, file } = defineProps<{
|
||||
path: string
|
||||
file: File
|
||||
}>()
|
||||
|
||||
const importModes = ref([
|
||||
{ label: t('importModelDialog.move'), value: 'move' },
|
||||
{ label: t('importModelDialog.copy'), value: 'copy' }
|
||||
])
|
||||
const modelStore = useModelStore()
|
||||
const modelFolders = ref<string[]>()
|
||||
const selectedType = ref<string>()
|
||||
const selectedImportMode = ref<string>('move')
|
||||
const importing = ref<boolean>(false)
|
||||
const importError = ref<string>()
|
||||
|
||||
const importIcon = computed(() => {
|
||||
return selectedImportMode.value === 'move'
|
||||
? 'pi pi-file-import'
|
||||
: 'pi pi-copy'
|
||||
})
|
||||
|
||||
const importModel = async () => {
|
||||
importing.value = true
|
||||
try {
|
||||
await electronAPI()?.['importModel'](
|
||||
file,
|
||||
selectedType.value,
|
||||
selectedImportMode.value
|
||||
)
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
dialogStore.closeDialog()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
importError.value = error.message
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
if (!modelStore.modelFolders.length) {
|
||||
await modelStore.loadModelFolders()
|
||||
}
|
||||
|
||||
modelFolders.value = modelStore.modelFolders.map((folder) => folder.directory)
|
||||
const type = await guessModelType(file)
|
||||
|
||||
if (!selectedType.value) {
|
||||
selectedType.value = type
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
</script>
|
||||
@@ -67,9 +67,9 @@ import Tabs from 'primevue/tabs'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSettingSearch } from '@/composables/setting/useSettingSearch'
|
||||
import { useSettingUI } from '@/composables/setting/useSettingUI'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { SettingTreeNode } from '@/stores/settingStore'
|
||||
import { ISettingGroup, SettingParams } from '@/types/settingTypes'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
@@ -107,7 +107,7 @@ const {
|
||||
getSearchResults
|
||||
} = useSettingSearch()
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
|
||||
// Sort groups for a category
|
||||
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
|
||||
@@ -140,7 +140,7 @@ watch(activeCategory, (_, oldValue) => {
|
||||
activeCategory.value = oldValue
|
||||
}
|
||||
if (activeCategory.value?.key === 'credits') {
|
||||
void authActions.fetchBalance()
|
||||
void authService.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -94,22 +94,13 @@
|
||||
<small class="text-muted text-center">
|
||||
{{ t('auth.apiKey.helpText') }}
|
||||
<a
|
||||
:href="`${COMFY_PLATFORM_BASE_URL}/login`"
|
||||
href="https://platform.comfy.org/login"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.apiKey.generateKey') }}
|
||||
</a>
|
||||
</small>
|
||||
<Message
|
||||
v-if="authActions.accessError.value"
|
||||
severity="info"
|
||||
icon="pi pi-info-circle"
|
||||
variant="outlined"
|
||||
closable
|
||||
>
|
||||
{{ t('toastMessages.useApiKeyTip') }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<!-- Terms & Contact -->
|
||||
@@ -143,12 +134,11 @@
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import Message from 'primevue/message'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
||||
import { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
|
||||
import ApiKeyForm from './signin/ApiKeyForm.vue'
|
||||
@@ -160,7 +150,7 @@ const { onSuccess } = defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
const isSecureContext = window.isSecureContext
|
||||
const isSignIn = ref(true)
|
||||
const showApiKeyForm = ref(false)
|
||||
@@ -171,25 +161,25 @@ const toggleState = () => {
|
||||
}
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
if (await authActions.signInWithGoogle()) {
|
||||
if (await authService.signInWithGoogle()) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const signInWithGithub = async () => {
|
||||
if (await authActions.signInWithGithub()) {
|
||||
if (await authService.signInWithGithub()) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const signInWithEmail = async (values: SignInData) => {
|
||||
if (await authActions.signInWithEmail(values.email, values.password)) {
|
||||
if (await authService.signInWithEmail(values.email, values.password)) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const signUpWithEmail = async (values: SignUpData) => {
|
||||
if (await authActions.signUpWithEmail(values.email, values.password)) {
|
||||
if (await authService.signUpWithEmail(values.email, values.password)) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
@@ -198,8 +188,4 @@ const userIsInChina = ref(false)
|
||||
onMounted(async () => {
|
||||
userIsInChina.value = await isInChina()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
authActions.accessError.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
|
||||
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
|
||||
|
||||
@@ -65,9 +65,9 @@ const {
|
||||
preselectedAmountOption?: number
|
||||
}>()
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
|
||||
const handleSeeDetails = async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
await authService.accessBillingPortal()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,10 +23,10 @@ import Button from 'primevue/button'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import PasswordFields from '@/components/dialog/content/signin/PasswordFields.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { updatePasswordSchema } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
const loading = ref(false)
|
||||
|
||||
const { onSuccess } = defineProps<{
|
||||
@@ -37,7 +37,7 @@ const onSubmit = async (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
await authActions.updatePassword(event.values.password)
|
||||
await authService.updatePassword(event.values.password)
|
||||
onSuccess()
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -41,9 +41,9 @@ import ProgressSpinner from 'primevue/progressspinner'
|
||||
import Tag from 'primevue/tag'
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
|
||||
const {
|
||||
amount,
|
||||
@@ -61,7 +61,7 @@ const loading = ref(false)
|
||||
|
||||
const handleBuyNow = async () => {
|
||||
loading.value = true
|
||||
await authActions.purchaseCredits(editable ? customAmount.value : amount)
|
||||
await authService.purchaseCredits(editable ? customAmount.value : amount)
|
||||
loading.value = false
|
||||
didClickBuyNow.value = true
|
||||
}
|
||||
@@ -69,7 +69,7 @@ const handleBuyNow = async () => {
|
||||
onBeforeUnmount(() => {
|
||||
if (didClickBuyNow.value) {
|
||||
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
|
||||
void authActions.fetchBalance()
|
||||
void authService.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
/>
|
||||
<div v-else class="h-full" @click="handleGridContainerClick">
|
||||
<VirtualGrid
|
||||
id="results-grid"
|
||||
:items="resultsWithKeys"
|
||||
:buffer-rows="3"
|
||||
:grid-style="GRID_STYLE"
|
||||
@@ -93,7 +92,7 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { merge } from 'lodash'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
@@ -200,10 +199,6 @@ const {
|
||||
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
|
||||
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
|
||||
|
||||
whenever(selectedTab, () => {
|
||||
pageNumber.value = 0
|
||||
})
|
||||
|
||||
const isUpdateAvailableTab = computed(
|
||||
() => selectedTab.value?.id === ManagerTab.UpdateAvailable
|
||||
)
|
||||
@@ -424,17 +419,6 @@ whenever(selectedNodePack, async () => {
|
||||
}
|
||||
})
|
||||
|
||||
let gridContainer: HTMLElement | null = null
|
||||
onMounted(() => {
|
||||
gridContainer = document.getElementById('results-grid')
|
||||
})
|
||||
watch(searchQuery, () => {
|
||||
gridContainer ??= document.getElementById('results-grid')
|
||||
if (gridContainer) {
|
||||
gridContainer.scrollTop = 0
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
getPackById.cancel()
|
||||
})
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
style: 'display: none'
|
||||
}
|
||||
}"
|
||||
:show-empty-message="false"
|
||||
@complete="stubTrue"
|
||||
@option-select="onOptionSelect"
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
text
|
||||
size="small"
|
||||
severity="secondary"
|
||||
@click="() => authActions.fetchBalance()"
|
||||
@click="() => authService.fetchBalance()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,8 +112,8 @@ import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
@@ -127,7 +127,7 @@ interface CreditHistoryItemData {
|
||||
const { t } = useI18n()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
@@ -142,7 +142,7 @@ const handlePurchaseCreditsClick = () => {
|
||||
}
|
||||
|
||||
const handleCreditsHistoryClick = async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
await authService.accessBillingPortal()
|
||||
}
|
||||
|
||||
const handleMessageSupport = () => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Tag from 'primevue/tag'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
@@ -21,16 +19,7 @@ describe('SettingItem', () => {
|
||||
const mountComponent = (props: any, options = {}): any => {
|
||||
return mount(SettingItem, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n, createPinia()],
|
||||
components: {
|
||||
Tag
|
||||
},
|
||||
directives: {
|
||||
tooltip: Tooltip
|
||||
},
|
||||
stubs: {
|
||||
'i-material-symbols:experiment-outline': true
|
||||
}
|
||||
plugins: [PrimeVue, i18n, createPinia()]
|
||||
},
|
||||
props,
|
||||
...options
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
<h3 class="font-medium">
|
||||
{{ $t('userSettings.email') }}
|
||||
</h3>
|
||||
<span class="text-muted">
|
||||
<a :href="'mailto:' + userEmail" class="hover:underline">
|
||||
{{ userEmail }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
|
||||
@@ -9,8 +9,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
||||
|
||||
import ApiKeyForm from './ApiKeyForm.vue'
|
||||
|
||||
const mockStoreApiKey = vi.fn()
|
||||
@@ -41,8 +39,7 @@ const i18n = createI18n({
|
||||
error: 'Invalid API Key',
|
||||
helpText: 'Need an API key?',
|
||||
generateKey: 'Get one here',
|
||||
whitelistInfo: 'About non-whitelisted sites',
|
||||
description: 'Use your Comfy API key to enable API Nodes'
|
||||
whitelistInfo: 'About non-whitelisted sites'
|
||||
}
|
||||
},
|
||||
g: {
|
||||
@@ -111,7 +108,7 @@ describe('ApiKeyForm', () => {
|
||||
const helpText = wrapper.find('small')
|
||||
expect(helpText.text()).toContain('Need an API key?')
|
||||
expect(helpText.find('a').attributes('href')).toBe(
|
||||
`${COMFY_PLATFORM_BASE_URL}/login`
|
||||
'https://platform.comfy.org/login'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<small class="text-muted">
|
||||
{{ t('auth.apiKey.helpText') }}
|
||||
<a
|
||||
:href="`${COMFY_PLATFORM_BASE_URL}/login`"
|
||||
href="https://platform.comfy.org/login"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
@@ -87,7 +87,6 @@ import Message from 'primevue/message'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
||||
import { apiKeySchema } from '@/schemas/signInSchema'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
@@ -80,12 +80,12 @@ import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
const firebaseAuthService = useFirebaseAuthService()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -102,6 +102,6 @@ const onSubmit = (event: FormSubmitEvent) => {
|
||||
|
||||
const handleForgotPassword = async (email: string) => {
|
||||
if (!email) return
|
||||
await firebaseAuthActions.sendPasswordReset(email)
|
||||
await firebaseAuthService.sendPasswordReset(email)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,17 +12,16 @@
|
||||
|
||||
<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'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { type DomWidgetState, useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
const widgetStates = computed(() =>
|
||||
Array.from(domWidgetStore.widgetStates.values())
|
||||
const widgetStates = computed(
|
||||
() => Array.from(domWidgetStore.widgetStates.values()) as DomWidgetState[]
|
||||
)
|
||||
|
||||
const updateWidgets = () => {
|
||||
@@ -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>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
class="w-full h-full touch-none"
|
||||
/>
|
||||
|
||||
<NodeBadge />
|
||||
<NodeTooltip v-if="tooltipEnabled" />
|
||||
<NodeSearchboxPopover />
|
||||
|
||||
@@ -52,6 +53,7 @@ import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import NodeBadge from '@/components/graph/NodeBadge.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
|
||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||
@@ -60,7 +62,6 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
||||
import { useCanvasDrop } from '@/composables/useCanvasDrop'
|
||||
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
|
||||
import { useCopy } from '@/composables/useCopy'
|
||||
@@ -70,7 +71,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'
|
||||
@@ -224,13 +225,39 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Save the drag & scale info in the serialized workflow if the setting is enabled
|
||||
watch(
|
||||
[
|
||||
() => canvasStore.canvas,
|
||||
() => settingStore.get('Comfy.EnableWorkflowViewRestore')
|
||||
],
|
||||
([canvas, enableWorkflowViewRestore]) => {
|
||||
const extra = canvas?.graph?.extra
|
||||
if (!extra) return
|
||||
|
||||
if (enableWorkflowViewRestore) {
|
||||
extra.ds = {
|
||||
get scale() {
|
||||
return canvas.ds.scale
|
||||
},
|
||||
get offset() {
|
||||
const [x, y] = canvas.ds.offset
|
||||
return [x, y]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
delete extra.ds
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
useEventListener(
|
||||
canvasRef,
|
||||
'litegraph:no-items-selected',
|
||||
() => {
|
||||
toastStore.add({
|
||||
severity: 'warn',
|
||||
summary: t('toastMessages.nothingSelected'),
|
||||
summary: 'No items selected',
|
||||
life: 2000
|
||||
})
|
||||
},
|
||||
@@ -253,7 +280,6 @@ const workflowPersistence = useWorkflowPersistence()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
useCanvasDrop(canvasRef)
|
||||
useLitegraphSettings()
|
||||
useNodeBadge()
|
||||
|
||||
onMounted(async () => {
|
||||
useGlobalLitegraph()
|
||||
@@ -267,7 +293,7 @@ onMounted(async () => {
|
||||
workspaceStore.spinner = true
|
||||
// ChangeTracker needs to be initialized before setup, as it will overwrite
|
||||
// some listeners of litegraph canvas.
|
||||
ChangeTracker.init()
|
||||
ChangeTracker.init(comfyApp)
|
||||
await loadCustomNodesI18n()
|
||||
try {
|
||||
await settingStore.loadSettingValues()
|
||||
@@ -292,8 +318,10 @@ onMounted(async () => {
|
||||
canvasStore.canvas.render_canvas_border = false
|
||||
workspaceStore.spinner = false
|
||||
|
||||
window.app = comfyApp
|
||||
window.graph = comfyApp.graph
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['app'] = comfyApp
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['graph'] = comfyApp.graph
|
||||
|
||||
comfyAppReady.value = true
|
||||
|
||||
|
||||
114
src/components/graph/NodeBadge.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- This component does not render anything visible. -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BadgePosition,
|
||||
LGraphBadge,
|
||||
type LGraphNode
|
||||
} from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const nodeSourceBadgeMode = computed(
|
||||
() => settingStore.get('Comfy.NodeBadge.NodeSourceBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
const nodeIdBadgeMode = computed(
|
||||
() => settingStore.get('Comfy.NodeBadge.NodeIdBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
const nodeLifeCycleBadgeMode = computed(
|
||||
() =>
|
||||
settingStore.get('Comfy.NodeBadge.NodeLifeCycleBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
|
||||
watch([nodeSourceBadgeMode, nodeIdBadgeMode, nodeLifeCycleBadgeMode], () => {
|
||||
app.graph?.setDirtyCanvas(true, true)
|
||||
})
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
function badgeTextVisible(
|
||||
nodeDef: ComfyNodeDefImpl | null,
|
||||
badgeMode: NodeBadgeMode
|
||||
): boolean {
|
||||
return !(
|
||||
badgeMode === NodeBadgeMode.None ||
|
||||
(nodeDef?.isCoreNode && badgeMode === NodeBadgeMode.HideBuiltIn)
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
app.registerExtension({
|
||||
name: 'Comfy.NodeBadge',
|
||||
nodeCreated(node: LGraphNode) {
|
||||
node.badgePosition = BadgePosition.TopRight
|
||||
|
||||
const badge = computed(() => {
|
||||
const nodeDef = nodeDefStore.fromLGraphNode(node)
|
||||
return new LGraphBadge({
|
||||
text: _.truncate(
|
||||
[
|
||||
badgeTextVisible(nodeDef, nodeIdBadgeMode.value)
|
||||
? `#${node.id}`
|
||||
: '',
|
||||
badgeTextVisible(nodeDef, nodeLifeCycleBadgeMode.value)
|
||||
? nodeDef?.nodeLifeCycleBadgeText ?? ''
|
||||
: '',
|
||||
badgeTextVisible(nodeDef, nodeSourceBadgeMode.value)
|
||||
? nodeDef?.nodeSource?.badgeText ?? ''
|
||||
: ''
|
||||
]
|
||||
.filter((s) => s.length > 0)
|
||||
.join(' '),
|
||||
{
|
||||
length: 31
|
||||
}
|
||||
),
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_BG_COLOR
|
||||
})
|
||||
})
|
||||
|
||||
node.badges.push(() => badge.value)
|
||||
|
||||
if (node.constructor.nodeData?.api_node) {
|
||||
const creditsBadge = computed(() => {
|
||||
return new LGraphBadge({
|
||||
text: '',
|
||||
iconOptions: {
|
||||
unicode: '\ue96b',
|
||||
fontFamily: 'PrimeIcons',
|
||||
color: '#FABC25',
|
||||
bgColor: '#353535',
|
||||
fontSize: 8
|
||||
},
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_BG_COLOR
|
||||
})
|
||||
})
|
||||
|
||||
node.badges.push(() => creditsBadge.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -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.canvas as LGraphCanvas)
|
||||
}, 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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<Button
|
||||
v-show="canvasStore.nodeSelected || canvasStore.groupSelected"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="() => (showColorPicker = !showColorPicker)"
|
||||
|
||||
@@ -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>
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="canvasStore.nodeSelected"
|
||||
v-tooltip.top="{
|
||||
value: isDisabled
|
||||
? t('selectionToolbox.executeButton.disabledTooltip')
|
||||
@@ -37,7 +36,7 @@ const buttonHovered = ref(false)
|
||||
const selectedOutputNodes = computed(
|
||||
() =>
|
||||
canvasStore.selectedItems.filter(
|
||||
(item) => isLGraphNode(item) && item.constructor.nodeData?.output_node
|
||||
(item) => isLGraphNode(item) && item.constructor.nodeData.output_node
|
||||
) as LGraphNode[]
|
||||
)
|
||||
|
||||
@@ -46,7 +45,7 @@ const isDisabled = computed(() => selectedOutputNodes.value.length === 0)
|
||||
function outputNodeStokeStyle(this: LGraphNode) {
|
||||
if (
|
||||
this.selected &&
|
||||
this.constructor.nodeData?.output_node &&
|
||||
this.constructor.nodeData.output_node &&
|
||||
buttonHovered.value
|
||||
) {
|
||||
return { color: 'orange', lineWidth: 2, padding: 10 }
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative w-full text-xs min-h-[28px] max-h-[200px] rounded-lg px-4 py-2 overflow-y-auto"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 break-all flex items-center gap-2">
|
||||
<span v-html="formattedText"></span>
|
||||
<Skeleton v-if="isParentNodeExecuting" class="!flex-1 !h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NodeId } from '@comfyorg/litegraph'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
defineProps<{
|
||||
widget?: object
|
||||
}>()
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const isParentNodeExecuting = ref(true)
|
||||
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
||||
|
||||
let executingNodeId: NodeId | null = null
|
||||
onMounted(() => {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
})
|
||||
|
||||
// Watch for either a new node has starting execution or overall execution ending
|
||||
const stopWatching = watch(
|
||||
[() => executionStore.executingNode, () => executionStore.isIdle],
|
||||
() => {
|
||||
if (
|
||||
executionStore.isIdle ||
|
||||
(executionStore.executingNode &&
|
||||
executionStore.executingNode.id !== executingNodeId)
|
||||
) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
}
|
||||
if (!executingNodeId) {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -17,16 +16,9 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const { isExtensionInstalled, isExtensionEnabled } = useExtensionStore()
|
||||
|
||||
const vhsAdvancedPreviews = computed(() => {
|
||||
return (
|
||||
isExtensionInstalled('VideoHelperSuite.Core') &&
|
||||
isExtensionEnabled('VideoHelperSuite.Core') &&
|
||||
settingStore.get('VHS.AdvancedPreviews') &&
|
||||
settingStore.get('VHS.AdvancedPreviews') !== 'Never'
|
||||
)
|
||||
})
|
||||
const vhsAdvancedPreviews = computed(() =>
|
||||
settingStore.get('VHS.AdvancedPreviews')
|
||||
)
|
||||
|
||||
const url = computed(() =>
|
||||
vhsAdvancedPreviews.value
|
||||
|
||||
@@ -69,11 +69,11 @@ import { onMounted } from 'vue'
|
||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
|
||||
const { userDisplayName, userEmail, userPhotoUrl } = useCurrentUser()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authService = useFirebaseAuthService()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
@@ -89,6 +89,6 @@ const handleOpenApiPricing = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void authActions.fetchBalance()
|
||||
void authService.fetchBalance()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import {
|
||||
BadgePosition,
|
||||
LGraphBadge,
|
||||
type LGraphNode
|
||||
} from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
|
||||
/**
|
||||
* Add LGraphBadge to LGraphNode based on settings.
|
||||
*
|
||||
* Following badges are added:
|
||||
* - Node ID badge
|
||||
* - Node source badge
|
||||
* - Node life cycle badge
|
||||
* - API node credits badge
|
||||
*/
|
||||
export const useNodeBadge = () => {
|
||||
const settingStore = useSettingStore()
|
||||
const extensionStore = useExtensionStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const nodeSourceBadgeMode = computed(
|
||||
() =>
|
||||
settingStore.get('Comfy.NodeBadge.NodeSourceBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
const nodeIdBadgeMode = computed(
|
||||
() => settingStore.get('Comfy.NodeBadge.NodeIdBadgeMode') as NodeBadgeMode
|
||||
)
|
||||
const nodeLifeCycleBadgeMode = computed(
|
||||
() =>
|
||||
settingStore.get(
|
||||
'Comfy.NodeBadge.NodeLifeCycleBadgeMode'
|
||||
) as NodeBadgeMode
|
||||
)
|
||||
|
||||
watch([nodeSourceBadgeMode, nodeIdBadgeMode, nodeLifeCycleBadgeMode], () => {
|
||||
app.graph?.setDirtyCanvas(true, true)
|
||||
})
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
function badgeTextVisible(
|
||||
nodeDef: ComfyNodeDefImpl | null,
|
||||
badgeMode: NodeBadgeMode
|
||||
): boolean {
|
||||
return !(
|
||||
badgeMode === NodeBadgeMode.None ||
|
||||
(nodeDef?.isCoreNode && badgeMode === NodeBadgeMode.HideBuiltIn)
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
extensionStore.registerExtension({
|
||||
name: 'Comfy.NodeBadge',
|
||||
nodeCreated(node: LGraphNode) {
|
||||
node.badgePosition = BadgePosition.TopRight
|
||||
|
||||
const badge = computed(() => {
|
||||
const nodeDef = nodeDefStore.fromLGraphNode(node)
|
||||
return new LGraphBadge({
|
||||
text: _.truncate(
|
||||
[
|
||||
badgeTextVisible(nodeDef, nodeIdBadgeMode.value)
|
||||
? `#${node.id}`
|
||||
: '',
|
||||
badgeTextVisible(nodeDef, nodeLifeCycleBadgeMode.value)
|
||||
? nodeDef?.nodeLifeCycleBadgeText ?? ''
|
||||
: '',
|
||||
badgeTextVisible(nodeDef, nodeSourceBadgeMode.value)
|
||||
? nodeDef?.nodeSource?.badgeText ?? ''
|
||||
: ''
|
||||
]
|
||||
.filter((s) => s.length > 0)
|
||||
.join(' '),
|
||||
{
|
||||
length: 31
|
||||
}
|
||||
),
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_BG_COLOR
|
||||
})
|
||||
})
|
||||
|
||||
node.badges.push(() => badge.value)
|
||||
|
||||
if (node.constructor.nodeData?.api_node) {
|
||||
const creditsBadge = computed(() => {
|
||||
return new LGraphBadge({
|
||||
text: '',
|
||||
iconOptions: {
|
||||
unicode: '\ue96b',
|
||||
fontFamily: 'PrimeIcons',
|
||||
color: '#FABC25',
|
||||
bgColor: '#353535',
|
||||
fontSize: 8
|
||||
},
|
||||
fgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_FG_COLOR,
|
||||
bgColor:
|
||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
.BADGE_BG_COLOR
|
||||
})
|
||||
})
|
||||
|
||||
node.badges.push(() => creditsBadge.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useTextPreviewWidget } from '@/composables/widgets/useProgressTextWidget'
|
||||
|
||||
const TEXT_PREVIEW_WIDGET_NAME = '$$node-text-preview'
|
||||
|
||||
/**
|
||||
* Composable for handling node text previews
|
||||
*/
|
||||
export function useNodeProgressText() {
|
||||
const textPreviewWidget = useTextPreviewWidget()
|
||||
|
||||
const findTextPreviewWidget = (node: LGraphNode) =>
|
||||
node.widgets?.find((w) => w.name === TEXT_PREVIEW_WIDGET_NAME)
|
||||
|
||||
const addTextPreviewWidget = (node: LGraphNode) =>
|
||||
textPreviewWidget(node, {
|
||||
name: TEXT_PREVIEW_WIDGET_NAME,
|
||||
type: 'progressText'
|
||||
})
|
||||
|
||||
/**
|
||||
* Shows text preview for a node
|
||||
* @param node The graph node to show the preview for
|
||||
*/
|
||||
function showTextPreview(node: LGraphNode, text: string) {
|
||||
const widget = findTextPreviewWidget(node) ?? addTextPreviewWidget(node)
|
||||
widget.value = text
|
||||
node.setDirtyCanvas?.(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes text preview from a node
|
||||
* @param node The graph node to remove the preview from
|
||||
*/
|
||||
function removeTextPreview(node: LGraphNode) {
|
||||
if (!node.widgets) return
|
||||
|
||||
const widgetIdx = node.widgets.findIndex(
|
||||
(w) => w.name === TEXT_PREVIEW_WIDGET_NAME
|
||||
)
|
||||
|
||||
if (widgetIdx > -1) {
|
||||
node.widgets[widgetIdx].onRemove?.()
|
||||
node.widgets.splice(widgetIdx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showTextPreview,
|
||||
removeTextPreview
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
const DEFAULT_TITLE = 'ComfyUI'
|
||||
const TITLE_SUFFIX = ' - ComfyUI'
|
||||
|
||||
export const useBrowserTabTitle = () => {
|
||||
const executionStore = useExecutionStore()
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const executionText = computed(() =>
|
||||
executionStore.isIdle
|
||||
? ''
|
||||
: `[${Math.round(executionStore.executionProgress * 100)}%]`
|
||||
)
|
||||
|
||||
const newMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
const isUnsavedText = computed(() =>
|
||||
workflowStore.activeWorkflow?.isModified ||
|
||||
!workflowStore.activeWorkflow?.isPersisted
|
||||
? ' *'
|
||||
: ''
|
||||
)
|
||||
const workflowNameText = computed(() => {
|
||||
const workflowName = workflowStore.activeWorkflow?.filename
|
||||
return workflowName
|
||||
? isUnsavedText.value + workflowName + TITLE_SUFFIX
|
||||
: DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
|
||||
const workflowTitle = computed(
|
||||
() =>
|
||||
executionText.value +
|
||||
(newMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)
|
||||
)
|
||||
|
||||
const title = computed(() => nodeExecutionTitle.value || workflowTitle.value)
|
||||
useTitle(title)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
@@ -14,6 +13,7 @@ import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
@@ -32,7 +32,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const dialogService = useDialogService()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
const firebaseAuthService = useFirebaseAuthService()
|
||||
const toastStore = useToastStore()
|
||||
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
|
||||
|
||||
@@ -320,7 +320,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: async () => {
|
||||
const batchCount = useQueueSettingsStore().batchCount
|
||||
const queueNodeIds = getSelectedNodes()
|
||||
.filter((node) => node.constructor.nodeData?.output_node)
|
||||
.filter((node) => node.constructor.nodeData.output_node)
|
||||
.map((node) => node.id)
|
||||
if (queueNodeIds.length === 0) {
|
||||
toastStore.add({
|
||||
@@ -671,7 +671,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Sign Out',
|
||||
versionAdded: '1.18.1',
|
||||
function: async () => {
|
||||
await firebaseAuthActions.logout()
|
||||
await firebaseAuthService.logout()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -128,10 +128,4 @@ export const useLitegraphSettings = () => {
|
||||
'LiteGraph.Pointer.TrackpadGestures'
|
||||
)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
LiteGraph.saveViewportWithGraph = settingStore.get(
|
||||
'Comfy.EnableWorkflowViewRestore'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useFavicon } from '@vueuse/core'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
|
||||
export const useProgressFavicon = () => {
|
||||
const defaultFavicon = '/assets/images/favicon_progress_16x16/frame_9.png'
|
||||
const favicon = useFavicon(defaultFavicon)
|
||||
const executionStore = useExecutionStore()
|
||||
const totalFrames = 10
|
||||
|
||||
watch(
|
||||
[() => executionStore.executionProgress, () => executionStore.isIdle],
|
||||
([progress, isIdle]) => {
|
||||
if (isIdle) {
|
||||
favicon.value = defaultFavicon
|
||||
} else {
|
||||
const frame = Math.floor(progress * totalFrames)
|
||||
favicon.value = `/assets/images/favicon_progress_16x16/frame_${frame}.png`
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import type { Hit } from 'algoliasearch/dist/lite/browser'
|
||||
import { memoize, orderBy } from 'lodash'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
AlgoliaNodePack,
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
import type { NodesIndexSuggestion } from '@/services/algoliaSearchService'
|
||||
import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
|
||||
|
||||
const SEARCH_DEBOUNCE_TIME = 320
|
||||
const SEARCH_DEBOUNCE_TIME = 256
|
||||
const DEFAULT_PAGE_SIZE = 64
|
||||
const DEFAULT_SORT_FIELD = SortableAlgoliaField.Downloads // Set in the index configuration
|
||||
const DEFAULT_MAX_CACHE_SIZE = 64
|
||||
|
||||
const SORT_DIRECTIONS: Record<SortableAlgoliaField, 'asc' | 'desc'> = {
|
||||
[SortableAlgoliaField.Downloads]: 'desc',
|
||||
[SortableAlgoliaField.Created]: 'desc',
|
||||
@@ -30,12 +30,7 @@ const isDateField = (field: SortableAlgoliaField): boolean =>
|
||||
/**
|
||||
* Composable for managing UI state of Comfy Node Registry search.
|
||||
*/
|
||||
export function useRegistrySearch(
|
||||
options: {
|
||||
maxCacheSize?: number
|
||||
} = {}
|
||||
) {
|
||||
const { maxCacheSize = DEFAULT_MAX_CACHE_SIZE } = options
|
||||
export function useRegistrySearch() {
|
||||
const isLoading = ref(false)
|
||||
const sortField = ref<SortableAlgoliaField>(SortableAlgoliaField.Downloads)
|
||||
const searchMode = ref<'nodes' | 'packs'>('packs')
|
||||
@@ -61,10 +56,7 @@ export function useRegistrySearch(
|
||||
: []
|
||||
)
|
||||
|
||||
const { searchPacksCached, toRegistryPack, clearSearchPacksCache } =
|
||||
useAlgoliaSearchService({
|
||||
maxCacheSize
|
||||
})
|
||||
const { searchPacks, toRegistryPack } = useAlgoliaSearchService()
|
||||
|
||||
const algoliaToRegistry = memoize(
|
||||
toRegistryPack,
|
||||
@@ -85,7 +77,7 @@ export function useRegistrySearch(
|
||||
if (!options.append) {
|
||||
pageNumber.value = 0
|
||||
}
|
||||
const { nodePacks, querySuggestions } = await searchPacksCached(
|
||||
const { nodePacks, querySuggestions } = await searchPacks(
|
||||
searchQuery.value,
|
||||
{
|
||||
pageSize: pageSize.value,
|
||||
@@ -124,8 +116,6 @@ export function useRegistrySearch(
|
||||
immediate: true
|
||||
})
|
||||
|
||||
onUnmounted(clearSearchPacksCache)
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
pageNumber,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { calculateImageGrid } from '@/scripts/ui/imagePreview'
|
||||
import { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { is_all_same_aspect_ratio } from '@/utils/imageUtil'
|
||||
|
||||
@@ -17,7 +16,7 @@ const renderPreview = (
|
||||
node: LGraphNode,
|
||||
shiftY: number
|
||||
) => {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
const canvas = app.canvas
|
||||
const mouse = canvas.graph_mouse
|
||||
|
||||
if (!canvas.pointer_is_down && node.pointerDown) {
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import TextPreviewWidget from '@/components/graph/widgets/TextPreviewWidget.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 useTextPreviewWidget = (
|
||||
options: {
|
||||
minHeight?: number
|
||||
} = {}
|
||||
) => {
|
||||
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpec
|
||||
) => {
|
||||
const widgetValue = ref<string>('')
|
||||
const widget = new ComponentWidgetImpl<string | object>({
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: TextPreviewWidget,
|
||||
inputSpec,
|
||||
options: {
|
||||
getValue: () => widgetValue.value,
|
||||
setValue: (value: string | object) => {
|
||||
widgetValue.value = typeof value === 'string' ? value : String(value)
|
||||
},
|
||||
getMinHeight: () => options.minHeight ?? 42 + PADDING
|
||||
}
|
||||
})
|
||||
addWidget(node, widget)
|
||||
return widget
|
||||
}
|
||||
|
||||
return widgetConstructor
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
export const COMFY_API_BASE_URL = __USE_PROD_CONFIG__
|
||||
? 'https://api.comfy.org'
|
||||
: 'https://stagingapi.comfy.org'
|
||||
|
||||
export const COMFY_PLATFORM_BASE_URL = __USE_PROD_CONFIG__
|
||||
? 'https://platform.comfy.org'
|
||||
: 'https://stagingplatform.comfy.org'
|
||||
|
||||
@@ -797,7 +797,7 @@ export class GroupNodeConfig {
|
||||
|
||||
export class GroupNodeHandler {
|
||||
node: LGraphNode
|
||||
groupData: any
|
||||
groupData
|
||||
innerNodes: any
|
||||
|
||||
constructor(node: LGraphNode) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -401,7 +401,6 @@ app.registerExtension({
|
||||
// @ts-expect-error
|
||||
data.groupNodes = {}
|
||||
}
|
||||
if (nodeData == null) throw new TypeError('nodeData is not set')
|
||||
// @ts-expect-error
|
||||
data.groupNodes[nodeData.name] = groupData
|
||||
// @ts-expect-error
|
||||
|
||||
@@ -3,7 +3,6 @@ Preview Any - original implement from
|
||||
https://github.com/rgthree/rgthree-comfy/blob/main/py/display_any.py
|
||||
upstream requested in https://github.com/Kosinkadink/rfcs/blob/main/rfcs/0000-corenodes.md#preview-nodes
|
||||
*/
|
||||
import { app } from '@/scripts/app'
|
||||
import { DOMWidget } from '@/scripts/domWidget'
|
||||
import { ComfyWidgets } from '@/scripts/widgets'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
@@ -117,10 +117,7 @@ app.registerExtension({
|
||||
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio)
|
||||
audioUIWidget.serialize = false
|
||||
|
||||
const { nodeData } = node.constructor
|
||||
if (nodeData == null) throw new TypeError('nodeData is null')
|
||||
|
||||
const isOutputNode = nodeData.output_node
|
||||
const isOutputNode = node.constructor.nodeData.output_node
|
||||
if (isOutputNode) {
|
||||
// Hide the audio widget when there is no audio initially.
|
||||
audioUIWidget.element.classList.add('empty-audio-widget')
|
||||
|
||||
@@ -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",
|
||||
@@ -1261,9 +1259,7 @@
|
||||
"failedToInitiateCreditPurchase": "Failed to initiate credit purchase: {error}",
|
||||
"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"
|
||||
"unauthorizedDomain": "Your domain {domain} is not authorized to use this service. Please contact {email} to add your domain to the whitelist."
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
@@ -1384,17 +1380,36 @@
|
||||
"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"
|
||||
"importModelDialog": {
|
||||
"title": "Import Model",
|
||||
"type": "Type",
|
||||
"move": "Move",
|
||||
"copy": "Copy"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -1352,7 +1362,6 @@
|
||||
"unableToGetModelFilePath": "No se puede obtener la ruta del archivo del modelo",
|
||||
"unauthorizedDomain": "Tu dominio {domain} no está autorizado para usar este servicio. Por favor, contacta a {email} para agregar tu dominio a la lista blanca.",
|
||||
"updateRequested": "Actualización solicitada",
|
||||
"useApiKeyTip": "Consejo: ¿No puedes acceder al inicio de sesión normal? Usa la opción de clave API de Comfy.",
|
||||
"userNotAuthenticated": "Usuario no autenticado"
|
||||
},
|
||||
"userSelect": {
|
||||
|
||||
@@ -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é",
|
||||
@@ -385,6 +396,12 @@
|
||||
"inbox": "Boîte de réception",
|
||||
"star": "Étoile"
|
||||
},
|
||||
"importModelDialog": {
|
||||
"copy": "Copier",
|
||||
"move": "Déplacer",
|
||||
"title": "Importer le modèle",
|
||||
"type": "Type"
|
||||
},
|
||||
"install": {
|
||||
"appDataLocationTooltip": "Répertoire des données de l'application ComfyUI. Stocke :\n- Logs\n- Configurations du serveur",
|
||||
"appPathLocationTooltip": "Répertoire des ressources de l'application ComfyUI. Stocke le code et les ressources de ComfyUI",
|
||||
@@ -1343,7 +1360,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 d’attente",
|
||||
"pendingTasksDeleted": "Tâches en attente supprimées",
|
||||
@@ -1352,7 +1368,6 @@
|
||||
"unableToGetModelFilePath": "Impossible d'obtenir le chemin du fichier modèle",
|
||||
"unauthorizedDomain": "Votre domaine {domain} n'est pas autorisé à utiliser ce service. Veuillez contacter {email} pour ajouter votre domaine à la liste blanche.",
|
||||
"updateRequested": "Mise à jour demandée",
|
||||
"useApiKeyTip": "Astuce : Vous ne pouvez pas accéder à la connexion normale ? Utilisez l’option Clé API Comfy.",
|
||||
"userNotAuthenticated": "Utilisateur non authentifié"
|
||||
},
|
||||
"userSelect": {
|
||||
|
||||
@@ -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": "有効",
|
||||
@@ -385,6 +396,12 @@
|
||||
"inbox": "受信トレイ",
|
||||
"star": "星"
|
||||
},
|
||||
"importModelDialog": {
|
||||
"copy": "コピー",
|
||||
"move": "移動",
|
||||
"title": "モデルをインポート",
|
||||
"type": "タイプ"
|
||||
},
|
||||
"install": {
|
||||
"appDataLocationTooltip": "ComfyUIのアプリデータディレクトリ。保存内容:\n- ログ\n- サーバー設定",
|
||||
"appPathLocationTooltip": "ComfyUIのアプリ資産ディレクトリ。ComfyUIのコードとアセットを保存します",
|
||||
@@ -1343,7 +1360,6 @@
|
||||
"no3dSceneToExport": "エクスポートする3Dシーンがありません",
|
||||
"noTemplatesToExport": "エクスポートするテンプレートがありません",
|
||||
"nodeDefinitionsUpdated": "ノード定義が更新されました",
|
||||
"nothingSelected": "選択されていません",
|
||||
"nothingToGroup": "グループ化するものがありません",
|
||||
"nothingToQueue": "キューに追加する項目がありません",
|
||||
"pendingTasksDeleted": "保留中のタスクが削除されました",
|
||||
@@ -1352,7 +1368,6 @@
|
||||
"unableToGetModelFilePath": "モデルファイルのパスを取得できません",
|
||||
"unauthorizedDomain": "あなたのドメイン {domain} はこのサービスを利用する権限がありません。ご利用のドメインをホワイトリストに追加するには、{email} までご連絡ください。",
|
||||
"updateRequested": "更新が要求されました",
|
||||
"useApiKeyTip": "ヒント:通常のログインにアクセスできませんか?Comfy APIキーオプションを使用してください。",
|
||||
"userNotAuthenticated": "ユーザーが認証されていません"
|
||||
},
|
||||
"userSelect": {
|
||||
|
||||
@@ -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": "활성화됨",
|
||||
@@ -385,6 +396,12 @@
|
||||
"inbox": "받은 편지함",
|
||||
"star": "별"
|
||||
},
|
||||
"importModelDialog": {
|
||||
"copy": "복사",
|
||||
"move": "이동",
|
||||
"title": "모델 가져오기",
|
||||
"type": "유형"
|
||||
},
|
||||
"install": {
|
||||
"appDataLocationTooltip": "ComfyUI의 앱 데이터 디렉토리. 저장소:\n- 로그\n- 서버 구성",
|
||||
"appPathLocationTooltip": "ComfyUI의 앱 에셋 디렉토리. ComfyUI 코드 및 에셋을 저장합니다.",
|
||||
@@ -644,20 +661,20 @@
|
||||
"Tolerance": "허용 오차"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "자동 실행 대기열",
|
||||
"autoQueue": "자동 실행 큐",
|
||||
"batchCount": "배치 수",
|
||||
"batchCountTooltip": "워크플로 작업을 실행 대기열에 반복 추가할 횟수",
|
||||
"batchCountTooltip": "워크플로 작업을 실행 큐에 반복 추가할 횟수",
|
||||
"clear": "워크플로 비우기",
|
||||
"clipspace": "클립스페이스 열기",
|
||||
"disabled": "비활성화됨",
|
||||
"disabledTooltip": "워크플로 작업을 자동으로 실행 대기열에 추가하지 않습니다.",
|
||||
"disabledTooltip": "워크플로 작업을 자동으로 실행 큐에 추가하지 않습니다.",
|
||||
"execute": "실행",
|
||||
"hideMenu": "메뉴 숨기기",
|
||||
"instant": "즉시",
|
||||
"instantTooltip": "워크플로 실행이 완료되면 즉시 실행 대기열에 추가합니다.",
|
||||
"instantTooltip": "워크플로 실행이 완료되면 즉시 실행 큐에 추가합니다.",
|
||||
"interrupt": "현재 실행 취소",
|
||||
"onChange": "변경 시",
|
||||
"onChangeTooltip": "변경이 있는 경우에만 워크플로를 실행 대기열에 추가합니다.",
|
||||
"onChangeTooltip": "변경이 있는 경우에만 워크플로를 실행 큐에 추가합니다.",
|
||||
"refresh": "노드 정의 새로 고침",
|
||||
"resetView": "캔버스 보기 재설정",
|
||||
"run": "실행",
|
||||
@@ -714,8 +731,8 @@
|
||||
"Pin/Unpin Selected Items": "선택한 항목 고정/고정 해제",
|
||||
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
|
||||
"Previous Opened Workflow": "이전 열린 워크플로",
|
||||
"Queue Prompt": "실행 대기열에 프롬프트 추가",
|
||||
"Queue Prompt (Front)": "실행 대기열 맨 앞에 프롬프트 추가",
|
||||
"Queue Prompt": "실행 큐에 프롬프트 추가",
|
||||
"Queue Prompt (Front)": "실행 큐 맨 앞에 프롬프트 추가",
|
||||
"Queue Selected Output Nodes": "선택한 출력 노드 대기열에 추가",
|
||||
"Quit": "종료",
|
||||
"Redo": "다시 실행",
|
||||
@@ -734,7 +751,7 @@
|
||||
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
|
||||
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
|
||||
"Toggle Progress Dialog": "진행 상황 대화 상자 전환",
|
||||
"Toggle Queue Sidebar": "실행 대기열 사이드바 전환",
|
||||
"Toggle Queue Sidebar": "실행 큐 사이드바 전환",
|
||||
"Toggle Search Box": "검색 상자 전환",
|
||||
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
|
||||
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
|
||||
@@ -805,7 +822,7 @@
|
||||
"photomaker": "포토메이커",
|
||||
"postprocessing": "후처리",
|
||||
"preprocessors": "전처리기",
|
||||
"primitive": "기본 입력",
|
||||
"primitive": "프리미티브",
|
||||
"samplers": "샘플러",
|
||||
"sampling": "샘플링",
|
||||
"schedulers": "스케줄러",
|
||||
@@ -1030,8 +1047,8 @@
|
||||
"Node Widget": "노드 위젯",
|
||||
"NodeLibrary": "노드 라이브러리",
|
||||
"Pointer": "포인터",
|
||||
"Queue": "실행 대기열",
|
||||
"QueueButton": "실행 대기열 버튼",
|
||||
"Queue": "실행 큐",
|
||||
"QueueButton": "실행 큐 버튼",
|
||||
"Reroute": "경유점",
|
||||
"RerouteBeta": "경유점 (베타)",
|
||||
"Scene": "장면",
|
||||
@@ -1057,7 +1074,7 @@
|
||||
"sortOrder": "정렬 순서"
|
||||
},
|
||||
"openWorkflow": "로컬 파일 시스템에서 워크플로 열기",
|
||||
"queue": "실행 대기열",
|
||||
"queue": "실행 큐",
|
||||
"queueTab": {
|
||||
"backToAllTasks": "모든 작업으로 돌아가기",
|
||||
"clearPendingTasks": "보류 중인 작업 지우기",
|
||||
@@ -1248,28 +1265,28 @@
|
||||
"mixing_controlnets": "여러 ControlNet 모델을 결합합니다."
|
||||
},
|
||||
"Flux": {
|
||||
"flux_canny_model_example": "검출된 경계선으로 이미지를 생성합니다.",
|
||||
"flux_depth_lora_example": "깊이 인식 LoRA 를 이용해 이미지를 생성합니다.",
|
||||
"flux_dev_checkpoint_example": "FLUX Dev 모델로 이미지를 생성합니다.",
|
||||
"flux_canny_model_example": "에지 감지로부터 이미지를 생성합니다.",
|
||||
"flux_depth_lora_example": "깊이 인식 LoRA로 이미지를 생성합니다.",
|
||||
"flux_dev_checkpoint_example": "Flux 개발 모델로 이미지를 생성합니다.",
|
||||
"flux_fill_inpaint_example": "이미지의 누락된 부분을 채웁니다.",
|
||||
"flux_fill_outpaint_example": "FLUX 아웃페인팅으로 이미지를 확장합니다.",
|
||||
"flux_fill_outpaint_example": "Flux 아웃페인팅으로 이미지를 확장합니다.",
|
||||
"flux_redux_model_example": "참조 이미지의 스타일을 가이드 이미지 생성에 적용합니다.",
|
||||
"flux_schnell": "FLUX Schnell 모델로 이미지를 빠르게 생성합니다."
|
||||
"flux_schnell": "Flux Schnell로 이미지를 빠르게 생성합니다."
|
||||
},
|
||||
"Image": {
|
||||
"hidream_e1_full": "HiDream E1 모델로 이미지를 편집합니다.",
|
||||
"hidream_i1_dev": "HiDream I1 Dev 모델로 이미지를 생성합니다.",
|
||||
"hidream_i1_fast": "HiDream I1 Fast 모델로 이미지를 빠르게 생성합니다.",
|
||||
"hidream_i1_full": "HiDream I1 Full 모델로 이미지를 생성합니다.",
|
||||
"sd3_5_large_blur": "SD 3.5 모델로 흐릿한 참조 이미지에서 이미지를 생성합니다.",
|
||||
"sd3_5_large_canny_controlnet_example": "Canny 에지 이미지를 통해 SD 3.5 모델 이미지 생성을 가이드합니다.",
|
||||
"sd3_5_large_depth": "깊이 인식 이미지를 통해 SD 3.5 모델 이미지 생성을 가이드합니다.",
|
||||
"sd3_5_simple_example": "SD 3.5 모델로 이미지를 생성합니다.",
|
||||
"hidream_e1_full": "HiDream E1로 이미지를 편집합니다.",
|
||||
"hidream_i1_dev": "HiDream I1 Dev로 이미지를 생성합니다.",
|
||||
"hidream_i1_fast": "HiDream I1로 이미지를 빠르게 생성합니다.",
|
||||
"hidream_i1_full": "HiDream I1로 이미지를 생성합니다.",
|
||||
"sd3_5_large_blur": "SD 3.5로 흐릿한 참조 이미지에서 이미지를 생성합니다.",
|
||||
"sd3_5_large_canny_controlnet_example": "SD 3.5에서 에지 감지로 이미지 생성을 가이드합니다.",
|
||||
"sd3_5_large_depth": "SD 3.5로 깊이 인식 이미지를 생성합니다.",
|
||||
"sd3_5_simple_example": "SD 3.5로 이미지를 생성합니다.",
|
||||
"sdxl_refiner_prompt_example": "SDXL 결과물을 리파이너로 향상시킵니다.",
|
||||
"sdxl_revision_text_prompts": "참조 이미지의 개념을 SDXL 이미지 생성에 적용합니다.",
|
||||
"sdxl_revision_zero_positive": "참조 이미지와 함께 텍스트 프롬프트를 추가하여 SDXL 이미지 생성을 가이드합니다.",
|
||||
"sdxl_simple_example": "SDXL 모델로 고품질 이미지를 생성합니다.",
|
||||
"sdxlturbo_example": "SDXL Turbo 모델로 1 스텝으로 이미지를 생성합니다."
|
||||
"sdxl_simple_example": "SDXL로 고품질 이미지를 생성합니다.",
|
||||
"sdxlturbo_example": "SDXL Turbo로 한 번에 이미지를 생성합니다."
|
||||
},
|
||||
"Image API": {
|
||||
"api-openai-dall-e-2-inpaint": "Dall-E 2 API로 이미지를 인페인팅합니다.",
|
||||
@@ -1343,7 +1360,6 @@
|
||||
"no3dSceneToExport": "내보낼 3D 장면이 없습니다",
|
||||
"noTemplatesToExport": "내보낼 템플릿이 없습니다",
|
||||
"nodeDefinitionsUpdated": "노드 정의가 업데이트되었습니다",
|
||||
"nothingSelected": "선택된 항목이 없습니다",
|
||||
"nothingToGroup": "그룹화할 항목이 없습니다",
|
||||
"nothingToQueue": "대기열에 추가할 항목이 없습니다",
|
||||
"pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다",
|
||||
@@ -1352,7 +1368,6 @@
|
||||
"unableToGetModelFilePath": "모델 파일 경로를 가져올 수 없습니다",
|
||||
"unauthorizedDomain": "귀하의 도메인 {domain}은(는) 이 서비스를 사용할 수 있는 권한이 없습니다. 도메인을 허용 목록에 추가하려면 {email}로 문의해 주세요.",
|
||||
"updateRequested": "업데이트 요청됨",
|
||||
"useApiKeyTip": "팁: 일반 로그인을 사용할 수 없나요? Comfy API Key 옵션을 사용하세요.",
|
||||
"userNotAuthenticated": "사용자가 인증되지 않았습니다"
|
||||
},
|
||||
"userSelect": {
|
||||
|
||||
@@ -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": "Включено",
|
||||
@@ -385,6 +396,12 @@
|
||||
"inbox": "Входящие",
|
||||
"star": "Звезда"
|
||||
},
|
||||
"importModelDialog": {
|
||||
"copy": "Копировать",
|
||||
"move": "Переместить",
|
||||
"title": "Импорт модели",
|
||||
"type": "Тип"
|
||||
},
|
||||
"install": {
|
||||
"appDataLocationTooltip": "Директория данных приложения ComfyUI. Хранит:\n- Логи\n- Конфигурации сервера",
|
||||
"appPathLocationTooltip": "Директория активов приложения ComfyUI. Хранит код и активы ComfyUI",
|
||||
@@ -1343,7 +1360,6 @@
|
||||
"no3dSceneToExport": "Нет 3D сцены для экспорта",
|
||||
"noTemplatesToExport": "Нет шаблонов для экспорта",
|
||||
"nodeDefinitionsUpdated": "Определения узлов обновлены",
|
||||
"nothingSelected": "Ничего не выбрано",
|
||||
"nothingToGroup": "Нечего группировать",
|
||||
"nothingToQueue": "Нет заданий в очереди",
|
||||
"pendingTasksDeleted": "Ожидающие задачи удалены",
|
||||
@@ -1352,7 +1368,6 @@
|
||||
"unableToGetModelFilePath": "Не удалось получить путь к файлу модели",
|
||||
"unauthorizedDomain": "Ваш домен {domain} не авторизован для использования этого сервиса. Пожалуйста, свяжитесь с {email}, чтобы добавить ваш домен в белый список.",
|
||||
"updateRequested": "Запрошено обновление",
|
||||
"useApiKeyTip": "Совет: Нет доступа к обычному входу? Используйте опцию Comfy API Key.",
|
||||
"userNotAuthenticated": "Пользователь не аутентифицирован"
|
||||
},
|
||||
"userSelect": {
|
||||
|
||||
@@ -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": "已启用",
|
||||
@@ -385,6 +396,12 @@
|
||||
"inbox": "收件箱",
|
||||
"star": "星星"
|
||||
},
|
||||
"importModelDialog": {
|
||||
"copy": "复制",
|
||||
"move": "移动",
|
||||
"title": "导入模型",
|
||||
"type": "类型"
|
||||
},
|
||||
"install": {
|
||||
"appDataLocationTooltip": "ComfyUI 的应用数据目录。存储:\n- 日志\n- 服务器配置",
|
||||
"appPathLocationTooltip": "ComfyUI 的应用资产目录。存储 ComfyUI 代码和资产",
|
||||
@@ -1343,7 +1360,6 @@
|
||||
"no3dSceneToExport": "没有3D场景可以导出",
|
||||
"noTemplatesToExport": "没有模板可以导出",
|
||||
"nodeDefinitionsUpdated": "节点定义已更新",
|
||||
"nothingSelected": "未选择任何内容",
|
||||
"nothingToGroup": "没有可分组的内容",
|
||||
"nothingToQueue": "没有可加入队列的内容",
|
||||
"pendingTasksDeleted": "待处理任务已删除",
|
||||
@@ -1352,7 +1368,6 @@
|
||||
"unableToGetModelFilePath": "无法获取模型文件路径",
|
||||
"unauthorizedDomain": "您的域名 {domain} 未被授权使用此服务。请联系 {email} 将您的域名添加到白名单。",
|
||||
"updateRequested": "已请求更新",
|
||||
"useApiKeyTip": "提示:无法正常登录?请使用 Comfy API Key 选项。",
|
||||
"userNotAuthenticated": "用户未认证"
|
||||
},
|
||||
"userSelect": {
|
||||
|
||||
@@ -82,17 +82,6 @@ const zExecutionErrorWsMessage = zExecutionWsMessageBase.extend({
|
||||
current_outputs: z.any()
|
||||
})
|
||||
|
||||
const zProgressTextWsMessage = z.object({
|
||||
nodeId: zNodeId,
|
||||
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()
|
||||
@@ -125,10 +114,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({
|
||||
@@ -466,7 +451,7 @@ const zSettings = z.object({
|
||||
'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']),
|
||||
'pysssss.SnapToGrid': z.boolean(),
|
||||
/** VHS setting is used for queue video preview support. */
|
||||
'VHS.AdvancedPreviews': z.string(),
|
||||
'VHS.AdvancedPreviews': z.boolean(),
|
||||
/** Settings used for testing */
|
||||
'test.setting': z.any(),
|
||||
'main.sub.setting.name': z.any(),
|
||||
|
||||
@@ -216,7 +216,6 @@ const zComfyNode = z
|
||||
|
||||
const zGroup = z
|
||||
.object({
|
||||
id: z.number().optional(),
|
||||
title: z.string(),
|
||||
bounding: z.tuple([z.number(), z.number(), z.number(), z.number()]),
|
||||
color: z.string().optional(),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import type {
|
||||
DisplayComponentWsMessage,
|
||||
EmbeddingsResponse,
|
||||
ExecutedWsMessage,
|
||||
ExecutingWsMessage,
|
||||
@@ -15,7 +14,6 @@ import type {
|
||||
LogsRawResponse,
|
||||
LogsWsMessage,
|
||||
PendingTaskItem,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage,
|
||||
PromptResponse,
|
||||
RunningTaskItem,
|
||||
@@ -84,6 +82,7 @@ interface QueuePromptRequestBody {
|
||||
interface FrontendApiCalls {
|
||||
graphChanged: ComfyWorkflowJSON
|
||||
promptQueued: { number: number; batchCount: number }
|
||||
unhandledFileDrop: { file: File }
|
||||
graphCleared: never
|
||||
reconnecting: never
|
||||
reconnected: never
|
||||
@@ -103,8 +102,6 @@ interface BackendApiCalls {
|
||||
logs: LogsWsMessage
|
||||
/** Binary preview/progress data */
|
||||
b_preview: Blob
|
||||
progress_text: ProgressTextWsMessage
|
||||
display_component: DisplayComponentWsMessage
|
||||
}
|
||||
|
||||
/** Dictionary of all api calls */
|
||||
@@ -317,20 +314,23 @@ export class ComfyApi extends EventTarget {
|
||||
* Provides type safety for the contravariance issue with EventTarget (last checked TS 5.6).
|
||||
* @param type The type of event to emit
|
||||
* @param detail The detail property used for a custom event ({@link CustomEventInit.detail})
|
||||
* @param init The event config used for a custom event ({@link CustomEventInit})
|
||||
*/
|
||||
dispatchCustomEvent<T extends SimpleApiEvents>(type: T): boolean
|
||||
dispatchCustomEvent<T extends ComplexApiEvents>(
|
||||
type: T,
|
||||
detail: ApiEventTypes[T] | null
|
||||
detail: ApiEventTypes[T] | null,
|
||||
init?: EventInit
|
||||
): boolean
|
||||
dispatchCustomEvent<T extends keyof ApiEventTypes>(
|
||||
type: T,
|
||||
detail?: ApiEventTypes[T]
|
||||
detail?: ApiEventTypes[T],
|
||||
init?: EventInit
|
||||
): boolean {
|
||||
const event =
|
||||
detail === undefined
|
||||
? new CustomEvent(type)
|
||||
: new CustomEvent(type, { detail })
|
||||
? new CustomEvent(type, { ...init })
|
||||
: new CustomEvent(type, { detail, ...init })
|
||||
return super.dispatchEvent(event)
|
||||
}
|
||||
|
||||
@@ -403,21 +403,12 @@ export class ComfyApi extends EventTarget {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
const view = new DataView(event.data)
|
||||
const eventType = view.getUint32(0)
|
||||
const imageType = view.getUint32(4)
|
||||
const imageData = event.data.slice(8)
|
||||
|
||||
let imageMime
|
||||
switch (eventType) {
|
||||
case 3:
|
||||
const decoder = new TextDecoder()
|
||||
const data = event.data.slice(4)
|
||||
const nodeIdLength = view.getUint32(4)
|
||||
this.dispatchCustomEvent('progress_text', {
|
||||
nodeId: decoder.decode(data.slice(4, 4 + nodeIdLength)),
|
||||
text: decoder.decode(data.slice(4 + nodeIdLength))
|
||||
})
|
||||
break
|
||||
case 1:
|
||||
const imageType = view.getUint32(4)
|
||||
const imageData = event.data.slice(8)
|
||||
switch (imageType) {
|
||||
case 2:
|
||||
imageMime = 'image/png'
|
||||
|
||||
@@ -25,11 +25,7 @@ import {
|
||||
type ModelFile,
|
||||
type NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import {
|
||||
type ComfyNodeDef as ComfyNodeDefV1,
|
||||
isComboInputSpecV1,
|
||||
isComboInputSpecV2
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { getFromWebmFile } from '@/scripts/metadata/ebml'
|
||||
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
|
||||
import { getFromIsobmffFile } from '@/scripts/metadata/isobmff'
|
||||
@@ -143,20 +139,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 +688,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 +752,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 +1086,6 @@ export class ComfyApp {
|
||||
title: t('errorDialog.loadWorkflowTitle'),
|
||||
reportType: 'loadWorkflowError'
|
||||
})
|
||||
console.error(error)
|
||||
return
|
||||
}
|
||||
for (const node of this.graph.nodes) {
|
||||
@@ -1235,7 +1225,6 @@ export class ComfyApp {
|
||||
title: t('errorDialog.promptExecutionError'),
|
||||
reportType: 'promptExecutionError'
|
||||
})
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof PromptExecutionError) {
|
||||
executionStore.lastNodeErrors = error.response.node_errors ?? null
|
||||
@@ -1263,10 +1252,22 @@ export class ComfyApp {
|
||||
return !executionStore.lastNodeErrors
|
||||
}
|
||||
|
||||
showErrorOnFileLoad(file: File) {
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.fileLoadError', { fileName: file.name })
|
||||
onUnhandledFile(file: File) {
|
||||
// Fire custom event to allow other parts of the app to handle the file
|
||||
const unhandled = api.dispatchCustomEvent(
|
||||
'unhandledFileDrop',
|
||||
{ file },
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
)
|
||||
|
||||
if (unhandled) {
|
||||
// Nothing handled the event, so show the error dialog
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.fileLoadError', { fileName: file.name })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1302,7 +1303,7 @@ export class ComfyApp {
|
||||
this.graph.serialize() as unknown as ComfyWorkflowJSON
|
||||
)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (file.type === 'image/webp') {
|
||||
const pngInfo = await getWebpMetadata(file)
|
||||
@@ -1315,7 +1316,7 @@ export class ComfyApp {
|
||||
} else if (prompt) {
|
||||
this.loadApiJson(JSON.parse(prompt), fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (file.type === 'audio/mpeg') {
|
||||
const { workflow, prompt } = await getMp3Metadata(file)
|
||||
@@ -1324,7 +1325,7 @@ export class ComfyApp {
|
||||
} else if (prompt) {
|
||||
this.loadApiJson(prompt, fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (file.type === 'audio/ogg') {
|
||||
const { workflow, prompt } = await getOggMetadata(file)
|
||||
@@ -1333,7 +1334,7 @@ export class ComfyApp {
|
||||
} else if (prompt) {
|
||||
this.loadApiJson(prompt, fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (file.type === 'audio/flac' || file.type === 'audio/x-flac') {
|
||||
const pngInfo = await getFlacMetadata(file)
|
||||
@@ -1345,7 +1346,7 @@ export class ComfyApp {
|
||||
} else if (prompt) {
|
||||
this.loadApiJson(JSON.parse(prompt), fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (file.type === 'video/webm') {
|
||||
const webmInfo = await getFromWebmFile(file)
|
||||
@@ -1354,7 +1355,7 @@ export class ComfyApp {
|
||||
} else if (webmInfo.prompt) {
|
||||
this.loadApiJson(webmInfo.prompt, fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (
|
||||
file.type === 'video/mp4' ||
|
||||
@@ -1377,7 +1378,7 @@ export class ComfyApp {
|
||||
} else if (svgInfo.prompt) {
|
||||
this.loadApiJson(svgInfo.prompt, fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (
|
||||
file.type === 'model/gltf-binary' ||
|
||||
@@ -1389,7 +1390,7 @@ export class ComfyApp {
|
||||
} else if (gltfInfo.prompt) {
|
||||
this.loadApiJson(gltfInfo.prompt, fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (
|
||||
file.type === 'application/json' ||
|
||||
@@ -1420,7 +1421,7 @@ export class ComfyApp {
|
||||
const info = await getLatentMetadata(file)
|
||||
// TODO define schema to LatentMetadata
|
||||
// @ts-expect-error
|
||||
if (info.workflow) {
|
||||
if (info?.workflow) {
|
||||
await this.loadGraphData(
|
||||
// @ts-expect-error
|
||||
JSON.parse(info.workflow),
|
||||
@@ -1429,14 +1430,14 @@ export class ComfyApp {
|
||||
fileName
|
||||
)
|
||||
// @ts-expect-error
|
||||
} else if (info.prompt) {
|
||||
} else if (info?.prompt) {
|
||||
// @ts-expect-error
|
||||
this.loadApiJson(JSON.parse(info.prompt))
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1583,26 +1584,12 @@ export class ComfyApp {
|
||||
if (!def?.input) continue
|
||||
|
||||
if (node.widgets) {
|
||||
const nodeInputs = def.input
|
||||
for (const widget of node.widgets) {
|
||||
if (widget.type === 'combo') {
|
||||
let inputType: 'required' | 'optional' | undefined
|
||||
if (nodeInputs.required?.[widget.name] !== undefined) {
|
||||
inputType = 'required'
|
||||
} else if (nodeInputs.optional?.[widget.name] !== undefined) {
|
||||
inputType = 'optional'
|
||||
}
|
||||
if (inputType !== undefined) {
|
||||
// Get the input spec associated with the widget
|
||||
const inputSpec = nodeInputs[inputType]?.[widget.name]
|
||||
if (inputSpec) {
|
||||
// Refresh the combo widget's options with the values from the input spec
|
||||
if (isComboInputSpecV2(inputSpec)) {
|
||||
widget.options.values = inputSpec[1]?.options
|
||||
} else if (isComboInputSpecV1(inputSpec)) {
|
||||
widget.options.values = inputSpec[0]
|
||||
}
|
||||
}
|
||||
if (def['input'].required?.[widget.name] !== undefined) {
|
||||
widget.options.values = def['input'].required[widget.name][0]
|
||||
} else if (def['input'].optional?.[widget.name] !== undefined) {
|
||||
widget.options.values = def['input'].optional[widget.name][0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
import { api } from './api'
|
||||
import type { ComfyApp } from './app'
|
||||
import { app } from './app'
|
||||
|
||||
function clone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
@@ -37,6 +36,11 @@ export class ChangeTracker {
|
||||
ds?: { scale: number; offset: [number, number] }
|
||||
nodeOutputs?: Record<string, any>
|
||||
|
||||
static app?: ComfyApp
|
||||
get app(): ComfyApp {
|
||||
return ChangeTracker.app!
|
||||
}
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The workflow that this change tracker is tracking
|
||||
@@ -64,18 +68,18 @@ export class ChangeTracker {
|
||||
|
||||
store() {
|
||||
this.ds = {
|
||||
scale: app.canvas.ds.scale,
|
||||
offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]]
|
||||
scale: this.app.canvas.ds.scale,
|
||||
offset: [this.app.canvas.ds.offset[0], this.app.canvas.ds.offset[1]]
|
||||
}
|
||||
}
|
||||
|
||||
restore() {
|
||||
if (this.ds) {
|
||||
app.canvas.ds.scale = this.ds.scale
|
||||
app.canvas.ds.offset = this.ds.offset
|
||||
this.app.canvas.ds.scale = this.ds.scale
|
||||
this.app.canvas.ds.offset = this.ds.offset
|
||||
}
|
||||
if (this.nodeOutputs) {
|
||||
app.nodeOutputs = this.nodeOutputs
|
||||
this.app.nodeOutputs = this.nodeOutputs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +105,10 @@ export class ChangeTracker {
|
||||
}
|
||||
|
||||
checkState() {
|
||||
if (!app.graph || this.changeCount) return
|
||||
const currentState = clone(app.graph.serialize()) as ComfyWorkflowJSON
|
||||
if (!this.app.graph || this.changeCount) return
|
||||
// @ts-expect-error zod type issue on ComfyWorkflowJSON. ComfyWorkflowJSON
|
||||
// is stricter than LiteGraph's serialisation schema.
|
||||
const currentState = clone(this.app.graph.serialize()) as ComfyWorkflowJSON
|
||||
if (!this.activeState) {
|
||||
this.activeState = currentState
|
||||
return
|
||||
@@ -126,7 +132,7 @@ export class ChangeTracker {
|
||||
target.push(this.activeState)
|
||||
this.restoringState = true
|
||||
try {
|
||||
await app.loadGraphData(prevState, false, false, this.workflow, {
|
||||
await this.app.loadGraphData(prevState, false, false, this.workflow, {
|
||||
showMissingModelsDialog: false,
|
||||
showMissingNodesDialog: false,
|
||||
checkForRerouteMigration: false
|
||||
@@ -183,11 +189,13 @@ export class ChangeTracker {
|
||||
}
|
||||
}
|
||||
|
||||
static init() {
|
||||
static init(app: ComfyApp) {
|
||||
const getCurrentChangeTracker = () =>
|
||||
useWorkflowStore().activeWorkflow?.changeTracker
|
||||
const checkState = () => getCurrentChangeTracker()?.checkState()
|
||||
|
||||
ChangeTracker.app = app
|
||||
|
||||
let keyIgnored = false
|
||||
window.addEventListener(
|
||||
'keydown',
|
||||
@@ -229,7 +237,7 @@ export class ChangeTracker {
|
||||
if (await changeTracker.undoRedo(e)) return
|
||||
|
||||
// If our active element is some type of input then handle changes after they're done
|
||||
if (ChangeTracker.bindInput(bindInputEl)) return
|
||||
if (ChangeTracker.bindInput(app, bindInputEl)) return
|
||||
logger.debug('checkState on keydown')
|
||||
changeTracker.checkState()
|
||||
})
|
||||
@@ -331,7 +339,7 @@ export class ChangeTracker {
|
||||
})
|
||||
}
|
||||
|
||||
static bindInput(activeEl: Element | null): boolean {
|
||||
static bindInput(_app: ComfyApp, activeEl: Element | null): boolean {
|
||||
if (
|
||||
!activeEl ||
|
||||
activeEl.tagName === 'CANVAS' ||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -143,12 +143,17 @@ export function getLatentMetadata(file) {
|
||||
const dataView = new DataView(safetensorsData.buffer)
|
||||
let header_size = dataView.getUint32(0, true)
|
||||
let offset = 8
|
||||
let header = JSON.parse(
|
||||
new TextDecoder().decode(
|
||||
safetensorsData.slice(offset, offset + header_size)
|
||||
try {
|
||||
let header = JSON.parse(
|
||||
new TextDecoder().decode(
|
||||
safetensorsData.slice(offset, offset + header_size)
|
||||
)
|
||||
)
|
||||
)
|
||||
r(header.__metadata__)
|
||||
r(header.__metadata__)
|
||||
} catch (e) {
|
||||
// Invalid header
|
||||
r(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
var slice = file.slice(0, 1024 * 1024 * 4)
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import QuickLRU from '@alloc/quick-lru'
|
||||
import type {
|
||||
BaseSearchParamsWithoutQuery,
|
||||
Hit,
|
||||
SearchQuery,
|
||||
SearchResponse
|
||||
} from 'algoliasearch/dist/lite/browser'
|
||||
import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { paramsToCacheKey } from '@/utils/formatUtil'
|
||||
|
||||
const DEFAULT_MAX_CACHE_SIZE = 64
|
||||
const DEFAULT_MIN_CHARS_FOR_SUGGESTIONS = 2
|
||||
|
||||
type SafeNestedProperty<
|
||||
T,
|
||||
@@ -21,10 +15,6 @@ type SafeNestedProperty<
|
||||
> = T[K1] extends undefined | null ? undefined : NonNullable<T[K1]>[K2]
|
||||
|
||||
type RegistryNodePack = components['schemas']['Node']
|
||||
type SearchPacksResult = {
|
||||
nodePacks: Hit<AlgoliaNodePack>[]
|
||||
querySuggestions: Hit<NodesIndexSuggestion>[]
|
||||
}
|
||||
|
||||
export interface AlgoliaNodePack {
|
||||
objectID: RegistryNodePack['id']
|
||||
@@ -101,33 +91,8 @@ type SearchNodePacksParams = BaseSearchParamsWithoutQuery & {
|
||||
restrictSearchableAttributes: SearchAttribute[]
|
||||
}
|
||||
|
||||
interface AlgoliaSearchServiceOptions {
|
||||
/**
|
||||
* Maximum number of search results to store in the cache.
|
||||
* The cache is automatically cleared when the component is unmounted.
|
||||
* @default 64
|
||||
*/
|
||||
maxCacheSize?: number
|
||||
/**
|
||||
* Minimum number of characters for suggestions. An additional query
|
||||
* will be made to the suggestions/completions index for queries that
|
||||
* are this length or longer.
|
||||
* @default 3
|
||||
*/
|
||||
minCharsForSuggestions?: number
|
||||
}
|
||||
|
||||
export const useAlgoliaSearchService = (
|
||||
options: AlgoliaSearchServiceOptions = {}
|
||||
) => {
|
||||
const {
|
||||
maxCacheSize = DEFAULT_MAX_CACHE_SIZE,
|
||||
minCharsForSuggestions = DEFAULT_MIN_CHARS_FOR_SUGGESTIONS
|
||||
} = options
|
||||
export const useAlgoliaSearchService = () => {
|
||||
const searchClient = algoliasearch(__ALGOLIA_APP_ID__, __ALGOLIA_API_KEY__)
|
||||
const searchPacksCache = new QuickLRU<string, SearchPacksResult>({
|
||||
maxSize: maxCacheSize
|
||||
})
|
||||
|
||||
const toRegistryLatestVersion = (
|
||||
algoliaNode: AlgoliaNodePack
|
||||
@@ -176,39 +141,34 @@ export const useAlgoliaSearchService = (
|
||||
const searchPacks = async (
|
||||
query: string,
|
||||
params: SearchNodePacksParams
|
||||
): Promise<SearchPacksResult> => {
|
||||
): Promise<{
|
||||
nodePacks: Hit<AlgoliaNodePack>[]
|
||||
querySuggestions: Hit<NodesIndexSuggestion>[]
|
||||
}> => {
|
||||
const { pageSize, pageNumber } = params
|
||||
const rest = omit(params, ['pageSize', 'pageNumber'])
|
||||
|
||||
const requests: SearchQuery[] = [
|
||||
{
|
||||
query,
|
||||
indexName: 'nodes_index',
|
||||
attributesToRetrieve: RETRIEVE_ATTRIBUTES,
|
||||
...rest,
|
||||
hitsPerPage: pageSize,
|
||||
page: pageNumber
|
||||
}
|
||||
]
|
||||
|
||||
const shouldQuerySuggestions = query.length >= minCharsForSuggestions
|
||||
|
||||
// If the query is long enough, also query the suggestions index
|
||||
if (shouldQuerySuggestions) {
|
||||
requests.push({
|
||||
indexName: 'nodes_index_query_suggestions',
|
||||
query
|
||||
})
|
||||
}
|
||||
|
||||
const { results } = await searchClient.search<
|
||||
AlgoliaNodePack | NodesIndexSuggestion
|
||||
>({
|
||||
requests,
|
||||
requests: [
|
||||
{
|
||||
query,
|
||||
indexName: 'nodes_index',
|
||||
attributesToRetrieve: RETRIEVE_ATTRIBUTES,
|
||||
...rest,
|
||||
hitsPerPage: pageSize,
|
||||
page: pageNumber
|
||||
},
|
||||
{
|
||||
indexName: 'nodes_index_query_suggestions',
|
||||
query
|
||||
}
|
||||
],
|
||||
strategy: 'none'
|
||||
})
|
||||
|
||||
const [nodePacks, querySuggestions = { hits: [] }] = results as [
|
||||
const [nodePacks, querySuggestions] = results as [
|
||||
SearchResponse<AlgoliaNodePack>,
|
||||
SearchResponse<NodesIndexSuggestion>
|
||||
]
|
||||
@@ -219,27 +179,8 @@ export const useAlgoliaSearchService = (
|
||||
}
|
||||
}
|
||||
|
||||
const searchPacksCached = async (
|
||||
query: string,
|
||||
params: SearchNodePacksParams
|
||||
): Promise<SearchPacksResult> => {
|
||||
const cacheKey = paramsToCacheKey({ query, ...params })
|
||||
const cachedResult = searchPacksCache.get(cacheKey)
|
||||
if (cachedResult !== undefined) return cachedResult
|
||||
|
||||
const result = await searchPacks(query, params)
|
||||
searchPacksCache.set(cacheKey, result)
|
||||
return result
|
||||
}
|
||||
|
||||
const clearSearchPacksCache = () => {
|
||||
searchPacksCache.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
searchPacks,
|
||||
searchPacksCached,
|
||||
toRegistryPack,
|
||||
clearSearchPacksCache
|
||||
toRegistryPack
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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'
|
||||
import ImportModelDialogContent from '@/components/dialog/content/ImportModelDialogContent.vue'
|
||||
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
|
||||
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
|
||||
import ManagerProgressDialogContent from '@/components/dialog/content/ManagerProgressDialogContent.vue'
|
||||
@@ -379,6 +381,42 @@ 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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showImportModelDialog(
|
||||
props: InstanceType<typeof ImportModelDialogContent>['$props']
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-import-model',
|
||||
component: ImportModelDialogContent,
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
showLoadWorkflowWarning,
|
||||
showMissingModelsWarning,
|
||||
@@ -394,6 +432,8 @@ export const useDialogService = () => {
|
||||
showSignInDialog,
|
||||
showTopUpCreditsDialog,
|
||||
showUpdatePasswordDialog,
|
||||
showApiNodesNewsDialog,
|
||||
showImportModelDialog,
|
||||
prompt,
|
||||
confirm
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FirebaseError } from 'firebase/app'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
@@ -12,13 +11,11 @@ import { usdToMicros } from '@/utils/formatUtil'
|
||||
* All actions are wrapped with error handling.
|
||||
* @returns {Object} - Object containing all Firebase Auth actions
|
||||
*/
|
||||
export const useFirebaseAuthActions = () => {
|
||||
export const useFirebaseAuthService = () => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const toastStore = useToastStore()
|
||||
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
|
||||
|
||||
const accessError = ref(false)
|
||||
|
||||
const reportError = (error: unknown) => {
|
||||
// Ref: https://firebase.google.com/docs/auth/admin/errors
|
||||
if (
|
||||
@@ -29,7 +26,6 @@ export const useFirebaseAuthActions = () => {
|
||||
'auth/unauthorized-continue-uri'
|
||||
].includes(error.code)
|
||||
) {
|
||||
accessError.value = true
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
@@ -145,7 +141,6 @@ export const useFirebaseAuthActions = () => {
|
||||
signInWithGithub,
|
||||
signInWithEmail,
|
||||
signUpWithEmail,
|
||||
updatePassword,
|
||||
accessError
|
||||
updatePassword
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@
|
||||
* Stores all DOM widgets that are used in the canvas.
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { type Raw, markRaw, ref } from 'vue'
|
||||
import { markRaw, ref } from 'vue'
|
||||
|
||||
import type { PositionConfig } from '@/composables/element/useAbsolutePosition'
|
||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
|
||||
export interface DomWidgetState extends PositionConfig {
|
||||
// Raw widget instance
|
||||
widget: Raw<BaseDOMWidget<object | string>>
|
||||
widget: BaseDOMWidget<object | string>
|
||||
visible: boolean
|
||||
readonly: boolean
|
||||
zIndex: number
|
||||
@@ -23,7 +23,7 @@ export const useDomWidgetStore = defineStore('domWidget', () => {
|
||||
widget: BaseDOMWidget<V>
|
||||
) => {
|
||||
widgetStates.value.set(widget.id, {
|
||||
widget: markRaw(widget) as unknown as Raw<BaseDOMWidget<object | string>>,
|
||||
widget: markRaw(widget) as unknown as BaseDOMWidget<object | string>,
|
||||
visible: true,
|
||||
readonly: false,
|
||||
zIndex: 0,
|
||||
|
||||