Files
DemonGatanjieuandClaude Opus 5.5 a173e4e561 feat(node): Current node panel with one list of saved parameters
The Node Assistant becomes "Current node" (当前节点): one page instead of the
"actions" / "parameter sets" tabs, model actions on top, Parameters below.

- node_parameter_sets.js: the node type's saved values from the material
  library and from recipes' parameter sets as one list; the same values
  saved in both are one entry naming both sources. For the selected node,
  exactly what an entry would change; seeds (and their after-run control),
  model files and choices this computer lacks are kept.
- ui_node_parameters.js: the list, "apply these N" (one undo in the
  receipt) and "save these values".
- api/node_material.py: POST /anomalous/save_node_material keeps one
  node's values as a node_parameter_selection material, deduplicated by
  type and values; the library lists it under parameters.
- Removed: ui_node_presets.js with its hover popovers, the beta notice and
  their styles and strings. The panel and help texts use the new name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:09:49 +08:00

66 lines
2.9 KiB
Python

"""Saving one canvas node's current values as a parameter material (the current-node panel's "save")."""
import asyncio
import hashlib
import json
import time
import uuid
from aiohttp import web
from . import materials
from .material_schema import MAX_MATERIAL_NAME_LENGTH, _material_summary, _normalise_material_tags
from .notebooks import MAX_NOTEBOOK_BYTES
def _node_material(payload):
node = payload.get("node")
if not isinstance(node, dict):
raise ValueError("Invalid node")
node_type = node.get("type")
if not isinstance(node_type, str) or not node_type.strip() or len(node_type) > 200:
raise ValueError("Invalid node type")
values = node.get("widgets_values")
if not isinstance(values, list) or not values:
raise ValueError("Invalid node values")
encoded = json.dumps([node_type, values], ensure_ascii=False, allow_nan=False, sort_keys=True)
if len(encoded.encode("utf-8")) > MAX_NOTEBOOK_BYTES:
raise ValueError("Node values are too large")
title = node.get("title") if isinstance(node.get("title"), str) else ""
title = title.strip()[:200] or node_type
name = payload.get("name") or title
if not isinstance(name, str) or not (name := name.strip()) or len(name) > MAX_MATERIAL_NAME_LENGTH:
raise ValueError("Invalid material name")
return {
"schema_version": materials.MATERIAL_SCHEMA_VERSION,
"id": uuid.uuid4().hex,
"kind": "node_parameter_selection",
"name": name,
"tags": _normalise_material_tags(payload.get("tags", [])),
"timestamp": int(time.time() * 1000),
"source": {
"type": "canvas_node",
"parameter_name": title,
"parameter_signature": hashlib.sha256(encoded.encode("utf-8")).hexdigest(),
},
"workflow": {"nodes": [{"id": 1, "type": node_type, "title": title, "widgets_values": values}]},
"selection": {"scope": "nodes", "node_ids": [1]},
"capabilities": ["apply_node_parameters"],
}
async def api_save_node_material(request):
try:
material = _node_material(await request.json())
except (AttributeError, TypeError, ValueError):
return web.json_response({"status": "error", "message": "Invalid node material"}, status=400)
filename = f"material_{int(time.time())}_{material['id']}.json"
try:
duplicate = await asyncio.to_thread(
materials._persist_parameter_material, materials.get_materials_dir(), filename, material, False)
except (OSError, ValueError):
return web.json_response({"status": "error", "message": "Could not save node material"}, status=500)
if duplicate:
return web.json_response({"status": "duplicate", "filename": duplicate["filename"], "name": duplicate["name"]}, status=409)
return web.json_response({"status": "success", "filename": filename, "material": _material_summary(filename, material)})