From 775f536d3071be3245fc7b6a25ccee6e708cebc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20S=C3=B6derqvist?= Date: Thu, 15 Aug 2024 16:45:40 +0200 Subject: [PATCH] Add more Zod types to api.ts (#440) --- src/components/graph/NodeTooltip.vue | 2 +- src/components/searchbox/NodeSearchBox.vue | 2 +- .../searchbox/NodeSearchBoxPopover.vue | 6 +- src/scripts/api.ts | 94 +++++++++++-------- src/scripts/app.ts | 2 +- src/scripts/ui/components/button.ts | 3 +- src/scripts/ui/settings.ts | 19 ++-- src/stores/settingStore.ts | 5 +- src/types/apiTypes.ts | 81 ++++++++++++++++ src/types/colorPalette.ts | 2 +- src/types/settingTypes.ts | 6 +- 11 files changed, 165 insertions(+), 57 deletions(-) diff --git a/src/components/graph/NodeTooltip.vue b/src/components/graph/NodeTooltip.vue index 88f0e558a..656bdae19 100644 --- a/src/components/graph/NodeTooltip.vue +++ b/src/components/graph/NodeTooltip.vue @@ -130,7 +130,7 @@ const onMouseMove = (e: MouseEvent) => { } watch( - () => settingStore.get('Comfy.EnableTooltips'), + () => settingStore.get('Comfy.EnableTooltips'), (enabled) => { if (enabled) { window.addEventListener('mousemove', onMouseMove) diff --git a/src/components/searchbox/NodeSearchBox.vue b/src/components/searchbox/NodeSearchBox.vue index 4325c466e..2877b94a0 100644 --- a/src/components/searchbox/NodeSearchBox.vue +++ b/src/components/searchbox/NodeSearchBox.vue @@ -68,7 +68,7 @@ import { useSettingStore } from '@/stores/settingStore' const settingStore = useSettingStore() const enableNodePreview = computed(() => - settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview') + settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview') ) const props = defineProps({ diff --git a/src/components/searchbox/NodeSearchBoxPopover.vue b/src/components/searchbox/NodeSearchBoxPopover.vue index 95187dc6f..7ae1928a3 100644 --- a/src/components/searchbox/NodeSearchBoxPopover.vue +++ b/src/components/searchbox/NodeSearchBoxPopover.vue @@ -95,10 +95,8 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => { }, 100) } -const linkReleaseTriggerMode = computed(() => { - return settingStore.get( - 'Comfy.NodeSearchBoxImpl.LinkReleaseTrigger' - ) +const linkReleaseTriggerMode = computed(() => { + return settingStore.get('Comfy.NodeSearchBoxImpl.LinkReleaseTrigger') }) const canvasEventHandler = (e: LiteGraphCanvasEvent) => { diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 69aa309ab..1c5238e0b 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -4,7 +4,13 @@ import { PendingTaskItem, RunningTaskItem, ComfyNodeDef, - validateComfyNodeDef + validateComfyNodeDef, + EmbeddingsResponse, + ExtensionsResponse, + PromptResponse, + SystemStats, + User, + Settings } from '@/types/apiTypes' interface QueuePromptRequestBody { @@ -47,7 +53,7 @@ class ComfyApi extends EventTarget { return this.api_base + route } - fetchApi(route, options?) { + fetchApi(route: string, options?: RequestInit) { if (!options) { options = {} } @@ -55,13 +61,17 @@ class ComfyApi extends EventTarget { options.headers = {} } if (!options.cache) { - options.cache = 'no-cache' + options.cache = 'no-cache' } options.headers['Comfy-User'] = this.user return fetch(this.apiURL(route), options) } - addEventListener(type, callback, options?) { + addEventListener( + type: string, + callback: any, + options?: AddEventListenerOptions + ) { super.addEventListener(type, callback, options) this.#registered.add(type) } @@ -85,7 +95,7 @@ class ComfyApi extends EventTarget { * Creates and connects a WebSocket for realtime updates * @param {boolean} isReconnect If the socket is connection is a reconnect attempt */ - #createSocket(isReconnect?) { + #createSocket(isReconnect?: boolean) { if (this.socket) { return } @@ -232,18 +242,16 @@ class ComfyApi extends EventTarget { /** * Gets a list of extension urls - * @returns An array of script urls to import */ - async getExtensions() { + async getExtensions(): Promise { const resp = await this.fetchApi('/extensions', { cache: 'no-store' }) return await resp.json() } /** * Gets a list of embedding names - * @returns An array of script urls to import */ - async getEmbeddings() { + async getEmbeddings(): Promise { const resp = await this.fetchApi('/embeddings', { cache: 'no-store' }) return await resp.json() } @@ -278,7 +286,10 @@ class ComfyApi extends EventTarget { * @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue * @param {object} prompt The prompt data to queue */ - async queuePrompt(number: number, { output, workflow }) { + async queuePrompt( + number: number, + { output, workflow } + ): Promise { const body: QueuePromptRequestBody = { client_id: this.clientId, prompt: output, @@ -313,7 +324,7 @@ class ComfyApi extends EventTarget { * @param {string} type The type of items to load, queue or history * @returns The items of the specified type grouped by their status */ - async getItems(type) { + async getItems(type: 'queue' | 'history') { if (type === 'queue') { return this.getQueue() } @@ -358,10 +369,12 @@ class ComfyApi extends EventTarget { ): Promise<{ History: HistoryTaskItem[] }> { try { const res = await this.fetchApi(`/history?max_items=${max_items}`) + const json: Promise = await res.json() return { - History: Object.values(await res.json()).map( - (item: HistoryTaskItem) => ({ ...item, taskType: 'History' }) - ) + History: Object.values(json).map((item) => ({ + ...item, + taskType: 'History' + })) } } catch (error) { console.error(error) @@ -373,7 +386,7 @@ class ComfyApi extends EventTarget { * Gets system & device stats * @returns System stats such as python version, OS, per device info */ - async getSystemStats() { + async getSystemStats(): Promise { const res = await this.fetchApi('/system_stats') return await res.json() } @@ -383,7 +396,7 @@ class ComfyApi extends EventTarget { * @param {*} type The endpoint to post to * @param {*} body Optional POST data */ - async #postItem(type, body) { + async #postItem(type: string, body: any) { try { await this.fetchApi('/' + type, { method: 'POST', @@ -402,7 +415,7 @@ class ComfyApi extends EventTarget { * @param {string} type The type of item to delete, queue or history * @param {number} id The id of the item to delete */ - async deleteItem(type, id) { + async deleteItem(type: string, id: string) { await this.#postItem(type, { delete: [id] }) } @@ -410,7 +423,7 @@ class ComfyApi extends EventTarget { * Clears the specified list * @param {string} type The type of list to clear, queue or history */ - async clearItems(type) { + async clearItems(type: string) { await this.#postItem(type, { clear: true }) } @@ -423,9 +436,8 @@ class ComfyApi extends EventTarget { /** * Gets user configuration data and where data should be stored - * @returns { Promise<{ storage: "server" | "browser", users?: Promise, migrated?: boolean }> } */ - async getUserConfig() { + async getUserConfig(): Promise { return (await this.fetchApi('/users')).json() } @@ -434,7 +446,7 @@ class ComfyApi extends EventTarget { * @param { string } username * @returns The fetch response */ - createUser(username) { + createUser(username: string) { return this.fetchApi('/users', { method: 'POST', headers: { @@ -448,7 +460,7 @@ class ComfyApi extends EventTarget { * Gets all setting values for the current user * @returns { Promise } A dictionary of id -> value */ - async getSettings() { + async getSettings(): Promise { return (await this.fetchApi('/settings')).json() } @@ -457,16 +469,14 @@ class ComfyApi extends EventTarget { * @param { string } id The id of the setting to fetch * @returns { Promise } The setting value */ - async getSetting(id) { + async getSetting(id: keyof Settings): Promise { return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json() } /** * Stores a dictionary of settings for the current user - * @param { Record } settings Dictionary of setting id -> value to save - * @returns { Promise } */ - async storeSettings(settings) { + async storeSettings(settings: Settings) { return this.fetchApi(`/settings`, { method: 'POST', body: JSON.stringify(settings) @@ -475,11 +485,8 @@ class ComfyApi extends EventTarget { /** * Stores a setting for the current user - * @param { string } id The id of the setting to update - * @param { unknown } value The value of the setting - * @returns { Promise } */ - async storeSetting(id, value) { + async storeSetting(id: keyof Settings, value: Settings[keyof Settings]) { return this.fetchApi(`/settings/${encodeURIComponent(id)}`, { method: 'POST', body: JSON.stringify(value) @@ -488,11 +495,8 @@ class ComfyApi extends EventTarget { /** * Gets a user data file for the current user - * @param { string } file The name of the userdata file to load - * @param { RequestInit } [options] - * @returns { Promise } The fetch response object */ - async getUserData(file, options?) { + async getUserData(file: string, options?: RequestInit) { return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options) } @@ -505,7 +509,7 @@ class ComfyApi extends EventTarget { */ async storeUserData( file: string, - data: unknown, + data: any, options: RequestInit & { overwrite?: boolean stringify?: boolean @@ -533,7 +537,7 @@ class ComfyApi extends EventTarget { * Deletes a user data file for the current user * @param { string } file The name of the userdata file to delete */ - async deleteUserData(file) { + async deleteUserData(file: string) { const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, { method: 'DELETE' }) @@ -549,7 +553,11 @@ class ComfyApi extends EventTarget { * @param { string } source The userdata file to move * @param { string } dest The destination for the file */ - async moveUserData(source, dest, options = { overwrite: false }) { + async moveUserData( + source: string, + dest: string, + options = { overwrite: false } + ) { const resp = await this.fetchApi( `/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`, { @@ -565,7 +573,7 @@ class ComfyApi extends EventTarget { * @param { string } dir The directory in which to list files * @param { boolean } [recurse] If the listing should be recursive * @param { true } [split] If the paths should be split based on the os path separator - * @returns { Promise> } The list of split file paths in the format [fullPath, ...splitPath] + * @returns { Promise } The list of split file paths in the format [fullPath, ...splitPath] */ /** * @overload @@ -575,6 +583,16 @@ class ComfyApi extends EventTarget { * @param { false | undefined } [split] If the paths should be split based on the os path separator * @returns { Promise } The list of files */ + async listUserData( + dir: string, + recurse: true, + split?: boolean + ): Promise + async listUserData( + dir: string, + recurse: false, + split?: boolean + ): Promise async listUserData(dir, recurse, split) { const resp = await this.fetchApi( `/userdata?${new URLSearchParams({ diff --git a/src/scripts/app.ts b/src/scripts/app.ts index eed287170..de78a7319 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -2186,7 +2186,7 @@ export class ComfyApp { if ( this.vueAppReady && - useSettingStore().get('Comfy.Validation.Workflows') + useSettingStore().get('Comfy.Validation.Workflows') ) { graphData = await validateComfyWorkflow(graphData, /* onError=*/ alert) if (!graphData) return diff --git a/src/scripts/ui/components/button.ts b/src/scripts/ui/components/button.ts index 18dca0ac0..52158234d 100644 --- a/src/scripts/ui/components/button.ts +++ b/src/scripts/ui/components/button.ts @@ -4,6 +4,7 @@ import { prop } from '../../utils' import type { ComfyPopup } from './popup' import type { ComfyComponent } from '.' import type { ComfyApp } from '@/scripts/app' +import { Settings } from '@/types/apiTypes' type ComfyButtonProps = { icon?: string @@ -14,7 +15,7 @@ type ComfyButtonProps = { enabled?: boolean action?: (e: Event, btn: ComfyButton) => void classList?: ClassList - visibilitySetting?: { id: string; showValue: any } + visibilitySetting?: { id: keyof Settings; showValue: boolean } app?: ComfyApp } diff --git a/src/scripts/ui/settings.ts b/src/scripts/ui/settings.ts index a728bafd1..ef8a1411c 100644 --- a/src/scripts/ui/settings.ts +++ b/src/scripts/ui/settings.ts @@ -4,6 +4,7 @@ import { ComfyDialog } from './dialog' import type { ComfyApp } from '../app' import type { Setting, SettingParams } from '@/types/settingTypes' import { useSettingStore } from '@/stores/settingStore' +import { Settings } from '@/types/apiTypes' export class ComfySettingsDialog extends ComfyDialog { app: ComfyApp @@ -98,16 +99,19 @@ export class ComfySettingsDialog extends ComfyDialog { return id } - getSettingValue(id: string, defaultValue?: T): T { + getSettingValue( + id: K, + defaultValue?: Settings[K] + ): Settings[K] { let value = this.settingsValues[this.getId(id)] if (value != null) { if (this.app.storageLocation === 'browser') { try { - value = JSON.parse(value) as T + value = JSON.parse(value) } catch (error) {} } } - return value ?? defaultValue + return (value ?? defaultValue) as Settings[K] } getSettingDefaultValue(id: string) { @@ -115,7 +119,10 @@ export class ComfySettingsDialog extends ComfyDialog { return param?.defaultValue } - async setSettingValueAsync(id: string, value: any) { + async setSettingValueAsync( + id: K, + value: Settings[K] + ) { const json = JSON.stringify(value) localStorage['Comfy.Settings.' + id] = json // backwards compatibility for extensions keep setting in storage @@ -130,14 +137,14 @@ export class ComfySettingsDialog extends ComfyDialog { await api.storeSetting(id, value) } - setSettingValue(id: string, value: any) { + setSettingValue(id: K, value: Settings[K]) { this.setSettingValueAsync(id, value).catch((err) => { alert(`Error saving setting '${id}'`) console.error(err) }) } - refreshSetting(id: string) { + refreshSetting(id: keyof Settings) { const value = this.getSettingValue(id) this.settingsLookup[id].onChange?.(value) this.#dispatchChange(id, value) diff --git a/src/stores/settingStore.ts b/src/stores/settingStore.ts index 46e6b161f..95fd2b239 100644 --- a/src/stores/settingStore.ts +++ b/src/stores/settingStore.ts @@ -9,6 +9,7 @@ import { app } from '@/scripts/app' import { ComfySettingsDialog } from '@/scripts/ui/settings' +import { Settings } from '@/types/apiTypes' import { LinkReleaseTriggerMode } from '@/types/searchBoxTypes' import { SettingParams } from '@/types/settingTypes' import { buildTree } from '@/utils/treeUtil' @@ -107,12 +108,12 @@ export const useSettingStore = defineStore('setting', { }) }, - set(key: string, value: any) { + set(key: K, value: Settings[K]) { this.settingValues[key] = value app.ui.settings.setSettingValue(key, value) }, - get(key: string): T { + get(key: K): Settings[K] { return ( this.settingValues[key] ?? app.ui.settings.getSettingDefaultValue(key) ) diff --git a/src/types/apiTypes.ts b/src/types/apiTypes.ts index fc8035630..f1318b5eb 100644 --- a/src/types/apiTypes.ts +++ b/src/types/apiTypes.ts @@ -1,6 +1,7 @@ import { ZodType, z } from 'zod' import { zComfyWorkflow, zNodeId } from './comfyWorkflow' import { fromZodError } from 'zod-validation-error' +import { colorPalettesSchema } from './colorPalette' const zNodeType = z.string() const zQueueIndex = z.number() @@ -356,3 +357,83 @@ export function validateComfyNodeDef( } return result.data } + +const zEmbeddingsResponse = z.array(z.string()) +const zExtensionsResponse = z.array(z.string()) +const zPromptResponse = z.object({ + node_errors: z.array(z.string()).optional(), + prompt_id: z.string().optional(), + exec_info: z + .object({ + queue_remaining: z.number().optional() + }) + .optional() +}) +export const zSystemStats = z.object({ + system: z.object({ + os: z.string(), + python_version: z.string(), + embedded_python: z.boolean() + }), + devices: z.array( + z.object({ + name: z.string(), + type: z.string(), + index: z.number().optional(), + vram_total: z.number(), + vram_free: z.number(), + torch_vram_total: z.number(), + torch_vram_free: z.number() + }) + ) +}) +const zUser = z.object({ + storage: z.enum(['server', 'browser']), + migrated: z.boolean(), + users: z.record(z.string(), z.unknown()) +}) +const zUserData = z.array(z.array(z.string(), z.string())) +const zSettings = z.record(z.any()).and( + z + .object({ + 'Comfy.ColorPalette': z.string(), + 'Comfy.CustomColorPalettes': colorPalettesSchema, + 'Comfy.ConfirmClear': z.boolean(), + 'Comfy.DevMode': z.boolean(), + 'Comfy.DisableFloatRounding': z.boolean(), + 'Comfy.DisableSliders': z.boolean(), + 'Comfy.DOMClippingEnabled': z.boolean(), + 'Comfy.EditAttention.Delta': z.number(), + 'Comfy.EnableTooltips': z.boolean(), + 'Comfy.EnableWorkflowViewRestore': z.boolean(), + 'Comfy.FloatRoundingPrecision': z.number(), + 'Comfy.InvertMenuScrolling': z.boolean(), + 'Comfy.Logging.Enabled': z.boolean(), + 'Comfy.NodeInputConversionSubmenus': z.boolean(), + 'Comfy.NodeSearchBoxImpl.LinkReleaseTrigger': z.enum([ + 'always', + 'hold shift', + 'NOT hold shift' + ]), + 'Comfy.NodeSearchBoxImpl.NodePreview': z.boolean(), + 'Comfy.NodeSearchBoxImpl': z.enum(['default', 'simple']), + 'Comfy.NodeSuggestions.number': z.number(), + 'Comfy.PreviewFormat': z.string(), + 'Comfy.PromptFilename': z.boolean(), + 'Comfy.Sidebar.Location': z.enum(['left', 'right']), + 'Comfy.Sidebar.Size': z.number(), + 'Comfy.SwitchUser': z.any(), + 'Comfy.SnapToGrid.GridSize': z.number(), + 'Comfy.UseNewMenu': z.any(), + 'Comfy.Validation.Workflows': z.boolean() + }) + .optional() +) + +export type EmbeddingsResponse = z.infer +export type ExtensionsResponse = z.infer +export type PromptResponse = z.infer +export type Settings = z.infer +export type SystemStats = z.infer +export type User = z.infer +export type UserData = z.infer diff --git a/src/types/colorPalette.ts b/src/types/colorPalette.ts index 36ad0dbd2..2d239c912 100644 --- a/src/types/colorPalette.ts +++ b/src/types/colorPalette.ts @@ -88,7 +88,7 @@ const paletteSchema = z.object({ colors: colorsSchema }) -const colorPalettesSchema = z.record(paletteSchema) +export const colorPalettesSchema = z.record(paletteSchema) export type Colors = z.infer export type Palette = z.infer diff --git a/src/types/settingTypes.ts b/src/types/settingTypes.ts index 3daf5d7c5..7f40c7211 100644 --- a/src/types/settingTypes.ts +++ b/src/types/settingTypes.ts @@ -1,3 +1,5 @@ +import { Settings } from './apiTypes' + export type StorageLocation = 'browser' | 'server' export type SettingInputType = @@ -21,14 +23,14 @@ export interface SettingOption { } export interface Setting { - id: string + id: keyof Settings onChange?: (value: any, oldValue?: any) => void name: string render: () => HTMLElement } export interface SettingParams { - id: string + id: keyof Settings name: string type: SettingInputType | SettingCustomRenderer defaultValue: any