refactor(schema): flatten CHART/GALLERIA V2 specs + add input type guards

Extend the FE-800 flattening (COLOR/TEXTAREA/MARKDOWN) to the two
remaining widgets that carried a nested `options` bag, and add the
missing type guards so every widget composable narrows through a
discriminated check instead of a bare type assertion.

- nodeDefSchemaV2:
  - zChartInputSpec: hoist nested options.{type,data} to top-level
    `chartType` + `data` (rename avoids collision with the discriminator
    `type: 'CHART'`).
  - zGalleriaInputSpec: hoist nested options.images to top-level
    `images`.
  - Add `isColorInputSpec`, `isTextareaInputSpec`, `isGalleriaInputSpec`
    — matches the existing INT/FLOAT/BOOLEAN/STRING/COMBO/CHART guards.
  - Drop unused public exports for ChartInputSpec/GalleriaInputSpec/
    TextareaInputSpec; they are now only referenced by their own
    guards, matching the IntInputSpec/FloatInputSpec pattern.
- useColorWidget, useTextareaWidget, useGalleriaWidget, useChartWidget:
  gate on the type guard up front instead of casting `as XInputSpec`;
  read all spec fields at the top level.
- WidgetChart: replace `NonNullable<ChartInputSpec['options']>` type
  derivation with an explicit local ChartWidgetOptions; the schema no
  longer surfaces an `options` field.

Amp-Thread-ID: https://ampcode.com/threads/T-019e5168-0c71-7667-b1df-683cd9f8acf7
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
DrJKL
2026-05-22 14:47:00 -07:00
parent 604656fb28
commit b6111cd25f
7 changed files with 60 additions and 59 deletions

View File

@@ -5,14 +5,14 @@ import { defineComponent, nextTick, ref } from 'vue'
import { createI18n } from 'vue-i18n'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import type { ChartInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import WidgetChart from './WidgetChart.vue'
import { createMockWidget } from './widgetTestUtils'
type ChartWidgetOptions = NonNullable<ChartInputSpec['options']> &
IWidgetOptions
type ChartWidgetOptions = IWidgetOptions & {
type?: 'bar' | 'line'
}
const i18n = createI18n({
legacy: false,

View File

@@ -17,11 +17,11 @@ import Chart from 'primevue/chart'
import { computed } from 'vue'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import type { ChartInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
type ChartWidgetOptions = NonNullable<ChartInputSpec['options']> &
IWidgetOptions
type ChartWidgetOptions = IWidgetOptions & {
type?: 'bar' | 'line'
}
const value = defineModel<ChartData>({ required: true })

View File

@@ -1,10 +1,7 @@
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { IChartWidget } from '@/lib/litegraph/src/types/widgets'
import { isChartInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type {
ChartInputSpec,
InputSpec as InputSpecV2
} from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
export const useChartWidget = (): ComfyWidgetConstructorV2 => {
@@ -13,14 +10,13 @@ export const useChartWidget = (): ComfyWidgetConstructorV2 => {
throw new Error('Invalid input spec for chart widget')
}
const { name, options = {} } = inputSpec as ChartInputSpec
const { name, chartType = 'line', data = {} } = inputSpec
const chartType = options.type || 'line'
const widgetOptions = { type: chartType }
const widget = node.addWidget('chart', name, options.data || {}, () => {}, {
const widget = node.addWidget('chart', name, data, () => {}, {
serialize: true,
type: chartType,
...options
...widgetOptions
}) as IChartWidget
return widget

View File

@@ -1,15 +1,16 @@
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { IColorWidget } from '@/lib/litegraph/src/types/widgets'
import type {
ColorInputSpec,
InputSpec as InputSpecV2
} from '@/schemas/nodeDef/nodeDefSchemaV2'
import { isColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
export const useColorWidget = (): ComfyWidgetConstructorV2 => {
return (node: LGraphNode, inputSpec: InputSpecV2): IColorWidget => {
const { name, default: defaultValue = '#000000' } =
inputSpec as ColorInputSpec
if (!isColorInputSpec(inputSpec)) {
throw new Error('Invalid input spec for color widget')
}
const { name, default: defaultValue = '#000000' } = inputSpec
const widget = node.addWidget('color', name, defaultValue, () => {}, {
serialize: true

View File

@@ -1,25 +1,20 @@
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { IGalleriaWidget } from '@/lib/litegraph/src/types/widgets'
import type {
GalleriaInputSpec,
InputSpec as InputSpecV2
} from '@/schemas/nodeDef/nodeDefSchemaV2'
import { isGalleriaInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
export const useGalleriaWidget = (): ComfyWidgetConstructorV2 => {
return (node: LGraphNode, inputSpec: InputSpecV2): IGalleriaWidget => {
const { name, options = {} } = inputSpec as GalleriaInputSpec
if (!isGalleriaInputSpec(inputSpec)) {
throw new Error('Invalid input spec for galleria widget')
}
const widget = node.addWidget(
'galleria',
name,
options.images || [],
() => {},
{
serialize: true,
...options
}
) as IGalleriaWidget
const { name, images = [] } = inputSpec
const widget = node.addWidget('galleria', name, images, () => {}, {
serialize: true
}) as IGalleriaWidget
return widget
}

View File

@@ -1,20 +1,19 @@
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { ITextareaWidget } from '@/lib/litegraph/src/types/widgets'
import type {
InputSpec as InputSpecV2,
TextareaInputSpec
} from '@/schemas/nodeDef/nodeDefSchemaV2'
import { isTextareaInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
export const useTextareaWidget = (): ComfyWidgetConstructorV2 => {
return (node: LGraphNode, inputSpec: InputSpecV2): ITextareaWidget => {
const textareaSpec = inputSpec as TextareaInputSpec
const { name, default: defaultValue = '' } = textareaSpec
const widgetOptions = {
rows: textareaSpec.rows ?? 5,
cols: textareaSpec.cols ?? 50
if (!isTextareaInputSpec(inputSpec)) {
throw new Error('Invalid input spec for textarea widget')
}
const { name, default: defaultValue = '', rows = 5, cols = 50 } = inputSpec
const widgetOptions = { rows, cols }
const widget = node.addWidget('textarea', name, defaultValue, () => {}, {
serialize: true,
...widgetOptions

View File

@@ -87,23 +87,15 @@ const zChartInputSpec = zBaseInputOptions.extend({
type: z.literal('CHART'),
name: z.string(),
isOptional: z.boolean().optional(),
options: z
.object({
type: z.enum(['bar', 'line']).optional(),
data: z.object({}).optional()
})
.optional()
chartType: z.enum(['bar', 'line']).optional(),
data: z.object({}).optional()
})
const zGalleriaInputSpec = zBaseInputOptions.extend({
type: z.literal('GALLERIA'),
name: z.string(),
isOptional: z.boolean().optional(),
options: z
.object({
images: z.array(z.string()).optional()
})
.optional()
images: z.array(z.string()).optional()
})
const zTextareaInputSpec = zBaseInputOptions.extend({
@@ -211,9 +203,9 @@ export type ComboInputSpec = z.infer<typeof zComboInputSpec>
export type ColorInputSpec = z.infer<typeof zColorInputSpec>
export type ImageCompareInputSpec = z.infer<typeof zImageCompareInputSpec>
export type BoundingBoxInputSpec = z.infer<typeof zBoundingBoxInputSpec>
export type ChartInputSpec = z.infer<typeof zChartInputSpec>
export type GalleriaInputSpec = z.infer<typeof zGalleriaInputSpec>
export type TextareaInputSpec = z.infer<typeof zTextareaInputSpec>
type ChartInputSpec = z.infer<typeof zChartInputSpec>
type GalleriaInputSpec = z.infer<typeof zGalleriaInputSpec>
type TextareaInputSpec = z.infer<typeof zTextareaInputSpec>
export type CurveInputSpec = z.infer<typeof zCurveInputSpec>
export type RangeInputSpec = z.infer<typeof zRangeInputSpec>
export type CustomInputSpec = z.infer<typeof zCustomInputSpec>
@@ -257,3 +249,21 @@ export const isChartInputSpec = (
): inputSpec is ChartInputSpec => {
return inputSpec.type === 'CHART'
}
export const isColorInputSpec = (
inputSpec: InputSpec
): inputSpec is ColorInputSpec => {
return inputSpec.type === 'COLOR'
}
export const isTextareaInputSpec = (
inputSpec: InputSpec
): inputSpec is TextareaInputSpec => {
return inputSpec.type === 'TEXTAREA'
}
export const isGalleriaInputSpec = (
inputSpec: InputSpec
): inputSpec is GalleriaInputSpec => {
return inputSpec.type === 'GALLERIA'
}