mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 13:42:12 -03:00
feat(workflow-template): add new tab template workflow with auto-zoom
- Add GET /api/lm/example-workflows endpoint to list available templates
- Add GET /api/lm/example-workflows/{filename} to retrieve specific workflow
- Add 'New Tab Template Workflow' setting in LoRA Manager settings
- Automatically apply 80% zoom level when loading template workflows
- Override workflow's saved view settings to prevent visual zoom flicker
The feature allows users to select a template workflow from example_workflows/
directory to load when creating new workflow tabs, with a hardcoded 0.8 zoom
level for better initial view experience.
This commit is contained in:
@@ -252,6 +252,100 @@ class SupportersHandler:
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
|
||||
class ExampleWorkflowsHandler:
|
||||
"""Handler for example workflow templates."""
|
||||
|
||||
def __init__(self, logger: logging.Logger | None = None) -> None:
|
||||
self._logger = logger or logging.getLogger(__name__)
|
||||
|
||||
def _get_workflows_dir(self) -> str:
|
||||
"""Get the example workflows directory path."""
|
||||
current_file = os.path.abspath(__file__)
|
||||
root_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(current_file)))
|
||||
)
|
||||
return os.path.join(root_dir, "example_workflows")
|
||||
|
||||
def _format_workflow_name(self, filename: str) -> str:
|
||||
"""Convert filename to human-readable name."""
|
||||
name = os.path.splitext(filename)[0]
|
||||
name = name.replace("_", " ")
|
||||
return name
|
||||
|
||||
async def get_example_workflows(self, request: web.Request) -> web.Response:
|
||||
"""Return list of available example workflows."""
|
||||
try:
|
||||
workflows_dir = self._get_workflows_dir()
|
||||
workflows = [
|
||||
{
|
||||
"value": "Default",
|
||||
"label": "Default (Blank)",
|
||||
"path": None,
|
||||
}
|
||||
]
|
||||
|
||||
if os.path.exists(workflows_dir):
|
||||
for filename in sorted(os.listdir(workflows_dir)):
|
||||
if filename.endswith(".json"):
|
||||
workflows.append(
|
||||
{
|
||||
"value": filename,
|
||||
"label": self._format_workflow_name(filename),
|
||||
"path": f"example_workflows/{filename}",
|
||||
}
|
||||
)
|
||||
|
||||
return web.json_response({"success": True, "workflows": workflows})
|
||||
except Exception as exc:
|
||||
self._logger.error(
|
||||
"Error listing example workflows: %s", exc, exc_info=True
|
||||
)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def get_example_workflow(self, request: web.Request) -> web.Response:
|
||||
"""Return a specific example workflow JSON content."""
|
||||
try:
|
||||
filename = request.match_info.get("filename")
|
||||
if not filename:
|
||||
return web.json_response(
|
||||
{"success": False, "error": "Filename not provided"}, status=400
|
||||
)
|
||||
|
||||
if filename == "Default":
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"workflow": {
|
||||
"last_node_id": 0,
|
||||
"last_link_id": 0,
|
||||
"nodes": [],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
workflows_dir = self._get_workflows_dir()
|
||||
filepath = os.path.join(workflows_dir, filename)
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"Workflow not found: {filename}"},
|
||||
status=404,
|
||||
)
|
||||
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
workflow = json.load(f)
|
||||
|
||||
return web.json_response({"success": True, "workflow": workflow})
|
||||
except Exception as exc:
|
||||
self._logger.error("Error loading example workflow: %s", exc, exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
|
||||
class SettingsHandler:
|
||||
"""Sync settings between backend and frontend."""
|
||||
|
||||
@@ -1523,6 +1617,7 @@ class MiscHandlerSet:
|
||||
filesystem: FileSystemHandler,
|
||||
custom_words: CustomWordsHandler,
|
||||
supporters: SupportersHandler,
|
||||
example_workflows: ExampleWorkflowsHandler,
|
||||
) -> None:
|
||||
self.health = health
|
||||
self.settings = settings
|
||||
@@ -1536,6 +1631,7 @@ class MiscHandlerSet:
|
||||
self.filesystem = filesystem
|
||||
self.custom_words = custom_words
|
||||
self.supporters = supporters
|
||||
self.example_workflows = example_workflows
|
||||
|
||||
def to_route_mapping(
|
||||
self,
|
||||
@@ -1565,6 +1661,8 @@ class MiscHandlerSet:
|
||||
"open_settings_location": self.filesystem.open_settings_location,
|
||||
"search_custom_words": self.custom_words.search_custom_words,
|
||||
"get_supporters": self.supporters.get_supporters,
|
||||
"get_example_workflows": self.example_workflows.get_example_workflows,
|
||||
"get_example_workflow": self.example_workflows.get_example_workflow,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -38,12 +38,24 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
||||
RouteDefinition("GET", "/api/lm/get-registry", "get_registry"),
|
||||
RouteDefinition("GET", "/api/lm/check-model-exists", "check_model_exists"),
|
||||
RouteDefinition("GET", "/api/lm/civitai/user-models", "get_civitai_user_models"),
|
||||
RouteDefinition("POST", "/api/lm/download-metadata-archive", "download_metadata_archive"),
|
||||
RouteDefinition("POST", "/api/lm/remove-metadata-archive", "remove_metadata_archive"),
|
||||
RouteDefinition("GET", "/api/lm/metadata-archive-status", "get_metadata_archive_status"),
|
||||
RouteDefinition("GET", "/api/lm/model-versions-status", "get_model_versions_status"),
|
||||
RouteDefinition(
|
||||
"POST", "/api/lm/download-metadata-archive", "download_metadata_archive"
|
||||
),
|
||||
RouteDefinition(
|
||||
"POST", "/api/lm/remove-metadata-archive", "remove_metadata_archive"
|
||||
),
|
||||
RouteDefinition(
|
||||
"GET", "/api/lm/metadata-archive-status", "get_metadata_archive_status"
|
||||
),
|
||||
RouteDefinition(
|
||||
"GET", "/api/lm/model-versions-status", "get_model_versions_status"
|
||||
),
|
||||
RouteDefinition("POST", "/api/lm/settings/open-location", "open_settings_location"),
|
||||
RouteDefinition("GET", "/api/lm/custom-words/search", "search_custom_words"),
|
||||
RouteDefinition("GET", "/api/lm/example-workflows", "get_example_workflows"),
|
||||
RouteDefinition(
|
||||
"GET", "/api/lm/example-workflows/{filename}", "get_example_workflow"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +79,11 @@ class MiscRouteRegistrar:
|
||||
definitions: Iterable[RouteDefinition] = MISC_ROUTE_DEFINITIONS,
|
||||
) -> None:
|
||||
for definition in definitions:
|
||||
self._bind(definition.method, definition.path, handler_lookup[definition.handler_name])
|
||||
self._bind(
|
||||
definition.method,
|
||||
definition.path,
|
||||
handler_lookup[definition.handler_name],
|
||||
)
|
||||
|
||||
def _bind(self, method: str, path: str, handler: Callable) -> None:
|
||||
add_method_name = self._METHOD_MAP[method.upper()]
|
||||
|
||||
@@ -19,6 +19,7 @@ from ..services.downloader import get_downloader
|
||||
from ..utils.usage_stats import UsageStats
|
||||
from .handlers.misc_handlers import (
|
||||
CustomWordsHandler,
|
||||
ExampleWorkflowsHandler,
|
||||
FileSystemHandler,
|
||||
HealthCheckHandler,
|
||||
LoraCodeHandler,
|
||||
@@ -38,9 +39,10 @@ from .misc_route_registrar import MiscRouteRegistrar
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1" or os.environ.get(
|
||||
"HF_HUB_DISABLE_TELEMETRY", "0"
|
||||
) == "0"
|
||||
standalone_mode = (
|
||||
os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
|
||||
or os.environ.get("HF_HUB_DISABLE_TELEMETRY", "0") == "0"
|
||||
)
|
||||
|
||||
|
||||
class MiscRoutes:
|
||||
@@ -75,7 +77,9 @@ class MiscRoutes:
|
||||
self._node_registry = node_registry or NodeRegistry()
|
||||
self._standalone_mode = standalone_mode_flag
|
||||
|
||||
self._handler_mapping: Mapping[str, Callable[[web.Request], Awaitable[web.StreamResponse]]] | None = None
|
||||
self._handler_mapping: (
|
||||
Mapping[str, Callable[[web.Request], Awaitable[web.StreamResponse]]] | None
|
||||
) = None
|
||||
|
||||
@staticmethod
|
||||
def setup_routes(app: web.Application) -> None:
|
||||
@@ -87,7 +91,9 @@ class MiscRoutes:
|
||||
registrar = self._registrar_factory(app)
|
||||
registrar.register_routes(self._ensure_handler_mapping())
|
||||
|
||||
def _ensure_handler_mapping(self) -> Mapping[str, Callable[[web.Request], Awaitable[web.StreamResponse]]]:
|
||||
def _ensure_handler_mapping(
|
||||
self,
|
||||
) -> Mapping[str, Callable[[web.Request], Awaitable[web.StreamResponse]]]:
|
||||
if self._handler_mapping is None:
|
||||
handler_set = self._create_handler_set()
|
||||
self._handler_mapping = handler_set.to_route_mapping()
|
||||
@@ -121,6 +127,7 @@ class MiscRoutes:
|
||||
)
|
||||
custom_words = CustomWordsHandler()
|
||||
supporters = SupportersHandler()
|
||||
example_workflows = ExampleWorkflowsHandler()
|
||||
|
||||
return self._handler_set_factory(
|
||||
health=health,
|
||||
@@ -135,6 +142,7 @@ class MiscRoutes:
|
||||
filesystem=filesystem,
|
||||
custom_words=custom_words,
|
||||
supporters=supporters,
|
||||
example_workflows=example_workflows,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -19,10 +19,63 @@ const TAG_SPACE_REPLACEMENT_DEFAULT = false;
|
||||
const USAGE_STATISTICS_SETTING_ID = "loramanager.usage_statistics";
|
||||
const USAGE_STATISTICS_DEFAULT = true;
|
||||
|
||||
const NEW_TAB_TEMPLATE_ID = "loramanager.new_tab_template";
|
||||
const NEW_TAB_TEMPLATE_DEFAULT = "Default";
|
||||
|
||||
const NEW_TAB_ZOOM_LEVEL = 0.8;
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
let workflowOptions = [NEW_TAB_TEMPLATE_DEFAULT];
|
||||
let workflowOptionsFull = [{ value: "Default", label: "Default (Blank)", path: null }];
|
||||
let workflowOptionsLoaded = false;
|
||||
|
||||
const loadWorkflowOptions = async () => {
|
||||
if (workflowOptionsLoaded) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch("/api/lm/example-workflows");
|
||||
const data = await response.json();
|
||||
if (data.success && data.workflows) {
|
||||
workflowOptionsFull = data.workflows;
|
||||
workflowOptions = data.workflows.map((w) => w.label);
|
||||
workflowOptionsLoaded = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("LoRA Manager: Failed to fetch workflow options", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getWorkflowOptions = () => {
|
||||
// Function may be called with or without parameters
|
||||
// Return the current workflow options array
|
||||
return workflowOptions;
|
||||
};
|
||||
|
||||
const loadTemplateWorkflow = async (templateName) => {
|
||||
if (!templateName || templateName === NEW_TAB_TEMPLATE_DEFAULT) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const workflow = workflowOptionsFull.find((w) => w.label === templateName);
|
||||
if (workflow && workflow.value) {
|
||||
const workflowResponse = await fetch(
|
||||
`/api/lm/example-workflows/${encodeURIComponent(workflow.value)}`
|
||||
);
|
||||
const workflowData = await workflowResponse.json();
|
||||
if (workflowData.success && workflowData.workflow) {
|
||||
return workflowData.workflow;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("LoRA Manager: Failed to load template workflow", error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getWheelSensitivity = (() => {
|
||||
let settingsUnavailableLogged = false;
|
||||
|
||||
@@ -153,6 +206,32 @@ const getUsageStatisticsPreference = (() => {
|
||||
};
|
||||
})();
|
||||
|
||||
const getNewTabTemplatePreference = (() => {
|
||||
let settingsUnavailableLogged = false;
|
||||
|
||||
return () => {
|
||||
const settingManager = app?.extensionManager?.setting;
|
||||
if (!settingManager || typeof settingManager.get !== "function") {
|
||||
if (!settingsUnavailableLogged) {
|
||||
console.warn("LoRA Manager: settings API unavailable, using default new tab template.");
|
||||
settingsUnavailableLogged = true;
|
||||
}
|
||||
return NEW_TAB_TEMPLATE_DEFAULT;
|
||||
}
|
||||
|
||||
try {
|
||||
const value = settingManager.get(NEW_TAB_TEMPLATE_ID);
|
||||
return value ?? NEW_TAB_TEMPLATE_DEFAULT;
|
||||
} catch (error) {
|
||||
if (!settingsUnavailableLogged) {
|
||||
console.warn("LoRA Manager: unable to read new tab template setting, using default.", error);
|
||||
settingsUnavailableLogged = true;
|
||||
}
|
||||
return NEW_TAB_TEMPLATE_DEFAULT;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
// ============================================================================
|
||||
// Register Extension with All Settings
|
||||
// ============================================================================
|
||||
@@ -205,11 +284,95 @@ app.registerExtension({
|
||||
tooltip: "When enabled, LoRA Manager will track model usage statistics during workflow execution. Disabling this will prevent unnecessary disk writes.",
|
||||
category: ["LoRA Manager", "Statistics", "Usage Tracking"],
|
||||
},
|
||||
{
|
||||
id: NEW_TAB_TEMPLATE_ID,
|
||||
name: "New Tab Template Workflow",
|
||||
type: "combo",
|
||||
options: getWorkflowOptions,
|
||||
defaultValue: NEW_TAB_TEMPLATE_DEFAULT,
|
||||
tooltip: "Choose a template workflow to load when creating a new workflow tab. 'Default (Blank)' keeps ComfyUI's original blank workflow behavior.",
|
||||
category: ["LoRA Manager", "Workflow", "New Tab Template"],
|
||||
},
|
||||
],
|
||||
async setup() {
|
||||
await loadWorkflowOptions();
|
||||
|
||||
const originalNewBlankWorkflow = async () => {
|
||||
const blankGraph = {
|
||||
last_node_id: 0,
|
||||
last_link_id: 0,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0.4,
|
||||
};
|
||||
await app.loadGraphData(blankGraph);
|
||||
};
|
||||
|
||||
const waitForCommandStore = async (maxWaitMs = 5000) => {
|
||||
const startTime = Date.now();
|
||||
while (Date.now() - startTime < maxWaitMs) {
|
||||
if (app.extensionManager?.command?.commands) {
|
||||
return true;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const patchCommand = async () => {
|
||||
const storeReady = await waitForCommandStore();
|
||||
if (!storeReady) {
|
||||
console.warn("LoRA Manager: Could not access command store to patch NewBlankWorkflow");
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = app.extensionManager.command.commands;
|
||||
for (const cmd of commands) {
|
||||
if (cmd.id === "Comfy.NewBlankWorkflow") {
|
||||
const originalFunc = cmd.function;
|
||||
cmd.function = async (metadata) => {
|
||||
const templateName = getNewTabTemplatePreference();
|
||||
|
||||
if (templateName && templateName !== NEW_TAB_TEMPLATE_DEFAULT) {
|
||||
const workflowData = await loadTemplateWorkflow(templateName);
|
||||
if (workflowData) {
|
||||
// Override the workflow's saved view settings with our custom zoom
|
||||
if (!workflowData.extra) {
|
||||
workflowData.extra = {};
|
||||
}
|
||||
if (!workflowData.extra.ds) {
|
||||
workflowData.extra.ds = { offset: [0, 0], scale: 1 };
|
||||
}
|
||||
workflowData.extra.ds.scale = NEW_TAB_ZOOM_LEVEL;
|
||||
|
||||
await app.loadGraphData(workflowData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await originalNewBlankWorkflow();
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
patchCommand();
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Exports
|
||||
// ============================================================================
|
||||
|
||||
export { getWheelSensitivity, getAutoPathCorrectionPreference, getPromptTagAutocompletePreference, getTagSpaceReplacementPreference, getUsageStatisticsPreference };
|
||||
export {
|
||||
getWheelSensitivity,
|
||||
getAutoPathCorrectionPreference,
|
||||
getPromptTagAutocompletePreference,
|
||||
getTagSpaceReplacementPreference,
|
||||
getUsageStatisticsPreference,
|
||||
getNewTabTemplatePreference,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user