Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ const config: KnipConfig = {
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'packages/registry-types/src/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts'
'src/scripts/ui/components/splitButton.ts',
// Linear Mode infrastructure - exports will be used by UI components in next PR
'src/renderer/extensions/linearMode/**/*.ts'
],
compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
Expand Down
48 changes: 48 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1846,5 +1846,53 @@
"vueNodesBanner": {
"message": "Nodes just got a new look and feel",
"tryItOut": "Try it out"
},
"linearMode": {
"title": "Linear Mode",
"open": "Open Linear Mode",
"close": "Close Linear Mode",
"generate": "Generate",
"generating": "Generating...",
"history": "History",
"noHistory": "No generations yet",
"noHistoryMessage": "Your generated images will appear here",
"loadTemplate": "Load Template",
"loadingTemplate": "Loading template...",
"templateLoadError": "Failed to load template",
"invalidTemplate": "Invalid template ID",
"widgetGroups": {
"content": "Content",
"dimensions": "Image Size",
"generation": "Generation Settings",
"advanced": "Advanced"
},
"widgets": {
"prompt": "Prompt",
"promptPlaceholder": "Describe the image you want to generate...",
"promptTooltip": "Describe what you want to see in the image",
"negativePrompt": "Negative Prompt",
"negativePromptPlaceholder": "What to avoid in the image...",
"negativePromptTooltip": "Describe what you want to avoid",
"seed": "Seed",
"seedTooltip": "Random seed for generation. Use same seed for reproducible results.",
"steps": "Steps",
"stepsTooltip": "Number of denoising steps. Higher = better quality but slower.",
"cfgScale": "CFG Scale",
"cfgScaleTooltip": "How closely to follow the prompt. Higher = more literal.",
"sampler": "Sampler",
"samplerTooltip": "Sampling algorithm to use",
"scheduler": "Scheduler",
"width": "Width",
"widthTooltip": "Output image width",
"height": "Height",
"heightTooltip": "Output image height",
"batchSize": "Batch Size",
"batchSizeTooltip": "Number of images to generate at once"
},
"errors": {
"queueFailed": "Failed to queue generation",
"noWorkflow": "No workflow loaded",
"widgetUpdateFailed": "Failed to update widget value"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ref } from 'vue'
import { app } from '@/scripts/app'
import { useLinearModeStore } from '../stores/linearModeStore'
import type { NodeExecutionId } from '@/types/nodeIdentification'

// @knipIgnore - Will be used by Linear Mode UI components
export function useLinearModeQueue() {
const linearModeStore = useLinearModeStore()
const isQueueing = ref(false)
const lastError = ref<Error | null>(null)

async function queuePrompt(
number: number = -1,
batchCount: number = 1,
queueNodeIds?: NodeExecutionId[]
): Promise<boolean> {
isQueueing.value = true
lastError.value = null

try {
const success = await app.queuePrompt(
number,
batchCount,
queueNodeIds,
(response) => {
if (response.prompt_id) {
linearModeStore.trackGeneratedPrompt(response.prompt_id)
}
}
)

return success
} catch (error) {
lastError.value =
error instanceof Error ? error : new Error(String(error))
return false
} finally {
isQueueing.value = false
}
}

return {
queuePrompt,
isQueueing,
lastError
}
}
154 changes: 154 additions & 0 deletions src/renderer/extensions/linearMode/linearModeConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import type { LinearModeTemplate, PromotedWidget } from './linearModeTypes'

const defaultLinearPromotedWidgets: PromotedWidget[] = [
{
nodeId: 6,
widgetName: 'text',
displayName: 'Prompt',
type: 'text',
config: {
multiline: true,
placeholder: 'Describe the image you want to generate...',
maxLength: 5000
},
tooltip: 'Describe what you want to see in the image',
group: 'content'
},
{
nodeId: 7,
widgetName: 'text',
displayName: 'Negative Prompt',
type: 'text',
config: {
multiline: true,
placeholder: 'What to avoid in the image...'
},
tooltip: 'Describe what you want to avoid',
group: 'content'
},
{
nodeId: 3,
widgetName: 'seed',
displayName: 'Seed',
type: 'number',
config: {
min: 0,
max: Number.MAX_SAFE_INTEGER,
randomizable: true
},
tooltip:
'Random seed for generation. Use same seed for reproducible results.',
group: 'generation'
},
{
nodeId: 3,
widgetName: 'steps',
displayName: 'Steps',
type: 'slider',
config: {
min: 1,
max: 150,
step: 1,
default: 20
},
tooltip: 'Number of denoising steps. Higher = better quality but slower.',
group: 'generation'
},
{
nodeId: 3,
widgetName: 'cfg',
displayName: 'CFG Scale',
type: 'slider',
config: {
min: 0,
max: 20,
step: 0.5,
default: 7.0
},
tooltip: 'How closely to follow the prompt. Higher = more literal.',
group: 'generation'
},
{
nodeId: 3,
widgetName: 'sampler_name',
displayName: 'Sampler',
type: 'combo',
config: {
options: ['euler', 'euler_a', 'dpmpp_2m', 'dpmpp_sde', 'ddim']
},
tooltip: 'Sampling algorithm to use',
group: 'advanced'
},
{
nodeId: 3,
widgetName: 'scheduler',
displayName: 'Scheduler',
type: 'combo',
config: {
options: ['normal', 'karras', 'exponential', 'sgm_uniform']
},
group: 'advanced'
},
{
nodeId: 5,
widgetName: 'width',
displayName: 'Width',
type: 'combo',
config: {
options: [512, 768, 1024, 1280, 1536, 2048]
},
tooltip: 'Output image width',
group: 'dimensions'
},
{
nodeId: 5,
widgetName: 'height',
displayName: 'Height',
type: 'combo',
config: {
options: [512, 768, 1024, 1280, 1536, 2048]
},
tooltip: 'Output image height',
group: 'dimensions'
},
{
nodeId: 5,
widgetName: 'batch_size',
displayName: 'Batch Size',
type: 'slider',
config: {
min: 1,
max: 8,
step: 1,
default: 1
},
tooltip: 'Number of images to generate at once',
group: 'advanced'
}
]

// @knipIgnore - Will be used by Linear Mode UI components
export const LINEAR_MODE_TEMPLATES: Record<string, LinearModeTemplate> = {
'template-default-linear': {
id: 'template-default-linear',
name: 'Linear Mode Template',
templatePath: '/templates/template-default-linear.json',
promotedWidgets: defaultLinearPromotedWidgets,
description: 'Default Linear Mode template for simplified image generation',
tags: ['text-to-image', 'default', 'recommended']
}
}

// @knipIgnore - Will be used by Linear Mode UI components
export const WIDGET_GROUPS = {
content: { label: 'Content', order: 1 },
dimensions: { label: 'Image Size', order: 2 },
generation: { label: 'Generation Settings', order: 3 },
advanced: { label: 'Advanced', order: 4, collapsible: true }
} as const

export function getTemplateConfig(
templateId: string
): LinearModeTemplate | null {
return LINEAR_MODE_TEMPLATES[templateId] ?? null
}
109 changes: 109 additions & 0 deletions src/renderer/extensions/linearMode/linearModeService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { app } from '@/scripts/app'
import { api } from '@/scripts/api'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useLinearModeStore } from './stores/linearModeStore'
import type {
ComfyNode,
ComfyWorkflowJSON
} from '@/platform/workflow/validation/schemas/workflowSchema'
import type { PromotedWidget } from './linearModeTypes'

export async function loadTemplate(
templatePath: string
): Promise<ComfyWorkflowJSON> {
const response = await fetch(api.fileURL(templatePath))
if (!response.ok) {
throw new Error(`Failed to load template: ${response.statusText}`)
}
return await response.json()
}

export function getWidgetValue(
workflow: ComfyWorkflowJSON,
nodeId: number,
widgetName: string
): unknown {
const nodeIdStr = String(nodeId)
const node = workflow.nodes?.find(
(n: ComfyNode) => String(n.id) === nodeIdStr
)
if (!node) return undefined

if (!node.widgets_values) return undefined
if (Array.isArray(node.widgets_values)) return undefined

return node.widgets_values[widgetName]
}

export function setWidgetValue(
workflow: ComfyWorkflowJSON,
nodeId: number,
widgetName: string,
value: unknown
): boolean {
const nodeIdStr = String(nodeId)
const node = workflow.nodes?.find(
(n: ComfyNode) => String(n.id) === nodeIdStr
)
if (!node) return false

if (!node.widgets_values) {
node.widgets_values = {}
}

if (Array.isArray(node.widgets_values)) {
return false
}

node.widgets_values[widgetName] = value
return true
}

export function getAllWidgetValues(): Map<string, unknown> {
const linearModeStore = useLinearModeStore()
const workflowStore = useWorkflowStore()

const values = new Map<string, unknown>()
const workflow = workflowStore.activeWorkflow?.activeState

if (!workflow) return values

for (const widget of linearModeStore.promotedWidgets) {
const value = getWidgetValue(workflow, widget.nodeId, widget.widgetName)
values.set(widget.displayName, value)
}

return values
}

export function updateWidgetValue(
widget: PromotedWidget,
value: unknown
): boolean {
const workflowStore = useWorkflowStore()
const workflow = workflowStore.activeWorkflow?.activeState

if (!workflow) return false

return setWidgetValue(workflow, widget.nodeId, widget.widgetName, value)
}

export async function activateTemplate(templateId: string): Promise<void> {
const linearModeStore = useLinearModeStore()
const template = linearModeStore.template

if (!template || template.id !== templateId) {
throw new Error(`Template not found: ${templateId}`)
}

const workflow = await loadTemplate(template.templatePath)

await app.loadGraphData(workflow)
}

export async function initializeLinearMode(templateId: string): Promise<void> {
const linearModeStore = useLinearModeStore()

linearModeStore.open(templateId)
await activateTemplate(templateId)
}
Loading