@ykurilov/agent-chat-flows 0.74.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @ykurilov/agent-chat-flows
2
2
 
3
+ ## 0.76.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6a50705: Общие переходы `anyState` перенесены из служебной ноды в отдельную панель настроек сценария. Шапка, меню телефона и обзор дают явный доступ; правила показывают условие и цель, сохраняют порядок, исключения и все настройки. Холст и мини-карта содержат только реальные состояния; старый `layout["@any"]` сохраняется, но не влияет на место или масштаб. Замечания проверки ведут к разделу и фокусируют правило; `readOnly` оставляет просмотр без изменений.
8
+
9
+ ### Как обновить приложение
10
+
11
+ Обновите используемые пакеты SDK одной версией. `FlowEditor`/`FlowTestChat` с `tandemFlowEditorSkin` не требуют новых пропсов. JSON и runtime не меняются, удалять legacy-раскладку не требуется. Собственный скин может обработать необязательные `FlowHeaderProps.onOpenAnyState` и `FlowExitCardProps.summary`, а существующий слот обзора `sections` должен отображаться. Старый экспорт `FlowAnyStateNode` сохранён, доска его не вызывает. Проверьте компактную шапку, переход из замечаний при закрытом инспекторе и сохранение правил.
12
+
13
+ ### Patch Changes
14
+
15
+ - @ykurilov/agent-chat-core@0.76.0
16
+ - @ykurilov/agent-chat-react@0.76.0
17
+
18
+ ## 0.75.0
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [dfb9869]
23
+ - @ykurilov/agent-chat-react@0.75.0
24
+ - @ykurilov/agent-chat-core@0.75.0
25
+
3
26
  ## 0.74.0
4
27
 
5
28
  ### Patch Changes
package/README.md CHANGED
@@ -123,3 +123,7 @@ checks })`. Поля приложения для карточки и попут
123
123
 
124
124
  С 0.55 к версии 2 добавился таймер «наступил день N программы» (`{ "type": "timer", "from": "program_day", "day": 3 }`):
125
125
  выход в начале дня N, когда бы человек ни пришёл в ноду. SDK до 0.55 такой сценарий не откроет.
126
+
127
+ ### Общие переходы в редакторе
128
+
129
+ С 0.76.0 `anyState` редактируется в панели «Общие переходы» рядом с холстом, а не отдельной нодой. В компактной шапке и на телефоне вход доступен через действия редактора. Порядок, условия, цели и исключения сохраняются в прежнем JSON; runtime не меняется. `readOnly` оставляет просмотр. Собственному скину доступны необязательные `FlowHeaderProps.onOpenAnyState` и `FlowExitCardProps.summary`; старый экспорт `FlowAnyStateNode` сохранён, но доской не используется.
package/dist/react.js CHANGED
@@ -722,7 +722,7 @@ function useFlowLabels() {
722
722
  }
723
723
 
724
724
  // src/react/editor/flow-editor.tsx
725
- import { useMemo as useMemo4, useState as useState4 } from "react";
725
+ import { useEffect as useEffect3, useMemo as useMemo4, useRef as useRef3, useState as useState4 } from "react";
726
726
 
727
727
  // src/react/editor/editor-model.ts
728
728
  function freeId(prefix, taken) {
@@ -884,7 +884,7 @@ function removeField(flow, list, fieldId) {
884
884
  };
885
885
  }
886
886
  function freeSpot(flow) {
887
- const points = Object.values(flow.layout);
887
+ const points = flow.states.flatMap((state) => flow.layout[state.id] ? [flow.layout[state.id]] : []);
888
888
  if (points.length === 0) return { x: 80, y: 80 };
889
889
  return { x: Math.min(...points.map((point) => point.x)), y: Math.max(...points.map((point) => point.y)) + 240 };
890
890
  }
@@ -1036,6 +1036,10 @@ function useBoardSpace(props, editor) {
1036
1036
  toggleMinimap: () => remember("minimap", !minimapOpen),
1037
1037
  inspectorOpen,
1038
1038
  inspectorToggleable: !needed,
1039
+ showInspector: () => {
1040
+ if (inspector === "collapsible") setFollowed(true);
1041
+ else remember("inspector", true);
1042
+ },
1039
1043
  toggleInspector: () => {
1040
1044
  if (needed) return;
1041
1045
  if (inspector === "collapsible") setFollowed(!inspectorOpen);
@@ -1476,25 +1480,17 @@ function StateNodeView({ data }) {
1476
1480
  data.draggable ? /* @__PURE__ */ jsx(Handle, { type: "source", position: Position.Right, id: NEW_EXIT_HANDLE, className: "flow-board__new-exit" }) : null
1477
1481
  ] });
1478
1482
  }
1479
- function AnyNodeView({ data }) {
1480
- const skin = useSkin();
1481
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1482
- /* @__PURE__ */ jsx(skin.FlowAnyStateNode, { ...data.view, renderExitHandle: (exitId) => exitHandle(exitId, data.draggable) }),
1483
- data.draggable ? /* @__PURE__ */ jsx(Handle, { type: "source", position: Position.Right, id: NEW_EXIT_HANDLE, className: "flow-board__new-exit" }) : null
1484
- ] });
1485
- }
1486
1483
  function EdgeView({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data }) {
1487
1484
  const skin = useSkin();
1488
1485
  const [path, labelX, labelY] = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
1489
1486
  if (!data) return null;
1490
1487
  return /* @__PURE__ */ jsx(skin.FlowEdge, { ...data, edgeId: id, path, labelX, labelY });
1491
1488
  }
1492
- var nodeTypes = { state: StateNodeView, any: AnyNodeView };
1489
+ var nodeTypes = { state: StateNodeView };
1493
1490
  var edgeTypes = { flow: EdgeView };
1494
1491
  function nodePosition(flow, nodeId, index) {
1495
1492
  const saved = flow.layout[nodeId];
1496
1493
  if (saved) return saved;
1497
- if (nodeId === ANY_STATE_LAYOUT_KEY) return { x: 40, y: -220 };
1498
1494
  return { x: 40 + index % 4 * 330, y: 40 + Math.floor(index / 4) * 260 };
1499
1495
  }
1500
1496
  function countFor(issues, match) {
@@ -1554,27 +1550,7 @@ function useBoardModel(editor) {
1554
1550
  }
1555
1551
  }
1556
1552
  }));
1557
- if (flow.anyState.exits.length > 0 || flow.anyState.except.length > 0 || flow.layout[ANY_STATE_LAYOUT_KEY]) {
1558
- nodes.push({
1559
- id: ANY_STATE_LAYOUT_KEY,
1560
- type: "any",
1561
- position: nodePosition(flow, ANY_STATE_LAYOUT_KEY, 0),
1562
- draggable: editable,
1563
- data: {
1564
- draggable: editable,
1565
- view: {
1566
- except: flow.anyState.except.map((id) => stateById(flow, id)?.title ?? id),
1567
- exits: exitRows(flow, catalog, flow.anyState.exits, issues, labels),
1568
- selected: selection.kind === "any",
1569
- dimmed: marks !== null,
1570
- issues: countFor(issues, (issue) => !issue.place.stateId && flow.anyState.exits.some((exit) => exit.id === issue.place.exitId)),
1571
- onSelect: () => selectable && select({ kind: "any" })
1572
- }
1573
- }
1574
- });
1575
- }
1576
1553
  const owners = [
1577
- { nodeId: ANY_STATE_LAYOUT_KEY, exits: flow.anyState.exits },
1578
1554
  ...flow.states.map((state) => ({ nodeId: state.id, exits: state.exits }))
1579
1555
  ];
1580
1556
  const edges = owners.flatMap(
@@ -1930,7 +1906,8 @@ function useFlowEditor(options) {
1930
1906
  );
1931
1907
  const goToIssue = useCallback3((issue) => {
1932
1908
  const { stateId, exitId, fieldId, sideTaskId } = issue.place;
1933
- if (stateId && flow.states.some((state) => state.id === stateId)) setSelection({ kind: "state", stateId, ...exitId ? { exitId } : {} });
1909
+ if (issue.code === "unknown_except_state") setSelection({ kind: "any" });
1910
+ else if (stateId && flow.states.some((state) => state.id === stateId)) setSelection({ kind: "state", stateId, ...exitId ? { exitId } : {} });
1934
1911
  else if (exitId && flow.anyState.exits.some((exit) => exit.id === exitId)) setSelection({ kind: "any", exitId });
1935
1912
  else if (sideTaskId) setSelection({ kind: "overview", sideTaskId });
1936
1913
  else if (fieldId) setSelection({ kind: "card" });
@@ -2098,6 +2075,10 @@ function FlowEditor({ skin, className, style, header: headerMode, legend, minima
2098
2075
  issues: editor.issueCount,
2099
2076
  onAddState: () => editor.addState(freeSpot(flow)),
2100
2077
  onOpenCard: () => editor.select({ kind: "card" }),
2078
+ onOpenAnyState: () => {
2079
+ editor.select({ kind: "any" });
2080
+ space.showInspector();
2081
+ },
2101
2082
  onOpenJson: editor.openJson,
2102
2083
  ...options.onSave && !editor.readOnly ? { onSave: () => void editor.save() } : {},
2103
2084
  saving: editor.saving,
@@ -2121,7 +2102,7 @@ function FlowEditor({ skin, className, style, header: headerMode, legend, minima
2121
2102
  {
2122
2103
  header,
2123
2104
  board: /* @__PURE__ */ jsx2(FlowBoard, { editor, skin, space }),
2124
- panel: space.inspectorOpen ? /* @__PURE__ */ jsx2(EditorPanel, { editor, skin, labels }) : null,
2105
+ panel: space.inspectorOpen ? /* @__PURE__ */ jsx2(EditorPanel, { editor, skin, labels, onNavigate: space.showInspector }) : null,
2125
2106
  dialog: /* @__PURE__ */ jsx2(EditorDialog, { editor, skin }),
2126
2107
  ...space.header === "compact" ? { compact: true } : {},
2127
2108
  ...space.fullscreen ? { style: { height: "100%", minHeight: 0 } } : { ...className !== void 0 ? { className } : {}, ...style !== void 0 ? { style } : {} }
@@ -2171,10 +2152,12 @@ function ExitCards({ editor, skin, labels, owner }) {
2171
2152
  const targets = targetOptions(flow, labels, owner !== null);
2172
2153
  return /* @__PURE__ */ jsx2(Fragment2, { children: exits.map((exit, index) => {
2173
2154
  const fieldType = exit.when.type === "data" ? fieldById(flow, exit.when.field)?.field.type : void 0;
2174
- return /* @__PURE__ */ jsx2(
2155
+ const summary = `${conditionText(flow, exit.when, labels, editor.catalog)} \u2192 ${targetTitle(flow, exit.to, labels) ?? labels.board.noTarget}`;
2156
+ return /* @__PURE__ */ jsx2(ExitFocus, { label: summary, exitId: exit.id, selection: editor.selection, owner, children: /* @__PURE__ */ jsx2(
2175
2157
  skin.FlowExitCard,
2176
2158
  {
2177
2159
  exitId: exit.id,
2160
+ ...owner === null ? { summary } : {},
2178
2161
  when: exit.when,
2179
2162
  to: exit.to,
2180
2163
  fields,
@@ -2189,11 +2172,20 @@ function ExitCards({ editor, skin, labels, owner }) {
2189
2172
  onMoveUp: () => editor.edit((current) => moveExit(current, owner, exit.id, -1)),
2190
2173
  onMoveDown: () => editor.edit((current) => moveExit(current, owner, exit.id, 1)),
2191
2174
  onDelete: () => editor.edit((current) => removeExit(current, owner, exit.id))
2192
- },
2193
- exit.id
2194
- );
2175
+ }
2176
+ ) }, exit.id);
2195
2177
  }) });
2196
2178
  }
2179
+ function ExitFocus({ label, exitId, selection, owner, children }) {
2180
+ const ref = useRef3(null);
2181
+ const focused = (selection.kind === "any" && owner === null || selection.kind === "state" && selection.stateId === owner) && selection.exitId === exitId;
2182
+ useEffect3(() => {
2183
+ if (!focused || !ref.current) return;
2184
+ ref.current.scrollIntoView?.({ block: "nearest" });
2185
+ ref.current.focus({ preventScroll: true });
2186
+ }, [focused, selection]);
2187
+ return /* @__PURE__ */ jsx2("div", { ref, role: "group", "aria-label": label, tabIndex: focused ? -1 : void 0, "data-flow-exit-focus": exitId, children });
2188
+ }
2197
2189
  function FieldRows({ editor, skin, list }) {
2198
2190
  const fields = editor.flow[list];
2199
2191
  return /* @__PURE__ */ jsx2(Fragment2, { children: fields.map((field, index) => /* @__PURE__ */ jsx2(
@@ -2212,15 +2204,18 @@ function FieldRows({ editor, skin, list }) {
2212
2204
  field.id
2213
2205
  )) });
2214
2206
  }
2215
- function EditorPanel({ editor, skin, labels }) {
2216
- const panel = /* @__PURE__ */ jsx2(EditablePanel, { editor, skin, labels });
2207
+ function EditorPanel({ editor, skin, labels, onNavigate }) {
2208
+ const panel = /* @__PURE__ */ jsx2(EditablePanel, { editor, skin, labels, onNavigate });
2217
2209
  if (!editor.readOnly || editor.mode !== "edit") return panel;
2218
2210
  return /* @__PURE__ */ jsx2("fieldset", { disabled: true, "data-flow-read-only": "", className: "m-0 flex min-h-0 min-w-0 flex-1 flex-col border-0 p-0", children: panel });
2219
2211
  }
2220
- function EditablePanel({ editor, skin, labels }) {
2212
+ function EditablePanel({ editor, skin, labels, onNavigate }) {
2221
2213
  const { flow, selection } = editor;
2222
2214
  if (editor.mode === "check") {
2223
- return /* @__PURE__ */ jsx2(skin.FlowCheckPanel, { issues: editor.issues.map((issue) => ({ ...issue, onGo: () => editor.goToIssue(issue) })) });
2215
+ return /* @__PURE__ */ jsx2(skin.FlowCheckPanel, { issues: editor.issues.map((issue) => ({ ...issue, onGo: () => {
2216
+ editor.goToIssue(issue);
2217
+ onNavigate();
2218
+ } })) });
2224
2219
  }
2225
2220
  if (editor.mode === "run" && editor.simulation) return /* @__PURE__ */ jsx2(RunPanel, { editor, skin, labels });
2226
2221
  if (selection.kind === "state") {
@@ -2261,18 +2256,7 @@ function EditablePanel({ editor, skin, labels }) {
2261
2256
  state.id
2262
2257
  );
2263
2258
  }
2264
- if (selection.kind === "any") {
2265
- return /* @__PURE__ */ jsx2(
2266
- skin.FlowAnyStatePanel,
2267
- {
2268
- except: flow.anyState.except,
2269
- states: stateLinks(editor),
2270
- onExceptChange: (except) => editor.edit((current) => setExcept(current, except)),
2271
- exits: /* @__PURE__ */ jsx2(ExitCards, { editor, skin, labels, owner: null }),
2272
- onAddExit: (kind) => editor.edit((current) => addExit(current, null, kind).flow)
2273
- }
2274
- );
2275
- }
2259
+ if (selection.kind === "any") return /* @__PURE__ */ jsx2(AnyStateSettings, { editor, skin, labels });
2276
2260
  if (selection.kind === "card") {
2277
2261
  return /* @__PURE__ */ jsx2(
2278
2262
  skin.FlowCardPanel,
@@ -2305,12 +2289,25 @@ function EditablePanel({ editor, skin, labels }) {
2305
2289
  states: stateLinks(editor),
2306
2290
  issues: editor.issueCount,
2307
2291
  sections: /* @__PURE__ */ jsxs2(Fragment2, { children: [
2292
+ /* @__PURE__ */ jsx2("div", { style: { flex: "none" }, children: /* @__PURE__ */ jsx2(AnyStateSettings, { editor, skin, labels }) }),
2308
2293
  /* @__PURE__ */ jsx2(ScheduleSection, { editor, skin, labels }),
2309
2294
  /* @__PURE__ */ jsx2(SideTasksSection, { editor, skin, focused: selection.kind === "overview" ? selection.sideTaskId : void 0 })
2310
2295
  ] })
2311
2296
  }
2312
2297
  );
2313
2298
  }
2299
+ function AnyStateSettings({ editor, skin, labels }) {
2300
+ return /* @__PURE__ */ jsx2(
2301
+ skin.FlowAnyStatePanel,
2302
+ {
2303
+ except: editor.flow.anyState.except,
2304
+ states: stateLinks(editor),
2305
+ onExceptChange: (except) => editor.edit((current) => setExcept(current, except)),
2306
+ exits: /* @__PURE__ */ jsx2(ExitCards, { editor, skin, labels, owner: null }),
2307
+ onAddExit: (kind) => editor.edit((current) => addExit(current, null, kind).flow)
2308
+ }
2309
+ );
2310
+ }
2314
2311
  var SCHEDULE_ISSUES = /* @__PURE__ */ new Set(["night_before_day_start", "unknown_deadline_state", "day_frame_soft_after_hard"]);
2315
2312
  var isScheduleIssue = (issue) => SCHEDULE_ISSUES.has(issue.code) || issue.code === "empty_nudges" && !issue.place.stateId;
2316
2313
  function NudgesEditor({ skin, nudges, optional, onChange }) {
@@ -2502,15 +2499,15 @@ function EditorDialog({ editor, skin }) {
2502
2499
  }
2503
2500
 
2504
2501
  // src/react/client-card/use-client-card.ts
2505
- import { useCallback as useCallback4, useEffect as useEffect3, useRef as useRef3, useState as useState5 } from "react";
2502
+ import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
2506
2503
  function useClientCard(options) {
2507
- const latest = useRef3(options);
2504
+ const latest = useRef4(options);
2508
2505
  latest.current = options;
2509
2506
  const [state, setState] = useState5("loading");
2510
2507
  const [entries, setEntries] = useState5([]);
2511
2508
  const [busy, setBusy] = useState5(/* @__PURE__ */ new Set());
2512
2509
  const [forgettingAll, setForgettingAll] = useState5(false);
2513
- const alive = useRef3(true);
2510
+ const alive = useRef4(true);
2514
2511
  const load = useCallback4(async () => {
2515
2512
  setState("loading");
2516
2513
  try {
@@ -2522,7 +2519,7 @@ function useClientCard(options) {
2522
2519
  if (alive.current) setState("failed");
2523
2520
  }
2524
2521
  }, []);
2525
- useEffect3(() => {
2522
+ useEffect4(() => {
2526
2523
  alive.current = true;
2527
2524
  void load();
2528
2525
  return () => {
@@ -2667,7 +2664,7 @@ function durationText2(ms, labels) {
2667
2664
  }
2668
2665
 
2669
2666
  // src/react/test-chat/use-flow-test-chat.ts
2670
- import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef4, useState as useState6 } from "react";
2667
+ import { useCallback as useCallback5, useEffect as useEffect5, useRef as useRef5, useState as useState6 } from "react";
2671
2668
  var RUN_POLL_MS = 1e3;
2672
2669
  function runOf(run) {
2673
2670
  if (!run || typeof run.state !== "string") return null;
@@ -2685,7 +2682,7 @@ function useFlowTestChat(connection, flow) {
2685
2682
  const [failed, setFailed] = useState6(false);
2686
2683
  const [nothingScheduled, setNothingScheduled] = useState6(false);
2687
2684
  const [run, setRun] = useState6(null);
2688
- const latest = useRef4({ connection, flow, chatId });
2685
+ const latest = useRef5({ connection, flow, chatId });
2689
2686
  latest.current = { connection, flow, chatId };
2690
2687
  const refresh = useCallback5(async (id) => {
2691
2688
  try {
@@ -2695,7 +2692,7 @@ function useFlowTestChat(connection, flow) {
2695
2692
  console.error("[flow] test chat: run not read", error);
2696
2693
  }
2697
2694
  }, []);
2698
- useEffect4(() => {
2695
+ useEffect5(() => {
2699
2696
  if (!opened || chatId === null) return;
2700
2697
  void refresh(chatId);
2701
2698
  const timer = setInterval(() => void refresh(chatId), RUN_POLL_MS);
@@ -2793,7 +2790,18 @@ function useTestChatView() {
2793
2790
  function withTestChat(skin) {
2794
2791
  function Header(props) {
2795
2792
  const { controller } = useTestChatView();
2796
- return /* @__PURE__ */ jsx4(skin.FlowHeader, { ...props, onOpenTestChat: controller.open });
2793
+ const openAnyState = props.onOpenAnyState;
2794
+ return /* @__PURE__ */ jsx4(
2795
+ skin.FlowHeader,
2796
+ {
2797
+ ...props,
2798
+ onOpenTestChat: controller.open,
2799
+ ...openAnyState ? { onOpenAnyState: () => {
2800
+ controller.close();
2801
+ openAnyState();
2802
+ } } : {}
2803
+ }
2804
+ );
2797
2805
  }
2798
2806
  function Frame(props) {
2799
2807
  const { panel } = useTestChatView();