From 09bb87d0897435518ac6d19075affb0188bf964d Mon Sep 17 00:00:00 2001 From: Crazywoola <100913391+crazywoola@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:41:17 +0800 Subject: [PATCH] feat: harden /create and /refine workflow generation for edge cases (#37336) Co-authored-by: Claude Fable 5 --- api/controllers/console/app/generator.py | 23 + .../generator/prompts/builder_prompts.py | 70 ++- api/core/workflow/generator/runner.py | 289 ++++++++++-- api/core/workflow/generator/types.py | 3 + .../core/workflow/generator/test_prompts.py | 101 +++++ .../core/workflow/generator/test_runner.py | 426 ++++++++++++++++++ .../__tests__/use-gen-graph.spec.ts | 80 ++++ .../workflow/workflow-generator/index.tsx | 24 +- .../workflow-generator/use-gen-graph.ts | 34 +- web/i18n/ar-TN/workflow.json | 4 + web/i18n/de-DE/workflow.json | 4 + web/i18n/en-US/workflow.json | 4 + web/i18n/es-ES/workflow.json | 4 + web/i18n/fa-IR/workflow.json | 4 + web/i18n/fr-FR/workflow.json | 4 + web/i18n/hi-IN/workflow.json | 4 + web/i18n/id-ID/workflow.json | 4 + web/i18n/it-IT/workflow.json | 4 + web/i18n/ja-JP/workflow.json | 4 + web/i18n/ko-KR/workflow.json | 4 + web/i18n/nl-NL/workflow.json | 4 + web/i18n/pl-PL/workflow.json | 4 + web/i18n/pt-BR/workflow.json | 4 + web/i18n/ro-RO/workflow.json | 4 + web/i18n/ru-RU/workflow.json | 4 + web/i18n/sl-SI/workflow.json | 4 + web/i18n/th-TH/workflow.json | 4 + web/i18n/tr-TR/workflow.json | 4 + web/i18n/uk-UA/workflow.json | 4 + web/i18n/vi-VN/workflow.json | 4 + web/i18n/zh-Hans/workflow.json | 4 + web/i18n/zh-Hant/workflow.json | 4 + 32 files changed, 1093 insertions(+), 49 deletions(-) create mode 100644 web/app/components/workflow/workflow-generator/__tests__/use-gen-graph.spec.ts diff --git a/api/controllers/console/app/generator.py b/api/controllers/console/app/generator.py index e471d0ed88d..7d7fa98238d 100644 --- a/api/controllers/console/app/generator.py +++ b/api/controllers/console/app/generator.py @@ -44,6 +44,13 @@ class InstructionTemplatePayload(BaseModel): type: str = Field(..., description="Instruction template type") +# Upper bound for the generator's free-text inputs. Generous for prose (a +# detailed instruction rarely passes 2k chars) while keeping the +# planner+builder prompts well inside every mainstream context window. +# Mirrored by the ``maxLength`` on the frontend generator textarea. +_MAX_INSTRUCTION_LENGTH = 10_000 + + class WorkflowGeneratePayload(BaseModel): """Payload for the cmd+k `/create` and `/refine` workflow generator endpoint. @@ -320,6 +327,22 @@ class WorkflowGenerateApi(Resource): "errors": [{"code": "EMPTY_INSTRUCTION", "detail": "Instruction is required"}], }, 400 + # Bound the prompt at the boundary too: an arbitrarily long + # instruction (or pasted document) blows the planner/builder context + # window and fails with an opaque provider error after two slow LLM + # calls. The cap matches the frontend textarea's maxLength. + if len(args.instruction) > _MAX_INSTRUCTION_LENGTH or len(args.ideal_output) > _MAX_INSTRUCTION_LENGTH: + return { + "error": "Instruction is too long", + "errors": [ + { + "code": "INSTRUCTION_TOO_LONG", + "detail": f"Instruction and ideal output must each be at most " + f"{_MAX_INSTRUCTION_LENGTH} characters", + } + ], + }, 400 + try: result = WorkflowGeneratorService.generate_workflow_graph( tenant_id=current_tenant_id, diff --git a/api/core/workflow/generator/prompts/builder_prompts.py b/api/core/workflow/generator/prompts/builder_prompts.py index b4f7cec6d74..9aaf75bddbd 100644 --- a/api/core/workflow/generator/prompts/builder_prompts.py +++ b/api/core/workflow/generator/prompts/builder_prompts.py @@ -526,13 +526,77 @@ Now emit the complete workflow graph JSON. """ +# Node wrapper fields that carry no meaning the builder needs: pure canvas / +# selection state, plus geometry the runner's postprocess recomputes anyway. +# Stripping them out of the refine prompt cuts its size roughly in half on +# hand-edited graphs — fewer tokens in, and (because the builder echoes +# untouched nodes verbatim) far fewer tokens out, which is where the latency +# lives. +_PRUNED_NODE_KEYS = frozenset( + { + "positionAbsolute", + "sourcePosition", + "targetPosition", + "selected", + "dragging", + "measured", + } +) + +# Additionally pruned from TOP-LEVEL nodes only: the layered auto-layout +# recomputes their position and size defaults, so the builder never needs to +# reproduce them. Container children keep ``position`` (relative to the +# parent, which we cannot recompute) and containers keep ``width`` / +# ``height`` (their canvas size is real config, not a default). +_PRUNED_TOP_LEVEL_NODE_KEYS = _PRUNED_NODE_KEYS | {"position", "width", "height"} + +_CONTAINER_DATA_TYPES = frozenset({"iteration", "loop"}) + +# Edge fields the builder must echo; everything else (ids, zIndex, +# sourceType / targetType, isInIteration / isInLoop markers) is recomputed +# by the runner's postprocess from the node topology. +_KEPT_EDGE_KEYS = ("source", "target", "sourceHandle", "targetHandle") + + +def compact_graph_for_builder(current_graph: dict) -> dict: + """ + Strip canvas noise out of a draft graph before prompt injection. + + Keeps everything semantically meaningful — ids, wrapper ``type``, + ``parentId``, the full ``data`` config, child positions, container + sizes — and drops geometry / selection state the postprocess pass + recomputes. The builder echoes untouched nodes verbatim, so every byte + removed here is removed twice (prompt AND completion). + """ + nodes_out: list[dict] = [] + for node in current_graph.get("nodes") or []: + if not isinstance(node, dict): + continue + is_child = bool(node.get("parentId")) + is_container = isinstance(node.get("data"), dict) and node["data"].get("type") in _CONTAINER_DATA_TYPES + pruned = _PRUNED_NODE_KEYS if (is_child or is_container) else _PRUNED_TOP_LEVEL_NODE_KEYS + compact = {k: v for k, v in node.items() if k not in pruned} + if is_container: + # Container position is still recomputed by the layout pass. + compact.pop("position", None) + nodes_out.append(compact) + edges_out = [ + {k: edge[k] for k in _KEPT_EDGE_KEYS if k in edge} + for edge in (current_graph.get("edges") or []) + if isinstance(edge, dict) + ] + return {"nodes": nodes_out, "edges": edges_out} + + def format_builder_existing_graph_section(current_graph: dict | None) -> str: """ - Refine mode: give the builder the FULL existing graph JSON so it can keep + Refine mode: give the builder the existing graph JSON so it can keep every node and edge the user's change does not touch byte-for-byte — same ids, same config, same prompt templates. Without the full config the builder would regenerate untouched nodes from scratch and silently drop - the user's hand-tuned settings. + the user's hand-tuned settings. Canvas-only fields are stripped first + (see ``compact_graph_for_builder``) — they're recomputed in postprocess, + so carrying them only slows the call down. Returns an empty string in create mode (no ``current_graph``); the builder then behaves exactly as before, constructing the graph purely from the @@ -540,7 +604,7 @@ def format_builder_existing_graph_section(current_graph: dict | None) -> str: """ if not current_graph: return "" - graph_json = json.dumps(current_graph, ensure_ascii=False, separators=(",", ":")) + graph_json = json.dumps(compact_graph_for_builder(current_graph), ensure_ascii=False, separators=(",", ":")) return ( "# Existing graph to refine (JSON)\n\n" "You are REFINING this existing graph, NOT building from scratch. Apply " diff --git a/api/core/workflow/generator/runner.py b/api/core/workflow/generator/runner.py index 2897166b240..0a5477231bb 100644 --- a/api/core/workflow/generator/runner.py +++ b/api/core/workflow/generator/runner.py @@ -70,6 +70,10 @@ logger = logging.getLogger(__name__) _NODE_X_OFFSET = 80 _NODE_X_STEP = 320 _NODE_Y = 280 +# Vertical gap between lanes when two branches share the same topological +# depth (e.g. the two arms of an if-else). Default node height is 100, so +# 160 leaves clear air between stacked nodes. +_NODE_Y_STEP = 160 _DEFAULT_VIEWPORT: GraphViewportDict = {"x": 0.0, "y": 0.0, "zoom": 0.7} _DEFAULT_NODE_WIDTH = 244 _DEFAULT_NODE_HEIGHT = 100 @@ -575,34 +579,32 @@ class WorkflowGenerator: # Defensive ID remap: Dify's run-time placeholder regex only accepts # ``[a-zA-Z0-9_]`` in the node-id slot, so anything the LLM emits with - # hyphens (``node-1``, ``node-Kstart``, etc.) would break every - # placeholder pointing at it. Strip hyphens out of every id + every + # hyphens, dots, or spaces (``node-1``, ``node.2``, etc.) would break + # every placeholder pointing at it. Sanitize every id + every # cross-reference (edges' ``source`` / ``target``, ``parentId``, # ``start_node_id`` / ``iteration_id`` / ``loop_id`` on data, and the # ``{{#…#}}`` and ``["node-id", "var"]`` references) BEFORE the rest # of the postprocess pass touches them. - cls._strip_hyphens_from_node_ids(nodes=nodes, edges=edges) + cls._sanitize_node_ids(nodes=nodes, edges=edges) # Container-child nodes carry their own relative positions inside the # parent and have a special ``type`` (custom-iteration-start / # custom-loop-start). We must not override their positions or wrapper - # ``type``; only top-level (parentId-less) nodes get the left-to-right - # auto layout. - top_level_index = 0 + # ``type``; only top-level (parentId-less) nodes get the layered + # auto layout (x = topological depth, y = lane within the layer). + cls._layout_top_level_nodes(nodes=nodes, edges=edges) for node in nodes: cls._fill_node_defaults(node) if node.get("parentId"): # Inner node — keep whatever the LLM emitted; only fill the # absolutely-required defaults so the canvas can render it. - node.setdefault("position", {"x": 0.0, "y": 0.0}) node.setdefault("zIndex", 1002) node.setdefault("extent", "parent") - else: - node["position"] = { - "x": float(_NODE_X_OFFSET + _NODE_X_STEP * top_level_index), - "y": float(_NODE_Y), - } - top_level_index += 1 + # Inner nodes keep their LLM-emitted relative position; top-level + # nodes were positioned by the layered layout. The setdefault only + # fires for inner nodes without a position and for a (broken) + # id-less node the layout pass couldn't see. + node.setdefault("position", {"x": 0.0, "y": 0.0}) node.setdefault("positionAbsolute", dict(node["position"])) node.setdefault("width", _DEFAULT_NODE_WIDTH) node.setdefault("height", _DEFAULT_NODE_HEIGHT) @@ -620,6 +622,12 @@ class WorkflowGenerator: if n.get("id") in inner_node_to_parent.values(): parent_type[n["id"]] = n.get("data", {}).get("type", "") + # Branch nodes (if-else / question-classifier) emit one handle per + # case; an edge leaving them on the default "source" handle dangles + # off a handle that doesn't exist on the canvas. Repair the + # unambiguous cases before edge ids are computed from the handles. + cls._repair_branch_edge_handles(nodes=nodes, edges=edges) + # Dedupe edges (LLMs sometimes emit the same edge twice). seen: set[tuple[str, str, str, str]] = set() deduped_edges = [] @@ -712,14 +720,19 @@ class WorkflowGenerator: r"\{\{#([a-zA-Z0-9_]{1,50})\.([a-zA-Z_][a-zA-Z0-9_]{0,29}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){0,9})#\}\}" ) - # Lenient sibling used only by the defensive hyphen-strip pass — it - # allows hyphens in the node-id slot so we can rewrite the LLM's - # ``{{#node-1.var#}}`` outputs BEFORE the strict walker sees them. + # Lenient sibling used only by the defensive id-sanitize pass — it + # accepts ANY character in the node-id slot (except the ``.`` separator + # and ``#`` terminator) so we can rewrite the LLM's ``{{#node-1.var#}}`` + # / ``{{#node 2.var#}}`` outputs BEFORE the strict walker sees them. # Never use this for validation, only for rewriting. - _LENIENT_VAR_REF_RE: ClassVar = re.compile(r"\{\{#([A-Za-z0-9_-]+)\.([^#]+)#\}\}") + _LENIENT_VAR_REF_RE: ClassVar = re.compile(r"\{\{#([^#.{}]+)\.([^#]+)#\}\}") + + # Characters the run-time placeholder regex rejects in the node-id slot. + # Anything matching this in a node id must be sanitized away. + _INVALID_ID_CHARS_RE: ClassVar = re.compile(r"[^a-zA-Z0-9_]") # Strings inside ``data`` that look like node-id slugs and need - # remapping when we defensively strip hyphens out of LLM-emitted ids. + # remapping when we defensively sanitize LLM-emitted ids. _ID_FIELDS: ClassVar = frozenset({"start_node_id", "iteration_id", "loop_id", "parentId"}) # ``data`` keys whose value is a plain string list, never a @@ -860,34 +873,51 @@ class WorkflowGenerator: return False @classmethod - def _strip_hyphens_from_node_ids(cls, *, nodes: list[dict[str, Any]], edges: list[dict[str, Any]]) -> None: + def _sanitize_node_ids(cls, *, nodes: list[dict[str, Any]], edges: list[dict[str, Any]]) -> None: """ - Strip ``-`` out of every node id and rewrite every cross-reference. + Rewrite every node id to ``[a-zA-Z0-9_]`` and fix every cross-reference. Dify's run-time ``VARIABLE_PATTERN`` accepts only ``[a-zA-Z0-9_]`` in the node-id slot of ``{{#…#}}`` placeholders. The builder LLM often - emits ``node-1`` style ids; left unfixed those make every placeholder - silently fail at run time, the literal ``{{#node-1.var#}}`` survives - into the prompt, and the LLM at run time echoes it back as the user's - output — the bug we are here to kill. + emits ``node-1`` style ids (and occasionally dots or spaces); left + unfixed those make every placeholder silently fail at run time, the + literal ``{{#node-1.var#}}`` survives into the prompt, and the LLM at + run time echoes it back as the user's output — the bug we are here + to kill. - Approach: build a one-to-one ``old → new`` map by removing hyphens, - then rewrite (a) every node ``id``, (b) every edge ``source`` / - ``target``, (c) every ``parentId`` / ``start_node_id`` / - ``iteration_id`` / ``loop_id`` inside ``data``, (d) every - ``{{#…#}}`` reference in any string, (e) every ``["node-id", "var"]`` - value-selector list. We do NOT rename variable names — only ids. + Approach: build a one-to-one ``old → new`` map by dropping the invalid + characters — collision-safe: when the sanitized id is already taken + (e.g. the builder emitted BOTH ``node-1`` and ``node1``) a numeric + suffix keeps the two distinct instead of silently merging every + reference onto one node. Then rewrite (a) every node ``id``, (b) every + edge ``source`` / ``target``, (c) every ``parentId`` / + ``start_node_id`` / ``iteration_id`` / ``loop_id`` inside ``data``, + (d) every ``{{#…#}}`` reference in any string, (e) every + ``["node-id", "var"]`` value-selector list. We do NOT rename variable + names — only ids. """ - # Build id rewrite map. Collision-safe because we just strip a single - # character class — two different hyphenated ids ``node-1`` and - # ``node1`` would collide, but the builder LLM has been instructed - # to pick one style so in practice it's one or the other. id_map: dict[str, str] = {} + # Ids that are already valid are reserved up front so a sanitized id + # can never collide with an untouched sibling. + used: set[str] = { + n["id"] for n in nodes if isinstance(n.get("id"), str) and not cls._INVALID_ID_CHARS_RE.search(n["id"]) + } + fallback_seq = 0 for node in nodes: old = node.get("id") - if not isinstance(old, str) or "-" not in old: + if not isinstance(old, str) or not cls._INVALID_ID_CHARS_RE.search(old): continue - new = old.replace("-", "") + base = cls._INVALID_ID_CHARS_RE.sub("", old) + if not base: + # Id was nothing but invalid characters (e.g. "节点", "--"). + fallback_seq += 1 + base = f"node_{fallback_seq}" + new = base + suffix = 2 + while new in used: + new = f"{base}_{suffix}" + suffix += 1 + used.add(new) id_map[old] = new node["id"] = new if not id_map: @@ -901,10 +931,12 @@ class WorkflowGenerator: edge[key] = id_map[v] # Also rewrite the edge id if the builder emitted one referencing # the old ids; the dedupe pass later recomputes it anyway, but - # rewriting here keeps logs sane. + # rewriting here keeps logs sane. Longest-first so an id that is + # a substring of another (``node-1`` in ``node-12``) can't corrupt + # the longer match. eid = edge.get("id") if isinstance(eid, str): - for old, new in id_map.items(): + for old, new in sorted(id_map.items(), key=lambda kv: -len(kv[0])): eid = eid.replace(old, new) edge["id"] = eid @@ -955,6 +987,116 @@ class WorkflowGenerator: new_id = id_map.get(node_id, node_id) return f"{{{{#{new_id}.{rest}#}}}}" + @classmethod + def _repair_branch_edge_handles(cls, *, nodes: list[dict[str, Any]], edges: list[dict[str, Any]]) -> None: + """ + Re-home edges that leave a branch node on the default "source" handle. + + if-else exposes one source handle per ``case_id`` plus the implicit + "false" (ELSE) handle; question-classifier exposes one per class id. + The builder prompt documents this, but LLMs still emit the default + handle, which renders as an edge hanging off a handle that doesn't + exist and the branch silently never runs. + + Repair only when unambiguous: default-handle edges are assigned to the + node's UNUSED branch handles in declaration order, and only when there + are at least as many unused handles as edges to fix. Anything + ambiguous is left alone — a wrong guess that swaps the IF and ELSE + arms is worse than a visible dangling edge. + """ + for node in nodes: + data = node.get("data") or {} + node_type = data.get("type") + if node_type == BuiltinNodeTypes.IF_ELSE: + branch_handles = [ + str(case["case_id"]) + for case in (data.get("cases") or []) + if isinstance(case, dict) and case.get("case_id") + ] + # ELSE is implicit — it has a handle even though no case + # declares it. + branch_handles.append("false") + elif node_type == BuiltinNodeTypes.QUESTION_CLASSIFIER: + branch_handles = [ + str(klass["id"]) + for klass in (data.get("classes") or []) + if isinstance(klass, dict) and klass.get("id") + ] + else: + continue + + node_id = node.get("id") + outgoing = [e for e in edges if e.get("source") == node_id] + taken = {e.get("sourceHandle") for e in outgoing if e.get("sourceHandle") in branch_handles} + unused = [h for h in branch_handles if h not in taken] + defaulted = [e for e in outgoing if e.get("sourceHandle") in (None, "", "source")] + if not defaulted or len(defaulted) > len(unused): + continue + for edge, handle in zip(defaulted, unused): + edge["sourceHandle"] = handle + logger.info( + "Workflow generator: re-homed default-handle edge %s -> %s onto branch handle %r", + node_id, + edge.get("target"), + handle, + ) + + @classmethod + def _layout_top_level_nodes(cls, *, nodes: list[dict[str, Any]], edges: list[dict[str, Any]]) -> None: + """ + Lay out top-level nodes by graph topology instead of array order. + + x = longest-path depth from the entry layer, y = lane within the + layer — so an if-else's two arms render as two parallel rows instead + of overlapping on one line, and a builder that emits nodes out of + execution order still gets a left-to-right canvas. Longest-path (not + BFS) layering keeps a join node (variable-aggregator, end) to the + right of its deepest branch. + + Cycle-safe: Kahn's algorithm simply never reaches nodes on a cycle, + and those get parked one layer past the deepest laid-out node in + declaration order — the cycle itself is flagged by the structural + validator afterwards. + """ + top_level = [n for n in nodes if not n.get("parentId") and isinstance(n.get("id"), str) and n.get("id")] + id_set = {n["id"] for n in top_level} + + succs: dict[str, list[str]] = {node_id: [] for node_id in id_set} + indegree: dict[str, int] = dict.fromkeys(id_set, 0) + seen_pairs: set[tuple[str, str]] = set() + for edge in edges: + src, tgt = edge.get("source"), edge.get("target") + if not isinstance(src, str) or not isinstance(tgt, str): + continue + if src not in id_set or tgt not in id_set or src == tgt or (src, tgt) in seen_pairs: + continue + seen_pairs.add((src, tgt)) + succs[src].append(tgt) + indegree[tgt] += 1 + + depth: dict[str, int] = {} + queue = [n["id"] for n in top_level if indegree[n["id"]] == 0] + for node_id in queue: + depth[node_id] = 0 + while queue: + cur = queue.pop(0) + for nxt in succs[cur]: + depth[nxt] = max(depth.get(nxt, 0), depth[cur] + 1) + indegree[nxt] -= 1 + if indegree[nxt] == 0: + queue.append(nxt) + + overflow_depth = (max(depth.values()) + 1) if depth else 0 + lanes: dict[int, int] = {} + for node in top_level: + d = depth.get(node["id"], overflow_depth) + lane = lanes.get(d, 0) + lanes[d] = lane + 1 + node["position"] = { + "x": float(_NODE_X_OFFSET + _NODE_X_STEP * d), + "y": float(_NODE_Y + _NODE_Y_STEP * lane), + } + @classmethod def _inject_start_variable(cls, start_node: dict[str, Any], var: str) -> None: """Add a default ``paragraph`` input so ``{{#start.#}}`` resolves.""" @@ -1122,6 +1264,24 @@ class WorkflowGenerator: errors.append(_err(WorkflowGenerateErrorCode.INVALID_SCHEMA, "Generated graph has no nodes")) return errors + # Duplicate ids make every cross-reference ambiguous (edges, variable + # placeholders, parentId all resolve to "whichever node wins"), so a + # graph with them is unusable no matter how the canvas renders it. + id_counts: dict[str, int] = {} + for node in nodes: + node_id = node.get("id", "") + if node_id: + id_counts[node_id] = id_counts.get(node_id, 0) + 1 + for node_id, count in id_counts.items(): + if count > 1: + errors.append( + _err( + WorkflowGenerateErrorCode.DUPLICATE_NODE_ID, + f"Duplicate node id {node_id!r} ({count} nodes share it)", + node_id=node_id, + ) + ) + types = [node.get("data", {}).get("type", "") for node in nodes] starts = [t for t in types if t == BuiltinNodeTypes.START] if len(starts) != 1: @@ -1156,6 +1316,11 @@ class WorkflowGenerator: if tgt not in known_ids: errors.append(_err(WorkflowGenerateErrorCode.DANGLING_EDGE, f"Edge target node not found: {tgt!r}")) + # Workflow graphs must be DAGs — a directed cycle hangs or errors the + # run, and nothing downstream of the cycle ever executes. (A "loop" + # container is the sanctioned way to iterate; its edges are internal.) + errors.extend(cls._collect_edge_cycle_errors(graph=graph, known_ids=known_ids)) + # Dangling node-id references in node ``data`` (parentId, start_node_id, iteration_id, loop_id). errors.extend(cls._collect_dangling_id_refs(nodes=nodes, known_ids=known_ids)) @@ -1175,6 +1340,54 @@ class WorkflowGenerator: return errors + @classmethod + def _collect_edge_cycle_errors(cls, *, graph: GraphDict, known_ids: set[str]) -> list[WorkflowGenerateErrorDict]: + """ + Flag directed cycles among the graph's edges (Kahn's algorithm). + + Self-loops are reported per node; a longer cycle is reported once, + naming every node Kahn's peeling never reaches (cycle members plus + anything downstream of them). Edges into unknown ids are ignored + here — the dangling-edge check already covers those. + """ + out: list[WorkflowGenerateErrorDict] = [] + succs: dict[str, list[str]] = {node_id: [] for node_id in known_ids} + indegree: dict[str, int] = dict.fromkeys(known_ids, 0) + for edge in graph.get("edges", []): + src, tgt = edge.get("source"), edge.get("target") + if src not in known_ids or tgt not in known_ids: + continue + if src == tgt: + out.append( + _err( + WorkflowGenerateErrorCode.GRAPH_CYCLE, + f"Node {src!r} has an edge pointing at itself", + node_id=src, + ) + ) + continue + succs[src].append(tgt) + indegree[tgt] += 1 + + queue = [node_id for node_id, deg in indegree.items() if deg == 0] + visited = 0 + while queue: + cur = queue.pop() + visited += 1 + for nxt in succs[cur]: + indegree[nxt] -= 1 + if indegree[nxt] == 0: + queue.append(nxt) + if visited < len(known_ids): + trapped = sorted(node_id for node_id, deg in indegree.items() if deg > 0) + out.append( + _err( + WorkflowGenerateErrorCode.GRAPH_CYCLE, + f"Workflow graph contains a cycle; affected nodes: {', '.join(trapped)}", + ) + ) + return out + @classmethod def _collect_dangling_id_refs( cls, *, nodes: list[dict[str, Any]], known_ids: set[str] diff --git a/api/core/workflow/generator/types.py b/api/core/workflow/generator/types.py index c62dc7a3f03..ddb2ba7ce62 100644 --- a/api/core/workflow/generator/types.py +++ b/api/core/workflow/generator/types.py @@ -19,6 +19,9 @@ class WorkflowGenerateErrorCode: INVALID_JSON: Final = "INVALID_JSON" INVALID_SCHEMA: Final = "INVALID_SCHEMA" EMPTY_INSTRUCTION: Final = "EMPTY_INSTRUCTION" + INSTRUCTION_TOO_LONG: Final = "INSTRUCTION_TOO_LONG" + DUPLICATE_NODE_ID: Final = "DUPLICATE_NODE_ID" + GRAPH_CYCLE: Final = "GRAPH_CYCLE" EMPTY_PLAN: Final = "EMPTY_PLAN" UNKNOWN_NODE_REFERENCE: Final = "UNKNOWN_NODE_REFERENCE" INVALID_CONTAINER: Final = "INVALID_CONTAINER" diff --git a/api/tests/unit_tests/core/workflow/generator/test_prompts.py b/api/tests/unit_tests/core/workflow/generator/test_prompts.py index e1ba146c2aa..4b43f648e4a 100644 --- a/api/tests/unit_tests/core/workflow/generator/test_prompts.py +++ b/api/tests/unit_tests/core/workflow/generator/test_prompts.py @@ -10,6 +10,8 @@ when data is present, and (3) round-trip the raw catalogue text unchanged. from core.workflow.generator.prompts.builder_prompts import ( BUILDER_SYSTEM_PROMPT_ADVANCED_CHAT, BUILDER_SYSTEM_PROMPT_WORKFLOW, + compact_graph_for_builder, + format_builder_existing_graph_section, format_builder_tool_catalogue_section, format_plan_block, get_builder_system_prompt, @@ -182,3 +184,102 @@ class TestFormatPlanBlockParentHints: ] ) assert "parent='Ghost Container'" in out + + +class TestCompactGraphForBuilder: + """ + The refine-mode existing-graph JSON is the single biggest token sink in + the pipeline — and the builder echoes untouched nodes back, doubling the + cost. The compactor must drop canvas noise (recomputed in postprocess) + while keeping everything the builder genuinely has to preserve. + """ + + @staticmethod + def _graph() -> dict: + return { + "nodes": [ + { + "id": "node1", + "type": "custom", + "position": {"x": 80, "y": 282}, + "positionAbsolute": {"x": 80, "y": 282}, + "width": 244, + "height": 100, + "sourcePosition": "right", + "targetPosition": "left", + "selected": True, + "data": {"type": "start", "title": "Start", "variables": []}, + }, + { + "id": "iter1", + "type": "custom", + "position": {"x": 400, "y": 282}, + "width": 808, + "height": 204, + "data": {"type": "iteration", "title": "Per Item", "start_node_id": "iter1start"}, + }, + { + "id": "iter1start", + "type": "custom-iteration-start", + "parentId": "iter1", + "position": {"x": 60, "y": 78}, + "positionAbsolute": {"x": 460, "y": 360}, + "data": {"type": "iteration-start", "title": ""}, + }, + ], + "edges": [ + { + "id": "node1-source-iter1-target", + "source": "node1", + "target": "iter1", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "zIndex": 0, + "data": {"sourceType": "start", "targetType": "iteration", "isInIteration": False}, + } + ], + "viewport": {"x": 0, "y": 0, "zoom": 0.7}, + } + + def test_drops_canvas_noise_from_top_level_nodes(self): + compact = compact_graph_for_builder(self._graph()) + start = next(n for n in compact["nodes"] if n["id"] == "node1") + for key in ("position", "positionAbsolute", "width", "height", "sourcePosition", "targetPosition", "selected"): + assert key not in start + # Semantics survive. + assert start["data"]["type"] == "start" + assert start["type"] == "custom" + + def test_keeps_container_size_but_not_position(self): + compact = compact_graph_for_builder(self._graph()) + container = next(n for n in compact["nodes"] if n["id"] == "iter1") + assert container["width"] == 808 + assert container["height"] == 204 + assert "position" not in container + + def test_keeps_child_relative_position(self): + compact = compact_graph_for_builder(self._graph()) + child = next(n for n in compact["nodes"] if n["id"] == "iter1start") + assert child["position"] == {"x": 60, "y": 78} + assert child["parentId"] == "iter1" + assert child["type"] == "custom-iteration-start" + assert "positionAbsolute" not in child + + def test_edges_keep_only_topology_fields(self): + compact = compact_graph_for_builder(self._graph()) + assert compact["edges"] == [ + {"source": "node1", "target": "iter1", "sourceHandle": "source", "targetHandle": "target"} + ] + + def test_viewport_is_dropped(self): + assert "viewport" not in compact_graph_for_builder(self._graph()) + + def test_existing_graph_section_embeds_the_compact_graph(self): + section = format_builder_existing_graph_section(self._graph()) + assert "Existing graph to refine" in section + assert "positionAbsolute" not in section + assert '"start_node_id":"iter1start"' in section + + def test_existing_graph_section_empty_for_create_mode(self): + assert format_builder_existing_graph_section(None) == "" diff --git a/api/tests/unit_tests/core/workflow/generator/test_runner.py b/api/tests/unit_tests/core/workflow/generator/test_runner.py index 0117f7d27ce..067fb1cf950 100644 --- a/api/tests/unit_tests/core/workflow/generator/test_runner.py +++ b/api/tests/unit_tests/core/workflow/generator/test_runner.py @@ -8,11 +8,13 @@ readable error envelope. """ import json +from typing import Any, cast from unittest.mock import MagicMock import pytest from core.workflow.generator.runner import WorkflowGenerator +from core.workflow.generator.types import GraphDict def _llm_result(text: str) -> MagicMock: @@ -2495,3 +2497,427 @@ class TestWorkflowGeneratorFileVariables: ] WorkflowGenerator._normalize_start_file_variables(nodes=nodes) assert "allowed_file_types" not in nodes[0]["data"]["variables"][0] + + +class TestWorkflowGeneratorIdSanitization: + """ + Beyond hyphens: the sanitize pass must handle ANY character the run-time + placeholder regex rejects (dots, spaces, unicode) and must stay + collision-safe when stripping makes two ids identical — silently merging + ``node-1`` and ``node1`` would point every reference at one node. + """ + + def test_collision_between_stripped_and_existing_id_gets_a_suffix(self): + nodes: list[dict[str, Any]] = [ + {"id": "node1", "data": {"type": "start", "variables": []}}, + { + "id": "node-1", + "data": { + "type": "llm", + "prompt_template": [{"role": "user", "text": "{{#node-1.text#}} and {{#node1.x#}}"}], + }, + }, + ] + edges = [{"id": "e", "source": "node1", "target": "node-1"}] + + WorkflowGenerator._sanitize_node_ids(nodes=nodes, edges=edges) + + ids = [n["id"] for n in nodes] + assert len(set(ids)) == 2 + assert ids[0] == "node1" + renamed = ids[1] + assert renamed != "node1" + # Edge target follows the rename; references to the untouched sibling + # stay untouched. + assert edges[0]["target"] == renamed + text = nodes[1]["data"]["prompt_template"][0]["text"] + assert f"{{{{#{renamed}.text#}}}}" in text + assert "{{#node1.x#}}" in text + + def test_sanitizes_dots_and_spaces(self): + nodes: list[dict[str, Any]] = [ + {"id": "step.one", "data": {"type": "start", "variables": []}}, + { + "id": "step two", + "data": {"type": "llm", "prompt_template": [{"role": "user", "text": "{{#step two.text#}}"}]}, + }, + ] + edges = [{"id": "e", "source": "step.one", "target": "step two"}] + + WorkflowGenerator._sanitize_node_ids(nodes=nodes, edges=edges) + + assert [n["id"] for n in nodes] == ["stepone", "steptwo"] + assert (edges[0]["source"], edges[0]["target"]) == ("stepone", "steptwo") + assert "{{#steptwo.text#}}" in nodes[1]["data"]["prompt_template"][0]["text"] + + def test_id_with_no_valid_characters_gets_a_fallback(self): + nodes = [ + {"id": "节点", "data": {"type": "start", "variables": []}}, + {"id": "node2", "data": {"type": "end", "outputs": []}}, + ] + edges = [{"id": "e", "source": "节点", "target": "node2"}] + + WorkflowGenerator._sanitize_node_ids(nodes=nodes, edges=edges) + + new_id = nodes[0]["id"] + assert new_id + assert new_id != "节点" + assert edges[0]["source"] == new_id + + +class TestWorkflowGeneratorLayeredLayout: + """ + Top-level layout is computed from topology (longest-path layering), not + array order: branches that run in parallel share a column and stack in + lanes, and a join lands to the right of its deepest input. + """ + + @staticmethod + def _node(node_id: str, node_type: str) -> dict: + return {"id": node_id, "type": "custom", "data": {"type": node_type, "title": node_id}} + + def test_diamond_branches_share_a_column_in_separate_lanes(self): + nodes = [ + self._node("start", "start"), + self._node("branch", "if-else"), + self._node("a", "llm"), + self._node("b", "llm"), + self._node("join", "variable-aggregator"), + ] + edges = [ + {"source": "start", "target": "branch"}, + {"source": "branch", "target": "a"}, + {"source": "branch", "target": "b"}, + {"source": "a", "target": "join"}, + {"source": "b", "target": "join"}, + ] + + WorkflowGenerator._layout_top_level_nodes(nodes=nodes, edges=edges) + + pos = {n["id"]: n["position"] for n in nodes} + assert pos["start"]["x"] < pos["branch"]["x"] < pos["a"]["x"] < pos["join"]["x"] + # The two arms share the column but not the lane. + assert pos["a"]["x"] == pos["b"]["x"] + assert pos["a"]["y"] != pos["b"]["y"] + + def test_out_of_order_node_array_still_flows_left_to_right(self): + # Builder emitted the array end-first; topology must win. + nodes = [ + self._node("end", "end"), + self._node("middle", "llm"), + self._node("start", "start"), + ] + edges = [ + {"source": "start", "target": "middle"}, + {"source": "middle", "target": "end"}, + ] + + WorkflowGenerator._layout_top_level_nodes(nodes=nodes, edges=edges) + + pos = {n["id"]: n["position"] for n in nodes} + assert pos["start"]["x"] < pos["middle"]["x"] < pos["end"]["x"] + + def test_join_lands_right_of_its_deepest_branch(self): + # start → a → b → join, start → join: BFS depth would put join at 1; + # longest-path layering must put it at 3. + nodes = [ + self._node("start", "start"), + self._node("a", "llm"), + self._node("b", "llm"), + self._node("join", "end"), + ] + edges = [ + {"source": "start", "target": "a"}, + {"source": "a", "target": "b"}, + {"source": "b", "target": "join"}, + {"source": "start", "target": "join"}, + ] + + WorkflowGenerator._layout_top_level_nodes(nodes=nodes, edges=edges) + + pos = {n["id"]: n["position"] for n in nodes} + assert pos["join"]["x"] > pos["b"]["x"] > pos["a"]["x"] > pos["start"]["x"] + + def test_container_children_are_not_repositioned(self): + nodes = [ + self._node("start", "start"), + self._node("iter", "iteration"), + { + "id": "inner", + "type": "custom", + "parentId": "iter", + "position": {"x": 60.0, "y": 78.0}, + "data": {"type": "llm", "title": "inner"}, + }, + self._node("end", "end"), + ] + edges = [ + {"source": "start", "target": "iter"}, + {"source": "iter", "target": "end"}, + ] + + WorkflowGenerator._layout_top_level_nodes(nodes=nodes, edges=edges) + + inner = next(n for n in nodes if n["id"] == "inner") + assert inner["position"] == {"x": 60.0, "y": 78.0} + + def test_cycle_members_are_parked_instead_of_hanging(self): + # A cycle must not hang the layout pass; its members get parked one + # layer past the laid-out nodes (validation flags the cycle itself). + nodes = [ + self._node("start", "start"), + self._node("a", "llm"), + self._node("b", "llm"), + ] + edges = [ + {"source": "start", "target": "a"}, + {"source": "a", "target": "b"}, + {"source": "b", "target": "a"}, + ] + + WorkflowGenerator._layout_top_level_nodes(nodes=nodes, edges=edges) + + for node in nodes: + assert "position" in node + + +class TestWorkflowGeneratorBranchHandleRepair: + """ + Edges leaving if-else / question-classifier on the default "source" + handle dangle off a handle that doesn't exist on the canvas. The repair + pass re-homes them onto unused branch handles when (and only when) the + assignment is unambiguous. + """ + + @staticmethod + def _if_else_node() -> dict: + return { + "id": "branch", + "data": { + "type": "if-else", + "cases": [{"case_id": "true", "conditions": []}], + }, + } + + def test_assigns_true_then_false_to_default_handle_edges(self): + nodes = [self._if_else_node()] + edges = [ + {"source": "branch", "target": "a", "sourceHandle": "source"}, + {"source": "branch", "target": "b"}, + ] + + WorkflowGenerator._repair_branch_edge_handles(nodes=nodes, edges=edges) + + assert edges[0]["sourceHandle"] == "true" + assert edges[1]["sourceHandle"] == "false" + + def test_respects_an_already_correct_handle(self): + nodes = [self._if_else_node()] + edges = [ + {"source": "branch", "target": "a", "sourceHandle": "true"}, + {"source": "branch", "target": "b", "sourceHandle": "source"}, + ] + + WorkflowGenerator._repair_branch_edge_handles(nodes=nodes, edges=edges) + + assert edges[0]["sourceHandle"] == "true" + assert edges[1]["sourceHandle"] == "false" + + def test_leaves_ambiguous_assignments_alone(self): + # Three default edges, only two free handles — guessing could swap + # the IF and ELSE arms, so the repair must not touch anything. + nodes = [self._if_else_node()] + edges = [ + {"source": "branch", "target": "a", "sourceHandle": "source"}, + {"source": "branch", "target": "b", "sourceHandle": "source"}, + {"source": "branch", "target": "c", "sourceHandle": "source"}, + ] + + WorkflowGenerator._repair_branch_edge_handles(nodes=nodes, edges=edges) + + assert all(e["sourceHandle"] == "source" for e in edges) + + def test_question_classifier_uses_class_ids(self): + nodes = [ + { + "id": "qc", + "data": { + "type": "question-classifier", + "classes": [{"id": "1", "name": "A"}, {"id": "2", "name": "B"}], + }, + } + ] + edges = [ + {"source": "qc", "target": "a"}, + {"source": "qc", "target": "b"}, + ] + + WorkflowGenerator._repair_branch_edge_handles(nodes=nodes, edges=edges) + + assert edges[0]["sourceHandle"] == "1" + assert edges[1]["sourceHandle"] == "2" + + def test_non_branch_nodes_are_untouched(self): + nodes = [{"id": "llm1", "data": {"type": "llm"}}] + edges = [{"source": "llm1", "target": "end", "sourceHandle": "source"}] + + WorkflowGenerator._repair_branch_edge_handles(nodes=nodes, edges=edges) + + assert edges[0]["sourceHandle"] == "source" + + +class TestWorkflowGeneratorGraphCycleValidation: + """A workflow graph must be a DAG; cycles hang or error the run.""" + + def test_self_loop_is_flagged_with_the_node_id(self): + graph = { + "nodes": [], + "edges": [{"source": "a", "target": "a"}], + "viewport": {"x": 0, "y": 0, "zoom": 0.7}, + } + errors = WorkflowGenerator._collect_edge_cycle_errors(graph=cast(GraphDict, graph), known_ids={"a"}) + assert len(errors) == 1 + assert errors[0]["code"] == "GRAPH_CYCLE" + assert errors[0]["node_id"] == "a" + + def test_two_node_cycle_is_flagged_once(self): + graph = { + "nodes": [], + "edges": [ + {"source": "start", "target": "a"}, + {"source": "a", "target": "b"}, + {"source": "b", "target": "a"}, + ], + "viewport": {"x": 0, "y": 0, "zoom": 0.7}, + } + errors = WorkflowGenerator._collect_edge_cycle_errors( + graph=cast(GraphDict, graph), known_ids={"start", "a", "b"} + ) + assert len(errors) == 1 + assert errors[0]["code"] == "GRAPH_CYCLE" + assert "a" in errors[0]["detail"] + assert "b" in errors[0]["detail"] + + def test_acyclic_graph_produces_no_errors(self): + graph = { + "nodes": [], + "edges": [ + {"source": "start", "target": "a"}, + {"source": "start", "target": "b"}, + {"source": "a", "target": "end"}, + {"source": "b", "target": "end"}, + ], + "viewport": {"x": 0, "y": 0, "zoom": 0.7}, + } + errors = WorkflowGenerator._collect_edge_cycle_errors( + graph=cast(GraphDict, graph), known_ids={"start", "a", "b", "end"} + ) + assert errors == [] + + def test_cyclic_builder_output_surfaces_graph_cycle_code(self): + planner = json.dumps( + { + "title": "t", + "description": "d", + "nodes": [ + {"label": "Start", "node_type": "start", "purpose": "x"}, + {"label": "LLM", "node_type": "llm", "purpose": "x"}, + {"label": "End", "node_type": "end", "purpose": "x"}, + ], + } + ) + builder = json.dumps( + { + "nodes": [ + { + "id": "node1", + "type": "custom", + "position": {"x": 0, "y": 0}, + "data": {"type": "start", "title": "Start", "variables": []}, + }, + { + "id": "node2", + "type": "custom", + "position": {"x": 0, "y": 0}, + "data": {"type": "llm", "title": "LLM"}, + }, + { + "id": "node3", + "type": "custom", + "position": {"x": 0, "y": 0}, + "data": {"type": "end", "title": "End", "outputs": []}, + }, + ], + "edges": [ + {"source": "node1", "target": "node2"}, + {"source": "node2", "target": "node2"}, + {"source": "node2", "target": "node3"}, + ], + "viewport": {"x": 0, "y": 0, "zoom": 0.7}, + } + ) + model_instance = MagicMock() + model_instance.invoke_llm.side_effect = [_llm_result(planner), _llm_result(builder)] + + result = WorkflowGenerator.generate_workflow_graph( + model_instance=model_instance, + model_parameters={}, + provider="openai", + model_name="gpt-4o", + model_mode="chat", + mode="workflow", + instruction="x", + ) + + assert any(e["code"] == "GRAPH_CYCLE" for e in result["errors"]) + + +class TestWorkflowGeneratorDuplicateNodeIds: + """Duplicate ids make every cross-reference ambiguous — fail loudly.""" + + def test_duplicate_ids_surface_dedicated_code(self): + planner = json.dumps( + { + "title": "t", + "description": "d", + "nodes": [ + {"label": "Start", "node_type": "start", "purpose": "x"}, + {"label": "End", "node_type": "end", "purpose": "x"}, + ], + } + ) + builder = json.dumps( + { + "nodes": [ + { + "id": "node1", + "type": "custom", + "position": {"x": 0, "y": 0}, + "data": {"type": "start", "title": "Start", "variables": []}, + }, + { + "id": "node1", + "type": "custom", + "position": {"x": 0, "y": 0}, + "data": {"type": "end", "title": "End", "outputs": []}, + }, + ], + "edges": [{"source": "node1", "target": "node1"}], + "viewport": {"x": 0, "y": 0, "zoom": 0.7}, + } + ) + model_instance = MagicMock() + model_instance.invoke_llm.side_effect = [_llm_result(planner), _llm_result(builder)] + + result = WorkflowGenerator.generate_workflow_graph( + model_instance=model_instance, + model_parameters={}, + provider="openai", + model_name="gpt-4o", + model_mode="chat", + mode="workflow", + instruction="x", + ) + + codes = {e["code"] for e in result["errors"]} + assert "DUPLICATE_NODE_ID" in codes diff --git a/web/app/components/workflow/workflow-generator/__tests__/use-gen-graph.spec.ts b/web/app/components/workflow/workflow-generator/__tests__/use-gen-graph.spec.ts new file mode 100644 index 00000000000..75ba9613d52 --- /dev/null +++ b/web/app/components/workflow/workflow-generator/__tests__/use-gen-graph.spec.ts @@ -0,0 +1,80 @@ +import type { GenerateWorkflowResponse } from '../types' +import { act, renderHook } from '@testing-library/react' +import useGenGraph from '../use-gen-graph' + +const makeVersion = (marker: string): GenerateWorkflowResponse => ({ + graph: { + nodes: [{ id: marker } as never], + edges: [], + viewport: { x: 0, y: 0, zoom: 0.7 }, + }, + message: marker, +}) + +describe('useGenGraph', () => { + beforeEach(() => { + sessionStorage.clear() + }) + + describe('addVersion', () => { + // The selected index must always point at the entry just appended so the + // preview pane shows the generation the user is waiting on. + it('should select the newly added version', () => { + const { result } = renderHook(() => useGenGraph({ storageKey: 'workflow-test' })) + + act(() => { + result.current.addVersion(makeVersion('v1')) + }) + act(() => { + result.current.addVersion(makeVersion('v2')) + }) + + expect(result.current.versions).toHaveLength(2) + expect(result.current.currentVersionIndex).toBe(1) + expect(result.current.current?.message).toBe('v2') + }) + + // Each version embeds a full graph and sessionStorage tops out around + // 5MB — an unbounded history would hit the quota mid-session. Oldest + // entries are dropped, and the index still tracks the latest entry. + it('should cap retained versions and keep the index on the latest', () => { + const { result } = renderHook(() => useGenGraph({ storageKey: 'workflow-test' })) + + for (let i = 0; i < 12; i++) { + act(() => { + result.current.addVersion(makeVersion(`v${i}`)) + }) + } + + expect(result.current.versions).toHaveLength(10) + // Oldest two were evicted; the window is v2..v11. + expect(result.current.versions?.[0]?.message).toBe('v2') + expect(result.current.currentVersionIndex).toBe(9) + expect(result.current.current?.message).toBe('v11') + }) + }) + + describe('currentVersionIndex clamping', () => { + // A stale persisted index (longer history capped, or cleared by another + // tab) must not strand the preview on an undefined entry. + it('should clamp an out-of-bounds persisted index to the last version', () => { + sessionStorage.setItem( + 'workflow-gen-workflow-test-versions', + JSON.stringify([makeVersion('only')]), + ) + sessionStorage.setItem('workflow-gen-workflow-test-version-index', '5') + + const { result } = renderHook(() => useGenGraph({ storageKey: 'workflow-test' })) + + expect(result.current.currentVersionIndex).toBe(0) + expect(result.current.current?.message).toBe('only') + }) + + it('should return index 0 and no current version when history is empty', () => { + const { result } = renderHook(() => useGenGraph({ storageKey: 'workflow-test' })) + + expect(result.current.currentVersionIndex).toBe(0) + expect(result.current.current).toBeUndefined() + }) + }) +}) diff --git a/web/app/components/workflow/workflow-generator/index.tsx b/web/app/components/workflow/workflow-generator/index.tsx index cd0128c2797..e7824ae0d9b 100644 --- a/web/app/components/workflow/workflow-generator/index.tsx +++ b/web/app/components/workflow/workflow-generator/index.tsx @@ -39,7 +39,15 @@ import { useWorkflowGeneratorStore } from './store' import useGenGraph from './use-gen-graph' const STORAGE_MODEL_KEY = 'workflow-gen-model' -const FE_TIMEOUT_MS = 60_000 +// Hard ceiling before we abort a hung request. Generous on purpose: the +// backend runs two sequential LLM calls and may retry a transient provider +// error (bounded backoff) or an unparseable response (one extra call), so a +// slow-but-succeeding generation can legitimately pass the one-minute mark. +// Aborting work that would have landed is the worse failure mode. +const FE_TIMEOUT_MS = 90_000 +// Mirrors the backend's instruction/ideal-output cap on /workflow-generate — +// keeping the limit client-side turns an opaque 400 into a visible input stop. +const MAX_INSTRUCTION_LENGTH = 10_000 // Stable default used both as the SSR/empty-storage seed for the persisted // model and as the merge base when patching a partial update. Module-level so @@ -264,7 +272,9 @@ const WorkflowGeneratorModal: React.FC = () => { // instead of starting from scratch. The modal mounts outside the Studio's // ReactFlow provider, so we read the persisted draft rather than the live // canvas. A fetch failure (no draft saved yet) degrades gracefully to a - // from-scratch generation — better than blocking the user entirely. + // from-scratch generation — better than blocking the user entirely — but + // the user asked to REFINE, so tell them their draft isn't being used + // instead of silently generating something unrelated. let currentGraph: Awaited>['graph'] | undefined if (isRefine && currentAppId) { try { @@ -275,6 +285,8 @@ const WorkflowGeneratorModal: React.FC = () => { catch { currentGraph = undefined } + if (!currentGraph) + toast.warning(t('workflowGenerator.refineDraftUnavailable')) } const res = await generateWorkflow({ @@ -300,6 +312,13 @@ const WorkflowGeneratorModal: React.FC = () => { toast.error(res.error) return } + if (!res.graph?.nodes?.length) { + // Defensive: a success envelope with an empty graph should never + // leave the backend, but if it does, an empty "version" would just + // pollute the selector with a blank preview. + toast.error(t('workflowGenerator.generateFailed')) + return + } addVersion(res) } catch (e: unknown) { @@ -452,6 +471,7 @@ const WorkflowGeneratorModal: React.FC = () => { : t('workflowGenerator.instructionPlaceholder')} value={instruction} onValueChange={setInstruction} + maxLength={MAX_INSTRUCTION_LENGTH} /> {/* Example prompts are create-from-scratch starters ("Summarize a diff --git a/web/app/components/workflow/workflow-generator/use-gen-graph.ts b/web/app/components/workflow/workflow-generator/use-gen-graph.ts index 797e2d3379c..f8fe8e8a8a4 100644 --- a/web/app/components/workflow/workflow-generator/use-gen-graph.ts +++ b/web/app/components/workflow/workflow-generator/use-gen-graph.ts @@ -1,9 +1,15 @@ import type { GenerateWorkflowResponse } from './types' import { useSessionStorageState } from 'ahooks' -import { useCallback } from 'react' +import { useCallback, useEffect, useRef } from 'react' const KEY_PREFIX = 'workflow-gen-' +// Upper bound on retained generations. Each version embeds a full graph +// (tens of KB for a large refine), and sessionStorage offers ~5MB for the +// whole origin — an unbounded history can hit the quota mid-session and +// silently stop persisting. Ten comfortably covers "compare a few attempts". +const MAX_VERSIONS = 10 + type Params = { storageKey: string } @@ -26,17 +32,33 @@ const useGenGraph = ({ storageKey }: Params) => { { defaultValue: 0 }, ) - const current = versions?.[currentVersionIndex ?? 0] + // Clamp the persisted index into bounds — sessionStorage can hold an index + // from a longer, since-capped history (or one cleared by another tab). + const safeIndex = Math.min(currentVersionIndex ?? 0, Math.max((versions?.length ?? 0) - 1, 0)) + const current = versions?.[safeIndex] + + // Version count including adds React hasn't committed yet. addVersion can + // run twice inside one batch (the functional setVersions appends both), so + // the selected-index math must not read `versions` from the render closure + // — it would be one add behind and select the wrong entry. + const versionCountRef = useRef(versions?.length ?? 0) + useEffect(() => { + versionCountRef.current = versions?.length ?? 0 + }, [versions]) const addVersion = useCallback((version: GenerateWorkflowResponse) => { - setCurrentVersionIndex(() => versions?.length || 0) - setVersions(prev => [...(prev ?? []), version]) - }, [setVersions, setCurrentVersionIndex, versions?.length]) + const nextCount = Math.min(versionCountRef.current + 1, MAX_VERSIONS) + versionCountRef.current = nextCount + // Functional update so batched adds append instead of clobbering each + // other; the slice keeps the retained history under the cap. + setVersions(prev => [...(prev ?? []), version].slice(-MAX_VERSIONS)) + setCurrentVersionIndex(nextCount - 1) + }, [setVersions, setCurrentVersionIndex]) return { versions, addVersion, - currentVersionIndex, + currentVersionIndex: safeIndex, setCurrentVersionIndex, current, } diff --git a/web/i18n/ar-TN/workflow.json b/web/i18n/ar-TN/workflow.json index f5ad90ca62b..fcfeba0ccd8 100644 --- a/web/i18n/ar-TN/workflow.json +++ b/web/i18n/ar-TN/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "قم بوصف ما تريد أن يفعله سير العمل. اختر نموذجًا، واكتب تعليمات، وقم بمعاينة الرسم البياني الذي تم إنشاؤه قبل تطبيقه على الاستوديو.", "workflowGenerator.dismiss": "استبعاد", "workflowGenerator.errors.DANGLING_EDGE": "يحتوي سير العمل الذي تم إنشاؤه على حافة تشير إلى عقدة غير موجودة. حاول مرة أخرى أو قم بتحسين تعليماتك.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "يحتوي سير العمل المُنشأ على معرّفات عقد مكررة. حاول إعادة الإنشاء.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "يرجى كتابة التعليمات أولا.", "workflowGenerator.errors.EMPTY_PLAN": "أعاد النموذج خطة فارغة. جرب تعليمات أكثر تحديدا.", + "workflowGenerator.errors.GRAPH_CYCLE": "يحتوي سير العمل المُنشأ على اتصال دائري. حاول إعادة الإنشاء أو تبسيط التعليمات.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "التعليمات طويلة جدًا. اختصرها وحاول مرة أخرى.", "workflowGenerator.errors.INVALID_CONTAINER": "يحتوي سير العمل الذي تم إنشاؤه على حلقة أو حاوية تكرار تالفة. جرب تعليمات أبسط أو اختر نموذجًا أكثر قدرة.", "workflowGenerator.errors.INVALID_JSON": "أعاد النموذج استجابة لم نتمكن من تحليلها. حاول مرة أخرى أو اختر طرازًا أكثر قدرة.", "workflowGenerator.errors.INVALID_SCHEMA": "أعاد النموذج رسمًا بيانيًا بشكل غير متوقع. حاول مرة أخرى أو اختر طرازًا أكثر قدرة.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "جارٍ التحقق من صحة الرسم البياني...", "workflowGenerator.placeholder": "اكتب تعليمات على اليسار، ثم انقر فوق \"إنشاء\" لمعاينة الرسم البياني لسير العمل.", "workflowGenerator.refineDescription": "قم بوصف التغيير الذي تريده. يتم استخدام المسودة الحالية كسياق؛ يستبدل الرسم البياني الذي تم إنشاؤه عند التقديم.", + "workflowGenerator.refineDraftUnavailable": "تعذّر تحميل المسودة الحالية — سيتم الإنشاء من الصفر بدلاً من ذلك.", "workflowGenerator.refineInstructionPlaceholder": "وصف التغيير - على سبيل المثال أضف خطوة ترجمة، وقم بالتبديل إلى أداة، وأضف معالجة الأخطاء.", "workflowGenerator.refineTitle": "تنقيح {{mode}}", "workflowGenerator.reload": "إعادة تحميل", diff --git a/web/i18n/de-DE/workflow.json b/web/i18n/de-DE/workflow.json index 82e9966dfef..7d3bdf3fd22 100644 --- a/web/i18n/de-DE/workflow.json +++ b/web/i18n/de-DE/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Beschreiben Sie, was der Workflow bewirken soll. Wählen Sie ein Modell aus, schreiben Sie eine Anweisung und zeigen Sie eine Vorschau des generierten Diagramms an, bevor Sie es auf Studio anwenden.", "workflowGenerator.dismiss": "Entlassen", "workflowGenerator.errors.DANGLING_EDGE": "Der generierte Workflow weist eine Kante auf, die auf einen Knoten zeigt, der nicht vorhanden ist. Versuchen Sie es noch einmal oder verfeinern Sie Ihre Anleitung.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Der generierte Workflow enthält doppelte Knoten-IDs. Versuchen Sie, ihn neu zu generieren.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Bitte schreiben Sie zunächst eine Anleitung.", "workflowGenerator.errors.EMPTY_PLAN": "Das Modell hat einen leeren Plan zurückgegeben. Versuchen Sie es mit einer spezifischeren Anweisung.", + "workflowGenerator.errors.GRAPH_CYCLE": "Der generierte Workflow enthält eine zirkuläre Verbindung. Generieren Sie neu oder vereinfachen Sie Ihre Anweisung.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "Die Anweisung ist zu lang. Kürzen Sie sie und versuchen Sie es erneut.", "workflowGenerator.errors.INVALID_CONTAINER": "Der generierte Workflow weist eine fehlerhafte Schleife oder einen fehlerhaften Iterationscontainer auf. Versuchen Sie es mit einer einfacheren Anleitung oder wählen Sie ein leistungsfähigeres Modell.", "workflowGenerator.errors.INVALID_JSON": "Das Modell hat eine Antwort zurückgegeben, die wir nicht analysieren konnten. Versuchen Sie es erneut oder wählen Sie ein leistungsfähigeres Modell.", "workflowGenerator.errors.INVALID_SCHEMA": "Das Modell hat ein Diagramm in einer unerwarteten Form zurückgegeben. Versuchen Sie es erneut oder wählen Sie ein leistungsfähigeres Modell.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Das Diagramm wird validiert…", "workflowGenerator.placeholder": "Schreiben Sie links eine Anweisung und klicken Sie dann auf „Generieren“, um eine Vorschau des Workflow-Diagramms anzuzeigen.", "workflowGenerator.refineDescription": "Beschreiben Sie die gewünschte Änderung. Als Kontext dient der aktuelle Entwurf; Das generierte Diagramm ersetzt es, wenn Sie es anwenden.", + "workflowGenerator.refineDraftUnavailable": "Der aktuelle Entwurf konnte nicht geladen werden — es wird stattdessen von Grund auf neu generiert.", "workflowGenerator.refineInstructionPlaceholder": "Beschreiben Sie die Änderung – z.B. Fügen Sie einen Übersetzungsschritt hinzu, wechseln Sie zu einem Tool und fügen Sie eine Fehlerbehandlung hinzu.", "workflowGenerator.refineTitle": "Verfeinern {{mode}}", "workflowGenerator.reload": "Neu laden", diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 1ba4095a60b..52b79654fdc 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Describe what you want the workflow to do. Pick a model, write an instruction, and preview the generated graph before applying it to Studio.", "workflowGenerator.dismiss": "Dismiss", "workflowGenerator.errors.DANGLING_EDGE": "The generated workflow has an edge pointing at a node that doesn't exist. Try again or refine your instruction.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "The generated workflow contains duplicate node IDs. Try regenerating.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Please write an instruction first.", "workflowGenerator.errors.EMPTY_PLAN": "The model returned an empty plan. Try a more specific instruction.", + "workflowGenerator.errors.GRAPH_CYCLE": "The generated workflow contains a circular connection. Try regenerating or simplifying your instruction.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "The instruction is too long. Shorten it and try again.", "workflowGenerator.errors.INVALID_CONTAINER": "The generated workflow has a malformed loop or iteration container. Try a simpler instruction or pick a more capable model.", "workflowGenerator.errors.INVALID_JSON": "The model returned a response we couldn't parse. Try again or pick a more capable model.", "workflowGenerator.errors.INVALID_SCHEMA": "The model returned a graph in an unexpected shape. Try again or pick a more capable model.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Validating the graph…", "workflowGenerator.placeholder": "Write an instruction on the left, then click Generate to preview the workflow graph.", "workflowGenerator.refineDescription": "Describe the change you want. The current draft is used as context; the generated graph replaces it when you apply.", + "workflowGenerator.refineDraftUnavailable": "Couldn't load the current draft — generating from scratch instead.", "workflowGenerator.refineInstructionPlaceholder": "Describe the change — e.g. add a translation step, switch to a tool, add error handling.", "workflowGenerator.refineTitle": "Refine {{mode}}", "workflowGenerator.reload": "Reload", diff --git a/web/i18n/es-ES/workflow.json b/web/i18n/es-ES/workflow.json index 1bde56032c3..d205d96e617 100644 --- a/web/i18n/es-ES/workflow.json +++ b/web/i18n/es-ES/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Describe lo que quieres que haga el flujo de trabajo. Elija un modelo, escriba una instrucción y obtenga una vista previa del gráfico generado antes de aplicarlo a Studio.", "workflowGenerator.dismiss": "Descartar", "workflowGenerator.errors.DANGLING_EDGE": "El flujo de trabajo generado tiene un borde que apunta a un nodo que no existe. Inténtalo de nuevo o mejora tus instrucciones.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "El flujo de trabajo generado contiene IDs de nodo duplicados. Intenta regenerarlo.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Primero escriba una instrucción.", "workflowGenerator.errors.EMPTY_PLAN": "La modelo devolvió un plano vacío. Pruebe con una instrucción más específica.", + "workflowGenerator.errors.GRAPH_CYCLE": "El flujo de trabajo generado contiene una conexión circular. Intenta regenerarlo o simplifica tu instrucción.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "La instrucción es demasiado larga. Acórtala e inténtalo de nuevo.", "workflowGenerator.errors.INVALID_CONTAINER": "El flujo de trabajo generado tiene un bucle o contenedor de iteración con formato incorrecto. Pruebe con una instrucción más simple o elija un modelo más capaz.", "workflowGenerator.errors.INVALID_JSON": "El modelo devolvió una respuesta que no pudimos analizar. Inténtalo de nuevo o elige un modelo más capaz.", "workflowGenerator.errors.INVALID_SCHEMA": "El modelo arrojó un gráfico con una forma inesperada. Inténtalo de nuevo o elige un modelo más capaz.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Validando el gráfico…", "workflowGenerator.placeholder": "Escriba una instrucción a la izquierda y luego haga clic en Generar para obtener una vista previa del gráfico de flujo de trabajo.", "workflowGenerator.refineDescription": "Describe el cambio que deseas. El borrador actual se utiliza como contexto; el gráfico generado lo reemplaza cuando aplica.", + "workflowGenerator.refineDraftUnavailable": "No se pudo cargar el borrador actual; se generará desde cero.", "workflowGenerator.refineInstructionPlaceholder": "Describe el cambio, p.e. agregue un paso de traducción, cambie a una herramienta, agregue manejo de errores.", "workflowGenerator.refineTitle": "Refinar {{mode}}", "workflowGenerator.reload": "recargar", diff --git a/web/i18n/fa-IR/workflow.json b/web/i18n/fa-IR/workflow.json index 8a0d71095dd..9fbe2d7b3cc 100644 --- a/web/i18n/fa-IR/workflow.json +++ b/web/i18n/fa-IR/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "آنچه را که می خواهید گردش کار انجام دهد را شرح دهید. یک مدل را انتخاب کنید، یک دستورالعمل بنویسید، و پیش نمایش نمودار تولید شده را قبل از اعمال آن در استودیو مشاهده کنید.", "workflowGenerator.dismiss": "رد کردن", "workflowGenerator.errors.DANGLING_EDGE": "گردش کار تولید شده دارای لبه ای است که به گره ای اشاره می کند که وجود ندارد. دوباره امتحان کنید یا دستورالعمل خود را اصلاح کنید.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "گردش‌کار تولیدشده شامل شناسه‌های گره تکراری است. دوباره تولید کنید.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "لطفا اول یک دستورالعمل بنویسید", "workflowGenerator.errors.EMPTY_PLAN": "مدل یک طرح خالی را برگرداند. یک دستورالعمل خاص تر را امتحان کنید.", + "workflowGenerator.errors.GRAPH_CYCLE": "گردش‌کار تولیدشده شامل اتصال حلقه‌ای است. دوباره تولید کنید یا دستور خود را ساده‌تر کنید.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "دستور بیش از حد طولانی است. آن را کوتاه کنید و دوباره تلاش کنید.", "workflowGenerator.errors.INVALID_CONTAINER": "گردش کار ایجاد شده دارای یک حلقه یا ظرف تکرار نادرست است. یک دستورالعمل ساده‌تر را امتحان کنید یا مدل توانمندتری را انتخاب کنید.", "workflowGenerator.errors.INVALID_JSON": "مدل پاسخی را برگرداند که ما نتوانستیم آن را تجزیه کنیم. دوباره امتحان کنید یا مدل توانمندتری را انتخاب کنید.", "workflowGenerator.errors.INVALID_SCHEMA": "مدل یک نمودار را به شکل غیرمنتظره ای برگرداند. دوباره امتحان کنید یا مدل توانمندتری را انتخاب کنید.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "در حال اعتبارسنجی نمودار…", "workflowGenerator.placeholder": "یک دستورالعمل در سمت چپ بنویسید، سپس روی Generate کلیک کنید تا نمودار گردش کار را پیش‌نمایش ببینید.", "workflowGenerator.refineDescription": "تغییری را که می خواهید توضیح دهید. پیش نویس فعلی به عنوان زمینه استفاده می شود. هنگامی که شما درخواست می کنید، نمودار تولید شده جایگزین آن می شود.", + "workflowGenerator.refineDraftUnavailable": "بارگیری پیش‌نویس فعلی ممکن نشد — در عوض از ابتدا تولید می‌شود.", "workflowGenerator.refineInstructionPlaceholder": "تغییر را توصیف کنید - به عنوان مثال یک مرحله ترجمه اضافه کنید، به یک ابزار بروید، مدیریت خطا را اضافه کنید.", "workflowGenerator.refineTitle": "اصلاح {{mode}}", "workflowGenerator.reload": "بارگذاری مجدد", diff --git a/web/i18n/fr-FR/workflow.json b/web/i18n/fr-FR/workflow.json index e4f9e3f9edc..8cb68762575 100644 --- a/web/i18n/fr-FR/workflow.json +++ b/web/i18n/fr-FR/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Décrivez ce que vous souhaitez que le flux de travail fasse. Choisissez un modèle, rédigez une instruction et prévisualisez le graphique généré avant de l'appliquer à Studio.", "workflowGenerator.dismiss": "Rejeter", "workflowGenerator.errors.DANGLING_EDGE": "Le flux de travail généré comporte un bord pointant vers un nœud qui n'existe pas. Réessayez ou affinez vos instructions.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Le workflow généré contient des ID de nœud en double. Essayez de le régénérer.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Veuillez d'abord rédiger une instruction.", "workflowGenerator.errors.EMPTY_PLAN": "Le modèle a renvoyé un plan vide. Essayez une instruction plus spécifique.", + "workflowGenerator.errors.GRAPH_CYCLE": "Le workflow généré contient une connexion circulaire. Régénérez-le ou simplifiez votre instruction.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "L'instruction est trop longue. Raccourcissez-la et réessayez.", "workflowGenerator.errors.INVALID_CONTAINER": "Le flux de travail généré comporte une boucle ou un conteneur d'itération mal formé. Essayez une instruction plus simple ou choisissez un modèle plus performant.", "workflowGenerator.errors.INVALID_JSON": "Le modèle a renvoyé une réponse que nous n'avons pas pu analyser. Réessayez ou choisissez un modèle plus performant.", "workflowGenerator.errors.INVALID_SCHEMA": "Le modèle a renvoyé un graphique sous une forme inattendue. Réessayez ou choisissez un modèle plus performant.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Validation du graphique…", "workflowGenerator.placeholder": "Écrivez une instruction sur la gauche, puis cliquez sur Générer pour prévisualiser le graphique du flux de travail.", "workflowGenerator.refineDescription": "Décrivez le changement que vous souhaitez. Le projet actuel est utilisé comme contexte ; le graphique généré le remplace lorsque vous postulez.", + "workflowGenerator.refineDraftUnavailable": "Impossible de charger le brouillon actuel — génération à partir de zéro à la place.", "workflowGenerator.refineInstructionPlaceholder": "Décrivez le changement — par ex. ajouter une étape de traduction, passer un outil, ajouter une gestion des erreurs.", "workflowGenerator.refineTitle": "Affiner {{mode}}", "workflowGenerator.reload": "Recharger", diff --git a/web/i18n/hi-IN/workflow.json b/web/i18n/hi-IN/workflow.json index f4cf49caee7..09565b9a974 100644 --- a/web/i18n/hi-IN/workflow.json +++ b/web/i18n/hi-IN/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "वर्णन करें कि आप वर्कफ़्लो से क्या कराना चाहते हैं. एक मॉडल चुनें, एक निर्देश लिखें और स्टूडियो पर लागू करने से पहले जेनरेट किए गए ग्राफ़ का पूर्वावलोकन करें।", "workflowGenerator.dismiss": "ख़ारिज करें", "workflowGenerator.errors.DANGLING_EDGE": "जेनरेट किए गए वर्कफ़्लो में एक किनारा उस नोड की ओर इशारा करता है जो मौजूद नहीं है। पुनः प्रयास करें या अपने निर्देश को परिष्कृत करें।", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "जनरेट किए गए वर्कफ़्लो में डुप्लिकेट नोड ID हैं। फिर से जनरेट करने का प्रयास करें।", "workflowGenerator.errors.EMPTY_INSTRUCTION": "कृपया पहले एक निर्देश लिखें.", "workflowGenerator.errors.EMPTY_PLAN": "मॉडल ने एक खाली योजना लौटा दी. अधिक विशिष्ट अनुदेश का प्रयास करें.", + "workflowGenerator.errors.GRAPH_CYCLE": "जनरेट किए गए वर्कफ़्लो में एक चक्रीय कनेक्शन है। फिर से जनरेट करें या अपने निर्देश को सरल बनाएं।", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "निर्देश बहुत लंबा है। इसे छोटा करके फिर से प्रयास करें।", "workflowGenerator.errors.INVALID_CONTAINER": "जेनरेट किए गए वर्कफ़्लो में एक विकृत लूप या पुनरावृत्ति कंटेनर है। एक सरल निर्देश आज़माएँ या अधिक सक्षम मॉडल चुनें।", "workflowGenerator.errors.INVALID_JSON": "मॉडल ने एक प्रतिक्रिया दी जिसे हम पार्स नहीं कर सके। पुनः प्रयास करें या अधिक सक्षम मॉडल चुनें।", "workflowGenerator.errors.INVALID_SCHEMA": "मॉडल ने एक ग्राफ़ को अप्रत्याशित आकार में लौटाया। पुनः प्रयास करें या अधिक सक्षम मॉडल चुनें।", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "ग्राफ़ का सत्यापन किया जा रहा है...", "workflowGenerator.placeholder": "बाईं ओर एक निर्देश लिखें, फिर वर्कफ़्लो ग्राफ़ का पूर्वावलोकन करने के लिए जेनरेट पर क्लिक करें।", "workflowGenerator.refineDescription": "आप जो परिवर्तन चाहते हैं उसका वर्णन करें. वर्तमान मसौदे को संदर्भ के रूप में उपयोग किया जाता है; जब आप आवेदन करते हैं तो उत्पन्न ग्राफ़ इसे बदल देता है।", + "workflowGenerator.refineDraftUnavailable": "वर्तमान ड्राफ़्ट लोड नहीं हो सका — इसके बजाय शुरुआत से जनरेट किया जा रहा है।", "workflowGenerator.refineInstructionPlaceholder": "परिवर्तन का वर्णन करें - उदा. अनुवाद चरण जोड़ें, टूल पर स्विच करें, त्रुटि प्रबंधन जोड़ें।", "workflowGenerator.refineTitle": "परिष्कृत करें {{mode}}", "workflowGenerator.reload": "पुनः लोड करें", diff --git a/web/i18n/id-ID/workflow.json b/web/i18n/id-ID/workflow.json index 3ee4a0a0cc2..99cd1968112 100644 --- a/web/i18n/id-ID/workflow.json +++ b/web/i18n/id-ID/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Jelaskan apa yang Anda ingin alur kerja lakukan. Pilih model, tulis instruksi, dan pratinjau grafik yang dihasilkan sebelum menerapkannya ke Studio.", "workflowGenerator.dismiss": "Singkirkan", "workflowGenerator.errors.DANGLING_EDGE": "Alur kerja yang dihasilkan memiliki tepi yang menunjuk pada simpul yang tidak ada. Coba lagi atau perbaiki instruksi Anda.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Alur kerja yang dihasilkan berisi ID node duplikat. Coba buat ulang.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Silakan tulis instruksi terlebih dahulu.", "workflowGenerator.errors.EMPTY_PLAN": "Model mengembalikan rencana kosong. Coba instruksi yang lebih spesifik.", + "workflowGenerator.errors.GRAPH_CYCLE": "Alur kerja yang dihasilkan berisi koneksi melingkar. Coba buat ulang atau sederhanakan instruksi Anda.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "Instruksi terlalu panjang. Persingkat lalu coba lagi.", "workflowGenerator.errors.INVALID_CONTAINER": "Alur kerja yang dihasilkan memiliki bentuk loop atau kontainer iterasi yang salah. Cobalah instruksi yang lebih sederhana atau pilih model yang lebih mumpuni.", "workflowGenerator.errors.INVALID_JSON": "Model tersebut mengembalikan respons yang tidak dapat kami uraikan. Coba lagi atau pilih model yang lebih mumpuni.", "workflowGenerator.errors.INVALID_SCHEMA": "Model tersebut mengembalikan grafik dalam bentuk yang tidak terduga. Coba lagi atau pilih model yang lebih mumpuni.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Memvalidasi grafik…", "workflowGenerator.placeholder": "Tulis instruksi di sebelah kiri, lalu klik Hasilkan untuk melihat pratinjau grafik alur kerja.", "workflowGenerator.refineDescription": "Jelaskan perubahan yang Anda inginkan. Draf yang ada saat ini digunakan sebagai konteks; grafik yang dihasilkan menggantikannya saat Anda melamar.", + "workflowGenerator.refineDraftUnavailable": "Tidak dapat memuat draf saat ini — membuat dari awal sebagai gantinya.", "workflowGenerator.refineInstructionPlaceholder": "Jelaskan perubahannya — mis. tambahkan langkah terjemahan, beralih ke alat, tambahkan penanganan kesalahan.", "workflowGenerator.refineTitle": "Sempurnakan {{mode}}", "workflowGenerator.reload": "Muat ulang", diff --git a/web/i18n/it-IT/workflow.json b/web/i18n/it-IT/workflow.json index 60bcfb518cb..853ceb3dfa5 100644 --- a/web/i18n/it-IT/workflow.json +++ b/web/i18n/it-IT/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Descrivi cosa vuoi che faccia il flusso di lavoro. Scegli un modello, scrivi un'istruzione e visualizza l'anteprima del grafico generato prima di applicarlo a Studio.", "workflowGenerator.dismiss": "Ignora", "workflowGenerator.errors.DANGLING_EDGE": "Il flusso di lavoro generato ha un bordo che punta a un nodo che non esiste. Riprova o perfeziona le tue istruzioni.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Il workflow generato contiene ID di nodo duplicati. Prova a rigenerarlo.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Si prega di scrivere prima un'istruzione.", "workflowGenerator.errors.EMPTY_PLAN": "Il modello ha restituito un piano vuoto. Prova un'istruzione più specifica.", + "workflowGenerator.errors.GRAPH_CYCLE": "Il workflow generato contiene una connessione circolare. Prova a rigenerarlo o semplifica l'istruzione.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "L'istruzione è troppo lunga. Accorciala e riprova.", "workflowGenerator.errors.INVALID_CONTAINER": "Il flusso di lavoro generato presenta un ciclo o un contenitore di iterazione non valido. Prova un'istruzione più semplice o scegli un modello più potente.", "workflowGenerator.errors.INVALID_JSON": "Il modello ha restituito una risposta che non è stato possibile analizzare. Riprova o scegli un modello più potente.", "workflowGenerator.errors.INVALID_SCHEMA": "Il modello ha restituito un grafico con una forma inaspettata. Riprova o scegli un modello più potente.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Convalida del grafico…", "workflowGenerator.placeholder": "Scrivi un'istruzione a sinistra, quindi fai clic su Genera per visualizzare in anteprima il grafico del flusso di lavoro.", "workflowGenerator.refineDescription": "Descrivi il cambiamento che desideri. La bozza attuale viene utilizzata come contesto; il grafico generato lo sostituisce quando si applica.", + "workflowGenerator.refineDraftUnavailable": "Impossibile caricare la bozza corrente: la generazione partirà da zero.", "workflowGenerator.refineInstructionPlaceholder": "Descrivi il cambiamento – ad es. aggiungere una fase di traduzione, passare a uno strumento, aggiungere la gestione degli errori.", "workflowGenerator.refineTitle": "Perfeziona {{mode}}", "workflowGenerator.reload": "Ricarica", diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/ja-JP/workflow.json index baed1c62ff8..938d1b1567e 100644 --- a/web/i18n/ja-JP/workflow.json +++ b/web/i18n/ja-JP/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "ワークフローで実行したいことを説明します。モデルを選択し、命令を記述し、生成されたグラフを Studio に適用する前にプレビューします。", "workflowGenerator.dismiss": "解雇する", "workflowGenerator.errors.DANGLING_EDGE": "生成されたワークフローには、存在しないノードを指すエッジがあります。もう一度お試しいただくか、手順を絞り込んでください。", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "生成されたワークフローに重複するノード ID が含まれています。再生成してください。", "workflowGenerator.errors.EMPTY_INSTRUCTION": "まずは説明書を書いてください。", "workflowGenerator.errors.EMPTY_PLAN": "モデルは空の計画を返しました。より具体的な手順をお試しください。", + "workflowGenerator.errors.GRAPH_CYCLE": "生成されたワークフローに循環する接続が含まれています。再生成するか、指示を簡素化してください。", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "指示が長すぎます。短くしてからもう一度お試しください。", "workflowGenerator.errors.INVALID_CONTAINER": "生成されたワークフローには、不正な形式のループまたは反復コンテナがあります。より簡単な手順を試すか、より有能なモデルを選択してください。", "workflowGenerator.errors.INVALID_JSON": "モデルは、解析できなかった応答を返しました。もう一度試すか、より機能的なモデルを選択してください。", "workflowGenerator.errors.INVALID_SCHEMA": "モデルは予期しない形でグラフを返しました。もう一度試すか、より機能的なモデルを選択してください。", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "グラフを検証中…", "workflowGenerator.placeholder": "左側に指示を書き、「生成」をクリックしてワークフロー グラフをプレビューします。", "workflowGenerator.refineDescription": "ご希望の変更点を説明してください。現在の下書きはコンテキストとして使用されます。適用すると、生成されたグラフが置き換わります。", + "workflowGenerator.refineDraftUnavailable": "現在のドラフトを読み込めませんでした。最初から生成します。", "workflowGenerator.refineInstructionPlaceholder": "変更点を説明してください。たとえば、翻訳ステップの追加、ツールへの切り替え、エラー処理の追加などです。", "workflowGenerator.refineTitle": "{{mode}} を絞り込む", "workflowGenerator.reload": "リロード", diff --git a/web/i18n/ko-KR/workflow.json b/web/i18n/ko-KR/workflow.json index 44af1f27d34..44373a0ee4c 100644 --- a/web/i18n/ko-KR/workflow.json +++ b/web/i18n/ko-KR/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "워크플로에서 수행하생성된 그래프를 스튜디오는 작업을 설명하세요 모델을 택하 지침을 작성하(에Studio용하기)에 미리 봅니다.", "workflowGenerator.dismiss": "닫기", "workflowGenerator.errors.DANGLING_EDGE": "생성된 워크플로에는 존재하지 않는 노드를 가리키는 에지가 있습니다. 다시 시도하거나 지침을 수정하세요.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "생성된 워크플로에 중복된 노드 ID가 있습니다. 다시 생성해 보세요.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "먼저 지침을 작성하십시오.", "workflowGenerator.errors.EMPTY_PLAN": "모델이 빈 계획을 반환했습니다. 더 구체적인 지침을 시도하십시오.", + "workflowGenerator.errors.GRAPH_CYCLE": "생성된 워크플로에 순환 연결이 있습니다. 다시 생성하거나 지시를 단순화해 보세요.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "지시가 너무 깁니다. 줄인 후 다시 시도하세요.", "workflowGenerator.errors.INVALID_CONTAINER": "생성된 워크플로에 잘못된 형식의 루프 또는 반복 컨테이너가 있습니다. 더 간단한 지침을 시도하거나 더 유능한 모델을 선택하십시오.", "workflowGenerator.errors.INVALID_JSON": "모델이 분석할 수 없는 응답을 반환했습니다. 다시 시도하거나 더 유능한 모델을 선택하세요.", "workflowGenerator.errors.INVALID_SCHEMA": "모델이 예상치 못한 모양의 그래프를 반환했습니다. 다시 시도하거나 더 유능한 모델을 선택하세요.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "그래프 검증 중…", "workflowGenerator.placeholder": "왼쪽에 지침을 작성한 다음 생성을 클릭하여 워크플로 그래프를 미리 봅니다.", "workflowGenerator.refineDescription": "원하는 변경 사항을 설명해 주세요. 현재 초안은 컨텍스트로 사용되며, 적용할 때 생성된 그래프가 이를 대체합니다.", + "workflowGenerator.refineDraftUnavailable": "현재 초안을 불러올 수 없어 처음부터 생성합니다.", "workflowGenerator.refineInstructionPlaceholder": "변경 사항 설명 — 예: 번역 단계 추가, 도구로 전환, 오류 처리 추가.", "workflowGenerator.refineTitle": "{{mode}} 세부 조정", "workflowGenerator.reload": "재장전", diff --git a/web/i18n/nl-NL/workflow.json b/web/i18n/nl-NL/workflow.json index 76631b87d14..2dcf6ade439 100644 --- a/web/i18n/nl-NL/workflow.json +++ b/web/i18n/nl-NL/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Beschrijf wat u wilt dat de workflow doet. Kies een model, schrijf een instructie en bekijk een voorbeeld van de gegenereerde grafiek voordat u deze in Studio toepast.", "workflowGenerator.dismiss": "Negeren", "workflowGenerator.errors.DANGLING_EDGE": "De gegenereerde werkstroom heeft een rand die naar een knooppunt wijst dat niet bestaat. Probeer het opnieuw of verfijn uw instructie.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "De gegenereerde workflow bevat dubbele node-ID's. Probeer opnieuw te genereren.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Schrijf eerst een instructie.", "workflowGenerator.errors.EMPTY_PLAN": "Het model retourneerde een leeg plan. Probeer een meer specifieke instructie.", + "workflowGenerator.errors.GRAPH_CYCLE": "De gegenereerde workflow bevat een circulaire verbinding. Genereer opnieuw of vereenvoudig je instructie.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "De instructie is te lang. Kort deze in en probeer het opnieuw.", "workflowGenerator.errors.INVALID_CONTAINER": "De gegenereerde werkstroom heeft een onjuist opgemaakte lus- of iteratiecontainer. Probeer een eenvoudiger instructie of kies een geschikter model.", "workflowGenerator.errors.INVALID_JSON": "Het model retourneerde een antwoord dat we niet konden parseren. Probeer het opnieuw of kies een geschikter model.", "workflowGenerator.errors.INVALID_SCHEMA": "Het model retourneerde een grafiek in een onverwachte vorm. Probeer het opnieuw of kies een geschikter model.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "De grafiek valideren...", "workflowGenerator.placeholder": "Schrijf een instructie aan de linkerkant en klik vervolgens op Genereer om een voorbeeld van de workflowgrafiek te bekijken.", "workflowGenerator.refineDescription": "Beschrijf de gewenste verandering. Het huidige concept wordt gebruikt als context; de gegenereerde grafiek vervangt deze wanneer u deze toepast.", + "workflowGenerator.refineDraftUnavailable": "Kan het huidige concept niet laden — er wordt vanaf nul gegenereerd.", "workflowGenerator.refineInstructionPlaceholder": "Beschrijf de verandering — b.v. voeg een vertaalstap toe, schakel over naar een tool, voeg foutafhandeling toe.", "workflowGenerator.refineTitle": "Verfijn {{mode}}", "workflowGenerator.reload": "Herladen", diff --git a/web/i18n/pl-PL/workflow.json b/web/i18n/pl-PL/workflow.json index 75bfaef03b4..36aee9c524a 100644 --- a/web/i18n/pl-PL/workflow.json +++ b/web/i18n/pl-PL/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Opisz, co chcesz zrobić w ramach przepływu pracy. Wybierz model, napisz instrukcję i wyświetl podgląd wygenerowanego wykresu przed zastosowaniem go w Studio.", "workflowGenerator.dismiss": "Odrzuć", "workflowGenerator.errors.DANGLING_EDGE": "Wygenerowany przepływ pracy ma krawędź wskazującą na węzeł, który nie istnieje. Spróbuj ponownie lub doprecyzuj instrukcję.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Wygenerowany przepływ pracy zawiera zduplikowane identyfikatory węzłów. Spróbuj wygenerować ponownie.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Najpierw napisz instrukcję.", "workflowGenerator.errors.EMPTY_PLAN": "Model zwrócił pusty plan. Wypróbuj bardziej szczegółową instrukcję.", + "workflowGenerator.errors.GRAPH_CYCLE": "Wygenerowany przepływ pracy zawiera cykliczne połączenie. Wygeneruj ponownie lub uprość instrukcję.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "Instrukcja jest zbyt długa. Skróć ją i spróbuj ponownie.", "workflowGenerator.errors.INVALID_CONTAINER": "Wygenerowany przepływ pracy zawiera zniekształconą pętlę lub kontener iteracji. Wypróbuj prostszą instrukcję lub wybierz bardziej wydajny model.", "workflowGenerator.errors.INVALID_JSON": "Model zwrócił odpowiedź, której nie mogliśmy przeanalizować. Spróbuj ponownie lub wybierz bardziej wydajny model.", "workflowGenerator.errors.INVALID_SCHEMA": "Model zwrócił wykres w nieoczekiwanym kształcie. Spróbuj ponownie lub wybierz bardziej wydajny model.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Sprawdzanie wykresu…", "workflowGenerator.placeholder": "Napisz instrukcję po lewej stronie, a następnie kliknij Generuj, aby wyświetlić podgląd wykresu przepływu pracy.", "workflowGenerator.refineDescription": "Opisz zmianę, której pragniesz. Bieżący projekt jest używany jako kontekst; wygenerowany wykres zastępuje go po złożeniu wniosku.", + "workflowGenerator.refineDraftUnavailable": "Nie udało się wczytać bieżącego szkicu — generowanie od podstaw.", "workflowGenerator.refineInstructionPlaceholder": "Opisz zmianę – np. dodaj krok tłumaczenia, przejdź do narzędzia, dodaj obsługę błędów.", "workflowGenerator.refineTitle": "Doprecyzuj {{mode}}", "workflowGenerator.reload": "Załaduj ponownie", diff --git a/web/i18n/pt-BR/workflow.json b/web/i18n/pt-BR/workflow.json index 1a2f0107a03..02de8500543 100644 --- a/web/i18n/pt-BR/workflow.json +++ b/web/i18n/pt-BR/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Descreva o que você deseja que o fluxo de trabalho faça. Escolha um modelo, escreva uma instrução e visualize o gráfico gerado antes de aplicá-lo ao Studio.", "workflowGenerator.dismiss": "Dispensar", "workflowGenerator.errors.DANGLING_EDGE": "O fluxo de trabalho gerado tem uma borda apontando para um nó que não existe. Tente novamente ou refine suas instruções.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "O fluxo de trabalho gerado contém IDs de nó duplicados. Tente gerar novamente.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Por favor, escreva uma instrução primeiro.", "workflowGenerator.errors.EMPTY_PLAN": "O modelo retornou um plano vazio. Tente uma instrução mais específica.", + "workflowGenerator.errors.GRAPH_CYCLE": "O fluxo de trabalho gerado contém uma conexão circular. Tente gerar novamente ou simplifique sua instrução.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "A instrução é muito longa. Encurte-a e tente novamente.", "workflowGenerator.errors.INVALID_CONTAINER": "O fluxo de trabalho gerado possui um loop ou contêiner de iteração malformado. Experimente uma instrução mais simples ou escolha um modelo mais capaz.", "workflowGenerator.errors.INVALID_JSON": "O modelo retornou uma resposta que não conseguimos analisar. Tente novamente ou escolha um modelo mais capaz.", "workflowGenerator.errors.INVALID_SCHEMA": "O modelo retornou um gráfico com um formato inesperado. Tente novamente ou escolha um modelo mais capaz.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Validando o gráfico…", "workflowGenerator.placeholder": "Escreva uma instrução à esquerda e clique em Gerar para visualizar o gráfico do fluxo de trabalho.", "workflowGenerator.refineDescription": "Descreva a mudança que você deseja. O rascunho atual é usado como contexto; o gráfico gerado o substitui quando você aplica.", + "workflowGenerator.refineDraftUnavailable": "Não foi possível carregar o rascunho atual — gerando do zero.", "workflowGenerator.refineInstructionPlaceholder": "Descreva a mudança - por ex. adicione uma etapa de tradução, mude para uma ferramenta, adicione tratamento de erros.", "workflowGenerator.refineTitle": "Refinar {{mode}}", "workflowGenerator.reload": "Recarregar", diff --git a/web/i18n/ro-RO/workflow.json b/web/i18n/ro-RO/workflow.json index 1f7d256ce64..e5f4464ca62 100644 --- a/web/i18n/ro-RO/workflow.json +++ b/web/i18n/ro-RO/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Descrieți ce doriți să facă fluxul de lucru. Alegeți un model, scrieți o instrucțiune și previzualizați graficul generat înainte de a-l aplica în Studio.", "workflowGenerator.dismiss": "Respingeți", "workflowGenerator.errors.DANGLING_EDGE": "Fluxul de lucru generat are o margine îndreptată către un nod care nu există. Încercați din nou sau îmbunătățiți instrucțiunile.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Fluxul de lucru generat conține ID-uri de nod duplicate. Încercați să îl regenerați.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Vă rugăm să scrieți mai întâi o instrucțiune.", "workflowGenerator.errors.EMPTY_PLAN": "Modelul a returnat un plan gol. Încercați o instrucțiune mai specifică.", + "workflowGenerator.errors.GRAPH_CYCLE": "Fluxul de lucru generat conține o conexiune circulară. Regenerați-l sau simplificați instrucțiunea.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "Instrucțiunea este prea lungă. Scurtați-o și încercați din nou.", "workflowGenerator.errors.INVALID_CONTAINER": "Fluxul de lucru generat are o buclă sau un container de iterație incorect. Încercați o instrucțiune mai simplă sau alegeți un model mai capabil.", "workflowGenerator.errors.INVALID_JSON": "Modelul a returnat un răspuns pe care nu l-am putut analiza. Încercați din nou sau alegeți un model mai capabil.", "workflowGenerator.errors.INVALID_SCHEMA": "Modelul a returnat un grafic într-o formă neașteptată. Încercați din nou sau alegeți un model mai capabil.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Se validează graficul...", "workflowGenerator.placeholder": "Scrieți o instrucțiune în partea stângă, apoi faceți clic pe Generare pentru a previzualiza graficul fluxului de lucru.", "workflowGenerator.refineDescription": "Descrieți schimbarea dorită. Proiectul actual este folosit ca context; graficul generat îl înlocuiește atunci când aplicați.", + "workflowGenerator.refineDraftUnavailable": "Schița curentă nu a putut fi încărcată — se generează de la zero.", "workflowGenerator.refineInstructionPlaceholder": "Descrieți schimbarea - de ex. adăugați un pas de traducere, treceți la un instrument, adăugați gestionarea erorilor.", "workflowGenerator.refineTitle": "Rafinați {{mode}}", "workflowGenerator.reload": "Reîncărcați", diff --git a/web/i18n/ru-RU/workflow.json b/web/i18n/ru-RU/workflow.json index 87f4923322b..e5ca0c9dc59 100644 --- a/web/i18n/ru-RU/workflow.json +++ b/web/i18n/ru-RU/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Опишите, что вы хотите, чтобы рабочий процесс делал. Выберите модель, напишите инструкцию и просмотрите созданный график, прежде чем применять его в Studio.", "workflowGenerator.dismiss": "Увольнять", "workflowGenerator.errors.DANGLING_EDGE": "Сгенерированный рабочий процесс имеет ребро, указывающее на несуществующий узел. Попробуйте еще раз или уточните инструкцию.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Сгенерированный рабочий процесс содержит повторяющиеся ID узлов. Попробуйте сгенерировать заново.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Сначала напишите инструкцию.", "workflowGenerator.errors.EMPTY_PLAN": "Модель вернула пустой план. Попробуйте более конкретную инструкцию.", + "workflowGenerator.errors.GRAPH_CYCLE": "Сгенерированный рабочий процесс содержит циклическое соединение. Сгенерируйте заново или упростите инструкцию.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "Инструкция слишком длинная. Сократите её и попробуйте снова.", "workflowGenerator.errors.INVALID_CONTAINER": "Сгенерированный рабочий процесс содержит неверный цикл или контейнер итерации. Попробуйте более простую инструкцию или выберите более функциональную модель.", "workflowGenerator.errors.INVALID_JSON": "Модель вернула ответ, который мы не смогли проанализировать. Попробуйте еще раз или выберите более мощную модель.", "workflowGenerator.errors.INVALID_SCHEMA": "Модель вернула график неожиданной формы. Попробуйте еще раз или выберите более мощную модель.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Проверка графика…", "workflowGenerator.placeholder": "Напишите инструкцию слева, затем нажмите «Создать», чтобы просмотреть график рабочего процесса.", "workflowGenerator.refineDescription": "Опишите изменения, которые вы хотите. Текущий проект используется в качестве контекста; сгенерированный график заменяет его при подаче заявки.", + "workflowGenerator.refineDraftUnavailable": "Не удалось загрузить текущий черновик — генерация начнётся с нуля.", "workflowGenerator.refineInstructionPlaceholder": "Опишите изменение – например. добавить этап перевода, переключиться на инструмент, добавить обработку ошибок.", "workflowGenerator.refineTitle": "Уточнить {{mode}}", "workflowGenerator.reload": "Перезагрузить", diff --git a/web/i18n/sl-SI/workflow.json b/web/i18n/sl-SI/workflow.json index 753f5661ea6..e1d3cd028f1 100644 --- a/web/i18n/sl-SI/workflow.json +++ b/web/i18n/sl-SI/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Opišite, kaj želite, da poteka delo. Izberite model, napišite navodila in si predoglejte ustvarjeni graf, preden ga uporabite v Studiu.", "workflowGenerator.dismiss": "Odpusti", "workflowGenerator.errors.DANGLING_EDGE": "Ustvarjeni potek dela ima rob, ki kaže na vozlišče, ki ne obstaja. Poskusite znova ali izboljšajte svoje navodilo.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Ustvarjeni potek dela vsebuje podvojene ID-je vozlišč. Poskusite ga znova ustvariti.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Najprej napišite navodila.", "workflowGenerator.errors.EMPTY_PLAN": "Model je vrnil prazen načrt. Poskusite z bolj specifičnimi navodili.", + "workflowGenerator.errors.GRAPH_CYCLE": "Ustvarjeni potek dela vsebuje krožno povezavo. Ustvarite ga znova ali poenostavite navodilo.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "Navodilo je predolgo. Skrajšajte ga in poskusite znova.", "workflowGenerator.errors.INVALID_CONTAINER": "Ustvarjeni potek dela ima napačno oblikovano zanko ali vsebnik ponovitve. Poskusite s preprostejšim navodilom ali izberite zmogljivejši model.", "workflowGenerator.errors.INVALID_JSON": "Model je vrnil odgovor, ki ga nismo mogli razčleniti. Poskusite znova ali izberite zmogljivejši model.", "workflowGenerator.errors.INVALID_SCHEMA": "Model je vrnil graf v nepričakovani obliki. Poskusite znova ali izberite zmogljivejši model.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Potrjevanje grafa ...", "workflowGenerator.placeholder": "Na levo napišite navodilo, nato kliknite Ustvari za predogled grafa poteka dela.", "workflowGenerator.refineDescription": "Opišite spremembo, ki jo želite. Trenutni osnutek se uporablja kot kontekst; ustvarjeni graf ga nadomesti, ko se prijavite.", + "workflowGenerator.refineDraftUnavailable": "Trenutnega osnutka ni bilo mogoče naložiti — ustvarjanje bo začeto od začetka.", "workflowGenerator.refineInstructionPlaceholder": "Opišite spremembo – npr. dodajte korak prevajanja, preklopite na orodje, dodajte obravnavanje napak.", "workflowGenerator.refineTitle": "Izboljšaj {{mode}}", "workflowGenerator.reload": "Ponovno naloži", diff --git a/web/i18n/th-TH/workflow.json b/web/i18n/th-TH/workflow.json index f6998eecab1..7fa8d0bf684 100644 --- a/web/i18n/th-TH/workflow.json +++ b/web/i18n/th-TH/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "อธิบายสิ่งที่คุณต้องการให้เวิร์กโฟลว์ทำ เลือกโมเดล เขียนคำสั่ง และดูกราฟที่สร้างขึ้นก่อนที่จะนำไปใช้กับ Studio", "workflowGenerator.dismiss": "อนุญาตให้ออกไป", "workflowGenerator.errors.DANGLING_EDGE": "เวิร์กโฟลว์ที่สร้างขึ้นมีขอบที่ชี้ไปที่โหนดที่ไม่มีอยู่ ลองอีกครั้งหรือปรับแต่งคำสั่งของคุณ", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "เวิร์กโฟลว์ที่สร้างขึ้นมี ID โหนดซ้ำกัน ลองสร้างใหม่อีกครั้ง", "workflowGenerator.errors.EMPTY_INSTRUCTION": "กรุณาเขียนคำแนะนำก่อน", "workflowGenerator.errors.EMPTY_PLAN": "โมเดลส่งคืนแผนเปล่า ลองใช้คำแนะนำที่เฉพาะเจาะจงมากขึ้น", + "workflowGenerator.errors.GRAPH_CYCLE": "เวิร์กโฟลว์ที่สร้างขึ้นมีการเชื่อมต่อแบบวนซ้ำ ลองสร้างใหม่หรือทำให้คำสั่งง่ายขึ้น", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "คำสั่งยาวเกินไป โปรดย่อให้สั้นลงแล้วลองใหม่", "workflowGenerator.errors.INVALID_CONTAINER": "เวิร์กโฟลว์ที่สร้างขึ้นมีการวนซ้ำหรือคอนเทนเนอร์การวนซ้ำที่มีรูปแบบไม่ถูกต้อง ลองใช้คำแนะนำที่ง่ายกว่านี้หรือเลือกรุ่นที่มีความสามารถมากกว่า", "workflowGenerator.errors.INVALID_JSON": "โมเดลส่งคืนการตอบกลับที่เราไม่สามารถแยกวิเคราะห์ได้ ลองอีกครั้งหรือเลือกรุ่นที่มีความสามารถมากกว่านี้", "workflowGenerator.errors.INVALID_SCHEMA": "โมเดลส่งคืนกราฟในรูปร่างที่ไม่คาดคิด ลองอีกครั้งหรือเลือกรุ่นที่มีความสามารถมากกว่านี้", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "กำลังตรวจสอบกราฟ...", "workflowGenerator.placeholder": "เขียนคำแนะนำทางด้านซ้าย จากนั้นคลิกสร้างเพื่อดูตัวอย่างกราฟเวิร์กโฟลว์", "workflowGenerator.refineDescription": "อธิบายการเปลี่ยนแปลงที่คุณต้องการ แบบร่างปัจจุบันถูกใช้เป็นบริบท กราฟที่สร้างขึ้นจะแทนที่เมื่อคุณใช้", + "workflowGenerator.refineDraftUnavailable": "ไม่สามารถโหลดฉบับร่างปัจจุบันได้ — จะสร้างใหม่ตั้งแต่ต้นแทน", "workflowGenerator.refineInstructionPlaceholder": "อธิบายการเปลี่ยนแปลง — เช่น เพิ่มขั้นตอนการแปล เปลี่ยนไปใช้เครื่องมือ เพิ่มการจัดการข้อผิดพลาด", "workflowGenerator.refineTitle": "ปรับแต่ง {{mode}}", "workflowGenerator.reload": "โหลดซ้ำ", diff --git a/web/i18n/tr-TR/workflow.json b/web/i18n/tr-TR/workflow.json index 39bfb01a83f..b478e8c9c04 100644 --- a/web/i18n/tr-TR/workflow.json +++ b/web/i18n/tr-TR/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "İş akışının ne yapmasını istediğinizi açıklayın. Bir model seçin, bir talimat yazın ve oluşturulan grafiği Studio'ya uygulamadan önce önizleyin.", "workflowGenerator.dismiss": "Kapat", "workflowGenerator.errors.DANGLING_EDGE": "Oluşturulan iş akışının var olmayan bir düğüme işaret eden bir kenarı var. Tekrar deneyin veya talimatınızı geliştirin.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Oluşturulan iş akışı yinelenen düğüm kimlikleri içeriyor. Yeniden oluşturmayı deneyin.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Lütfen önce bir talimat yazın.", "workflowGenerator.errors.EMPTY_PLAN": "Model boş bir plan döndürdü. Daha spesifik bir talimat deneyin.", + "workflowGenerator.errors.GRAPH_CYCLE": "Oluşturulan iş akışı döngüsel bir bağlantı içeriyor. Yeniden oluşturun veya talimatınızı sadeleştirin.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "Talimat çok uzun. Kısaltıp tekrar deneyin.", "workflowGenerator.errors.INVALID_CONTAINER": "Oluşturulan iş akışında hatalı biçimlendirilmiş bir döngü veya yineleme kapsayıcısı var. Daha basit bir talimat deneyin veya daha yetenekli bir model seçin.", "workflowGenerator.errors.INVALID_JSON": "Model ayrıştıramadığımız bir yanıt döndürdü. Tekrar deneyin veya daha yetenekli bir model seçin.", "workflowGenerator.errors.INVALID_SCHEMA": "Model beklenmedik bir biçimde bir grafik döndürdü. Tekrar deneyin veya daha yetenekli bir model seçin.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Grafik doğrulanıyor…", "workflowGenerator.placeholder": "Sol tarafa bir talimat yazın ve ardından iş akışı grafiğinin önizlemesini görmek için Oluştur'a tıklayın.", "workflowGenerator.refineDescription": "İstediğiniz değişikliği açıklayın. Mevcut taslak bağlam olarak kullanılıyor; oluşturulan grafik, başvurduğunuzda onun yerini alır.", + "workflowGenerator.refineDraftUnavailable": "Mevcut taslak yüklenemedi — bunun yerine sıfırdan oluşturuluyor.", "workflowGenerator.refineInstructionPlaceholder": "Değişikliği açıklayın - ör. bir çeviri adımı ekleyin, bir araca geçin, hata işleme ekleyin.", "workflowGenerator.refineTitle": "{{mode}} hassaslaştır", "workflowGenerator.reload": "Yeniden yükle", diff --git a/web/i18n/uk-UA/workflow.json b/web/i18n/uk-UA/workflow.json index 9be46ee2e5e..948f9e1dab0 100644 --- a/web/i18n/uk-UA/workflow.json +++ b/web/i18n/uk-UA/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Опишіть, що ви хочете робити в робочому процесі. Виберіть модель, напишіть інструкцію та попередньо перегляньте згенерований графік перед застосуванням його в Studio.", "workflowGenerator.dismiss": "Відхилити", "workflowGenerator.errors.DANGLING_EDGE": "Згенерований робочий процес має край, який вказує на вузол, якого не існує. Спробуйте ще раз або уточніть свою інструкцію.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Згенерований робочий процес містить повторювані ID вузлів. Спробуйте згенерувати знову.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Спочатку напишіть інструкцію.", "workflowGenerator.errors.EMPTY_PLAN": "Модель повернула порожній план. Спробуйте більш конкретну інструкцію.", + "workflowGenerator.errors.GRAPH_CYCLE": "Згенерований робочий процес містить циклічне з'єднання. Згенеруйте знову або спростіть інструкцію.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "Інструкція задовга. Скоротіть її та спробуйте ще раз.", "workflowGenerator.errors.INVALID_CONTAINER": "Згенерований робочий процес має неправильний цикл або контейнер ітерації. Спробуйте простішу інструкцію або виберіть більш ефективну модель.", "workflowGenerator.errors.INVALID_JSON": "Модель повернула відповідь, яку ми не змогли проаналізувати. Спробуйте ще раз або виберіть більш потужну модель.", "workflowGenerator.errors.INVALID_SCHEMA": "Модель повернула графік у неочікуваній формі. Спробуйте ще раз або виберіть більш потужну модель.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Перевірка графіка…", "workflowGenerator.placeholder": "Напишіть інструкцію ліворуч, а потім натисніть «Створити», щоб переглянути графік робочого процесу.", "workflowGenerator.refineDescription": "Опишіть зміну, яку ви хочете. Поточна чернетка використовується як контекст; згенерований графік замінює його, коли ви застосовуєте.", + "workflowGenerator.refineDraftUnavailable": "Не вдалося завантажити поточну чернетку — генерація почнеться з нуля.", "workflowGenerator.refineInstructionPlaceholder": "Опишіть зміну — напр. додати крок перекладу, перейти на інструмент, додати обробку помилок.", "workflowGenerator.refineTitle": "Уточнити {{mode}}", "workflowGenerator.reload": "Перезавантажити", diff --git a/web/i18n/vi-VN/workflow.json b/web/i18n/vi-VN/workflow.json index 049f7f60bd8..139f378b2d8 100644 --- a/web/i18n/vi-VN/workflow.json +++ b/web/i18n/vi-VN/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "Mô tả quy trình làm việc mà bạn muốn. Chọn một mô hình, viết hướng dẫn, và xem trước biểu đồ đã tạo trước khi áp dụng vào Studio.", "workflowGenerator.dismiss": "Bỏ qua", "workflowGenerator.errors.DANGLING_EDGE": "Quy trình làm việc được tạo có một cạnh trỏ đến nút không tồn tại. Hãy thử lại hoặc tinh chỉnh hướng dẫn của bạn.", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "Quy trình được tạo chứa các ID nút trùng lặp. Hãy thử tạo lại.", "workflowGenerator.errors.EMPTY_INSTRUCTION": "Hãy viết một hướng dẫn đầu tiên.", "workflowGenerator.errors.EMPTY_PLAN": "Mô hình trả về một kế hoạch trống. Hãy thử một hướng dẫn cụ thể hơn.", + "workflowGenerator.errors.GRAPH_CYCLE": "Quy trình được tạo chứa kết nối vòng lặp. Hãy tạo lại hoặc đơn giản hóa hướng dẫn của bạn.", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "Hướng dẫn quá dài. Hãy rút ngắn và thử lại.", "workflowGenerator.errors.INVALID_CONTAINER": "Quy trình làm việc được tạo có vòng lặp hoặc vùng chứa lặp không đúng định dạng. Hãy thử hướng dẫn đơn giản hơn hoặc chọn một mô hình mạnh hơn.", "workflowGenerator.errors.INVALID_JSON": "Mô hình trả về phản hồi mà chúng tôi không thể phân tích cú pháp. Hãy thử lại hoặc chọn một mô hình có khả năng hơn.", "workflowGenerator.errors.INVALID_SCHEMA": "Mô hình trả về một biểu đồ có hình dạng không mong muốn. Hãy thử lại hoặc chọn một mô hình có khả năng hơn.", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "Đang xác thực biểu đồ…", "workflowGenerator.placeholder": "Viết hướng dẫn ở bên trái, sau đó nhấp Tạo để xem trước biểu đồ quy trình làm việc.", "workflowGenerator.refineDescription": "Mô tả sự thay đổi bạn mong muốn. Bản dự thảo hiện tại được sử dụng làm bối cảnh; biểu đồ được tạo sẽ thay thế nó khi bạn áp dụng.", + "workflowGenerator.refineDraftUnavailable": "Không thể tải bản nháp hiện tại — sẽ tạo từ đầu.", "workflowGenerator.refineInstructionPlaceholder": "Mô tả sự thay đổi - ví dụ: thêm bước dịch, chuyển sang công cụ, thêm xử lý lỗi.", "workflowGenerator.refineTitle": "Tinh chỉnh {{mode}}", "workflowGenerator.reload": "Tải lại", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 3cdddef1121..272e852e2e0 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "描述你希望工作流完成的任务。选择模型、撰写指令,预览生成的图后再应用到 Studio。", "workflowGenerator.dismiss": "关闭", "workflowGenerator.errors.DANGLING_EDGE": "生成的工作流中有连线指向了不存在的节点,请重试或细化指令。", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "生成的工作流中存在重复的节点 ID,请重新生成。", "workflowGenerator.errors.EMPTY_INSTRUCTION": "请先填写指令。", "workflowGenerator.errors.EMPTY_PLAN": "模型没有返回任何规划,尝试写一个更具体的指令。", + "workflowGenerator.errors.GRAPH_CYCLE": "生成的工作流中存在循环连线,请重新生成或简化指令。", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "指令过长,请缩短后重试。", "workflowGenerator.errors.INVALID_CONTAINER": "生成的工作流中循环或迭代容器结构异常。尝试简化指令或更换更强的模型。", "workflowGenerator.errors.INVALID_JSON": "模型返回的内容无法解析,请重试或更换更强的模型。", "workflowGenerator.errors.INVALID_SCHEMA": "模型返回的图结构不符合预期,请重试或更换更强的模型。", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "正在校验图……", "workflowGenerator.placeholder": "在左侧填写指令,点击「生成」预览工作流图。", "workflowGenerator.refineDescription": "描述你想要的修改。当前草稿会作为上下文;应用时生成的图将替换它。", + "workflowGenerator.refineDraftUnavailable": "无法加载当前草稿,将改为从头生成。", "workflowGenerator.refineInstructionPlaceholder": "描述修改内容——例如添加翻译步骤、改用某个工具、增加错误处理。", "workflowGenerator.refineTitle": "优化 {{mode}}", "workflowGenerator.reload": "重新加载", diff --git a/web/i18n/zh-Hant/workflow.json b/web/i18n/zh-Hant/workflow.json index d8ebd946624..71b13c8eee3 100644 --- a/web/i18n/zh-Hant/workflow.json +++ b/web/i18n/zh-Hant/workflow.json @@ -1258,8 +1258,11 @@ "workflowGenerator.description": "描述您希望工作流程執行的操作。選擇模型、編寫指令並然後再將其套用到 工作室覽生成的圖形,单间公寓。", "workflowGenerator.dismiss": "解僱", "workflowGenerator.errors.DANGLING_EDGE": "產生的工作流程有一條邊指向不存在的節點。再試一次或完善您的指示。", + "workflowGenerator.errors.DUPLICATE_NODE_ID": "生成的工作流中存在重複的節點 ID,請重新生成。", "workflowGenerator.errors.EMPTY_INSTRUCTION": "請先寫一個指令。", "workflowGenerator.errors.EMPTY_PLAN": "該模型返回了一個空計劃。嘗試更具體的說明。", + "workflowGenerator.errors.GRAPH_CYCLE": "生成的工作流中存在循環連線,請重新生成或簡化指令。", + "workflowGenerator.errors.INSTRUCTION_TOO_LONG": "指令過長,請縮短後重試。", "workflowGenerator.errors.INVALID_CONTAINER": "生成的工作流具有格式错误的循环或迭代容器。尝试更简单的说明或选择更有能力的模型。", "workflowGenerator.errors.INVALID_JSON": "該模型返回了我們無法解析的回應。再試一次或選擇功能更強大的型號。", "workflowGenerator.errors.INVALID_SCHEMA": "該模型傳回了一個意外形狀的圖表。再試一次或選擇功能更強大的型號。", @@ -1297,6 +1300,7 @@ "workflowGenerator.phases.validating": "驗證圖表...", "workflowGenerator.placeholder": "在左侧编写说明,然后单击生成以预览工作流程图。", "workflowGenerator.refineDescription": "描述您想要的改变。当前草稿用作上下文;应用时生成的图形将替换它。", + "workflowGenerator.refineDraftUnavailable": "無法載入目前草稿,將改為從頭生成。", "workflowGenerator.refineInstructionPlaceholder": "描述變更-例如新增翻譯步驟、切換到工具、新增錯誤處理。", "workflowGenerator.refineTitle": "精煉{{mode}}", "workflowGenerator.reload": "重新載入",