@anlyx/ui 0.1.2 → 0.1.5

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.
Files changed (62) hide show
  1. package/README.md +3 -2
  2. package/dist/capture/capture-runtime.d.ts +14 -0
  3. package/dist/capture/capture-runtime.js +300 -0
  4. package/dist/components/AnalysisEvidenceList.d.ts +5 -0
  5. package/dist/components/AnalysisEvidenceList.js +61 -0
  6. package/dist/components/AnlyxAppShell.d.ts +1 -1
  7. package/dist/components/AnlyxAppShell.js +16 -7
  8. package/dist/components/ApiCallList.d.ts +3 -2
  9. package/dist/components/ApiCallList.js +12 -2
  10. package/dist/components/CaptureStatusEmptyState.js +2 -2
  11. package/dist/components/EndpointMapCanvas.js +1 -1
  12. package/dist/components/FlowStoryView.d.ts +22 -0
  13. package/dist/components/FlowStoryView.js +117 -0
  14. package/dist/components/InspectorPanel.d.ts +1 -1
  15. package/dist/components/InspectorPanel.js +46 -1
  16. package/dist/components/PageStoryboardView.js +9 -1
  17. package/dist/components/ProcessFlowView.js +8 -1
  18. package/dist/components/ReplayControls.d.ts +2 -1
  19. package/dist/components/ReplayControls.js +29 -2
  20. package/dist/components/Sidebar.d.ts +2 -2
  21. package/dist/components/Sidebar.js +15 -3
  22. package/dist/components/StatusBadge.d.ts +2 -2
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +1 -0
  25. package/dist/mock-data.js +50 -4
  26. package/dist/overlay/AnlyxFlowEdge.d.ts +2 -0
  27. package/dist/overlay/AnlyxFlowEdge.js +15 -0
  28. package/dist/overlay/AnlyxFlowNode.d.ts +13 -0
  29. package/dist/overlay/AnlyxFlowNode.js +28 -0
  30. package/dist/overlay/FlowDrawer.d.ts +2 -0
  31. package/dist/overlay/FlowDrawer.js +59 -0
  32. package/dist/overlay/MainFlowCanvas.d.ts +20 -0
  33. package/dist/overlay/MainFlowCanvas.js +285 -0
  34. package/dist/overlay/RecentApiEventsTable.d.ts +5 -0
  35. package/dist/overlay/RecentApiEventsTable.js +19 -0
  36. package/dist/overlay/overlay-entry.d.ts +8 -0
  37. package/dist/overlay/overlay-entry.js +14 -0
  38. package/dist/overlay/overlay-ui.css +2 -0
  39. package/dist/overlay/overlay-ui.js +14 -0
  40. package/dist/overlay/types.d.ts +38 -0
  41. package/dist/overlay/types.js +1 -0
  42. package/dist/overlay/ui.d.ts +18 -0
  43. package/dist/overlay/ui.js +13 -0
  44. package/dist/readme-demo/ReadmeDemoApp.d.ts +15 -0
  45. package/dist/readme-demo/ReadmeDemoApp.js +184 -0
  46. package/dist/readme-demo/readme-demo-entry.d.ts +1 -0
  47. package/dist/readme-demo/readme-demo-entry.js +8 -0
  48. package/dist/styles.css +1165 -38
  49. package/dist/viewer/ViewerApp.js +26 -16
  50. package/dist/viewer/styles.css +2639 -0
  51. package/dist/viewer/viewer-entry.d.ts +1 -0
  52. package/dist/viewer/viewer-entry.js +1 -0
  53. package/dist/viewer/workspace/anlyx-logo-transparent.png +0 -0
  54. package/dist/viewer/workspace/workspace.css +6354 -0
  55. package/dist/workspace/ScanTreeMap.d.ts +6 -0
  56. package/dist/workspace/ScanTreeMap.js +838 -0
  57. package/dist/workspace/WorkspaceApp.d.ts +8 -0
  58. package/dist/workspace/WorkspaceApp.js +2293 -0
  59. package/dist/workspace/project-view-model.d.ts +63 -0
  60. package/dist/workspace/project-view-model.js +170 -0
  61. package/dist/workspace/workspace.css +6354 -0
  62. package/package.json +10 -3
@@ -0,0 +1,117 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Braces, Code2, Database, FileText, GitBranch, Globe2, Layers3, Maximize2, MonitorSmartphone, MousePointerClick, Route, ShieldCheck, Wifi } from "lucide-react";
3
+ import { ReplayControls } from "./ReplayControls.js";
4
+ import { StatusBadge } from "./StatusBadge.js";
5
+ export function FlowStoryView({ data, endpoint, flow, page, replayDisabled, replayLoop, replaySpeed, replayState, replaySteps, selectedNodeId, onPause, onPlay, onRestart, onSelectNode, onSpeedChange, onToggleLoop }) {
6
+ const storyPage = page ?? findPageForEndpoint(data, endpoint);
7
+ const screenshot = storyPage?.screenshots[0];
8
+ const stats = getFlowStoryStats(flow);
9
+ const replayStepLabels = buildReplayStepLabels(flow);
10
+ const primaryApiCall = storyPage?.apiCalls.find((apiCall) => apiCall.endpointId === endpoint?.id);
11
+ return (_jsxs("main", { className: "anlyx-flow-story", children: [_jsxs("header", { className: "anlyx-flow-story__header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Flow Story" }), _jsx("h1", { children: endpoint ? titleForEndpoint(endpoint) : "Application flow story" }), _jsxs("div", { className: "anlyx-flow-story__summary", "aria-label": "Flow Story summary", children: [_jsxs("span", { children: [stats.mainSteps, " main steps"] }), _jsxs("span", { children: [stats.supportCalls, " support calls"] }), _jsxs("span", { children: [stats.evidenceCount, " evidence items"] })] })] }), _jsxs("div", { className: "anlyx-flow-story__actions", children: [_jsx(StatusBadge, { tone: "neutral", children: "Replay Lite" }), _jsxs("button", { className: "anlyx-toolbar-button", type: "button", children: [_jsx(Maximize2, { size: 14, strokeWidth: 2.4 }), "Fit view"] })] })] }), _jsx(InteractionEvidenceChain, { apiCallLabel: formatObservedApiCall(primaryApiCall, endpoint), backendStepCount: stats.mainSteps, pageRoute: storyPage?.route }), _jsxs("section", { className: "anlyx-flow-story__stage", "aria-label": "Flow Story canvas", children: [_jsx(PagePreviewCard, { page: storyPage }), _jsxs("div", { className: "anlyx-flow-story__request", "aria-hidden": "true", children: [_jsx("span", { children: "Request" }), _jsx("i", {})] }), _jsx(FlowStoryDiagram, { flow: flow, replayState: replayState, selectedNodeId: selectedNodeId, onSelectNode: onSelectNode }), _jsx("div", { className: "anlyx-flow-story__response", "aria-hidden": "true", children: _jsx("span", { children: "Response" }) })] }), _jsx(ReplayControls, { disabled: replayDisabled, loop: replayLoop, speed: replaySpeed, state: replayState, stepLabels: replayStepLabels, steps: replaySteps, unavailableReason: "Replay is unavailable because this flow has no scanned main path.", onPause: onPause, onPlay: onPlay, onRestart: onRestart, onSpeedChange: onSpeedChange, onToggleLoop: onToggleLoop }), screenshot?.path ? _jsx("span", { className: "anlyx-sr-only", children: screenshot.path }) : null] }));
12
+ }
13
+ function InteractionEvidenceChain({ apiCallLabel, backendStepCount, pageRoute }) {
14
+ return (_jsxs("section", { className: "anlyx-interaction-chain", "aria-label": "Interaction evidence chain", children: [_jsx(EvidenceChainItem, { detail: pageRoute ?? "No page linked", icon: MousePointerClick, label: "Triggered page", tone: "page" }), _jsx(EvidenceChainItem, { detail: apiCallLabel, icon: Wifi, label: "Browser API event", tone: "api" }), _jsx(EvidenceChainItem, { detail: `${backendStepCount} steps`, icon: Route, label: "Scanned backend path", tone: "backend" }), _jsx(EvidenceChainItem, { detail: "not runtime tracing", icon: ShieldCheck, label: "Static evidence", tone: "guard" })] }));
15
+ }
16
+ function EvidenceChainItem({ detail, icon: Icon, label, tone }) {
17
+ return (_jsxs("article", { className: `anlyx-interaction-chain__item anlyx-interaction-chain__item--${tone}`, children: [_jsx("span", { className: "anlyx-interaction-chain__icon", "aria-hidden": "true", children: _jsx(Icon, { size: 16, strokeWidth: 2.5 }) }), _jsxs("div", { children: [_jsx("span", { children: label }), _jsx("strong", { children: detail })] })] }));
18
+ }
19
+ function PagePreviewCard({ page }) {
20
+ const screenshot = page?.screenshots[0];
21
+ const primaryApiCall = page?.apiCalls[0];
22
+ return (_jsxs("article", { className: "anlyx-page-preview", "aria-label": "Frontend page preview", children: [_jsxs("div", { className: "anlyx-page-preview__chrome", children: [_jsx(MonitorSmartphone, { size: 16, strokeWidth: 2.4 }), _jsx("span", { children: page?.route ?? "No page linked" })] }), _jsxs("div", { className: "anlyx-page-preview__screen", children: [_jsxs("div", { className: "anlyx-page-preview__topbar", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {}), _jsx("strong", { children: "STARBUCKS" })] }), _jsxs("div", { className: "anlyx-page-preview__hero", children: [_jsxs("div", { children: [_jsx("span", { children: screenshot?.title ?? "Captured page" }), _jsx("strong", { children: "Birthday Reward" }), _jsx("p", { children: "Enjoy a free drink or food item on your birthday." })] }), _jsxs("div", { className: "anlyx-page-preview__reward", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("strong", { children: "1" })] })] }), _jsxs("div", { className: "anlyx-page-preview__facts", children: [_jsx("span", { children: "Valid Jan 1 - Dec 31" }), _jsx("span", { children: "Once per year" })] }), _jsxs("div", { className: "anlyx-page-preview__steps", children: [_jsx("span", { children: "Show this reward in store" }), _jsx("span", { children: "Choose drink or food" }), _jsx("span", { children: "Enjoy birthday treat" })] }), _jsxs("div", { className: "anlyx-page-preview__api", children: [_jsx("strong", { children: primaryApiCall?.method ?? "GET" }), _jsx("span", { children: primaryApiCall?.path ?? "/api/..." })] })] }), _jsxs("div", { className: "anlyx-page-preview__meta", children: [_jsx(StatusBadge, { tone: page?.captureStatus ?? "pending", children: page?.captureStatus ?? "pending" }), _jsxs("span", { children: [page?.apiCalls.length ?? 0, " API calls"] }), _jsxs("span", { children: [page?.screenshots.length ?? 0, " screenshots"] })] })] }));
23
+ }
24
+ function FlowStoryDiagram({ flow, replayState, selectedNodeId, onSelectNode }) {
25
+ if (!flow) {
26
+ return (_jsx("div", { className: "anlyx-flow-story__diagram anlyx-flow-story__diagram--empty", children: _jsx("p", { children: "No scanned backend flow available for this endpoint." }) }));
27
+ }
28
+ const nodesById = new Map(flow.nodes.map((node) => [node.id, node]));
29
+ const mainNodes = flow.mainPath
30
+ .map((nodeId) => nodesById.get(nodeId))
31
+ .filter((node) => Boolean(node))
32
+ .filter((node) => node.type !== "page");
33
+ const supportingNodes = flow.subFlows.flatMap((subFlow) => subFlow.nodes);
34
+ return (_jsxs("div", { className: "anlyx-flow-story__diagram", role: "region", "aria-label": "Flow Story path", children: [_jsxs("div", { className: "anlyx-flow-story__diagram-head", children: [_jsxs("div", { children: [_jsx("span", { children: "Main path" }), _jsxs("strong", { children: [mainNodes.length, " steps"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Support" }), _jsxs("strong", { children: [supportingNodes.length, " calls"] })] })] }), _jsx("div", { className: "anlyx-flow-story__lane", "aria-label": "Main request path", children: mainNodes.map((node, index) => (_jsx(FlowStoryStep, { isActive: replayState.activeNodeId === node.id, isSelected: selectedNodeId === node.id, node: node, stepNumber: index + 1, showArrow: index < mainNodes.length - 1, onSelectNode: onSelectNode }, node.id))) }), supportingNodes.length > 0 ? (_jsxs("div", { className: "anlyx-flow-story__support", children: [_jsxs("div", { className: "anlyx-flow-story__support-heading", children: [_jsx(GitBranch, { size: 15, strokeWidth: 2.5 }), _jsx("span", { children: "Support calls from service" })] }), _jsx("div", { className: "anlyx-flow-story__support-grid", children: supportingNodes.map((node) => (_jsx(FlowStorySupportNode, { isActive: replayState.activeNodeId === node.id, isSelected: selectedNodeId === node.id, node: node, onSelectNode: onSelectNode }, node.id))) })] })) : null] }));
35
+ }
36
+ function FlowStoryStep({ node, isActive, isSelected, stepNumber, showArrow, onSelectNode }) {
37
+ const Icon = getFlowStoryIcon(node.type);
38
+ return (_jsxs("div", { className: "anlyx-flow-story__step-wrap", children: [_jsxs("button", { className: [
39
+ "anlyx-flow-story__step",
40
+ `anlyx-flow-story__step--${node.type}`,
41
+ isActive ? "anlyx-flow-story__step--active" : "",
42
+ isSelected ? "anlyx-flow-story__step--selected" : ""
43
+ ]
44
+ .filter(Boolean)
45
+ .join(" "), type: "button", onClick: () => onSelectNode(node), "aria-label": `Select ${node.type} step ${stepNumber}: ${node.label}`, children: [_jsx("span", { className: "anlyx-flow-story__step-number", children: String(stepNumber).padStart(2, "0") }), _jsx("span", { className: "anlyx-flow-story__step-icon", "aria-hidden": "true", children: _jsx(Icon, { size: 16, strokeWidth: 2.5 }) }), _jsx("span", { className: "anlyx-flow-story__step-type", children: node.type }), _jsx("strong", { children: node.label }), _jsx(StatusBadge, { tone: node.confidence ?? "unknown", label: "confidence", children: node.confidence ?? "unknown" })] }), showArrow ? _jsx("span", { className: "anlyx-flow-story__arrow", "aria-hidden": "true" }) : null] }));
46
+ }
47
+ function FlowStorySupportNode({ node, isActive, isSelected, onSelectNode }) {
48
+ const Icon = getFlowStoryIcon(node.type);
49
+ return (_jsxs("button", { className: [
50
+ "anlyx-flow-story__support-node",
51
+ isActive ? "anlyx-flow-story__support-node--active" : "",
52
+ isSelected ? "anlyx-flow-story__support-node--selected" : ""
53
+ ]
54
+ .filter(Boolean)
55
+ .join(" "), type: "button", onClick: () => onSelectNode(node), "aria-label": `Select support call ${node.label}`, children: [_jsx(Icon, { size: 15, strokeWidth: 2.5 }), _jsx("span", { children: node.type }), _jsx("strong", { children: node.label }), _jsx(StatusBadge, { tone: node.confidence ?? "unknown", label: "confidence", children: node.confidence ?? "unknown" })] }));
56
+ }
57
+ function findPageForEndpoint(data, endpoint) {
58
+ if (!endpoint) {
59
+ return data.pages[0];
60
+ }
61
+ return (data.pages.find((page) => page.apiCalls.some((apiCall) => apiCall.endpointId === endpoint.id)) ?? data.pages[0]);
62
+ }
63
+ function titleForEndpoint(endpoint) {
64
+ const handler = endpoint.controller && endpoint.handler
65
+ ? `${endpoint.controller}#${endpoint.handler}`
66
+ : undefined;
67
+ return handler ?? `${endpoint.method} ${endpoint.path}`;
68
+ }
69
+ function formatObservedApiCall(apiCall, endpoint) {
70
+ if (apiCall) {
71
+ return `${apiCall.method} ${apiCall.path}`;
72
+ }
73
+ if (endpoint) {
74
+ return `${endpoint.method} ${endpoint.path}`;
75
+ }
76
+ return "No API event matched";
77
+ }
78
+ function getFlowStoryStats(flow) {
79
+ if (!flow) {
80
+ return { evidenceCount: 0, mainSteps: 0, supportCalls: 0 };
81
+ }
82
+ return {
83
+ mainSteps: flow.mainPath.filter((nodeId) => !nodeId.startsWith("page:")).length,
84
+ supportCalls: flow.subFlows.reduce((count, subFlow) => count + subFlow.nodes.length, 0),
85
+ evidenceCount: flow.nodes.reduce((count, node) => count + (node.evidence?.length ?? 0), 0) +
86
+ flow.edges.reduce((count, edge) => count + (edge.evidence?.length ?? 0), 0) +
87
+ flow.subFlows.reduce((count, subFlow) => count +
88
+ subFlow.nodes.reduce((nodeCount, node) => nodeCount + (node.evidence?.length ?? 0), 0) +
89
+ subFlow.edges.reduce((edgeCount, edge) => edgeCount + (edge.evidence?.length ?? 0), 0), 0)
90
+ };
91
+ }
92
+ function buildReplayStepLabels(flow) {
93
+ if (!flow) {
94
+ return {};
95
+ }
96
+ return Object.fromEntries(flow.nodes.map((node) => [node.id, node.label]));
97
+ }
98
+ function getFlowStoryIcon(type) {
99
+ switch (type) {
100
+ case "endpoint":
101
+ return Globe2;
102
+ case "controller":
103
+ return Code2;
104
+ case "service":
105
+ return Layers3;
106
+ case "repository":
107
+ return Braces;
108
+ case "database":
109
+ return Database;
110
+ case "mapper":
111
+ case "validator":
112
+ case "utility":
113
+ return GitBranch;
114
+ default:
115
+ return FileText;
116
+ }
117
+ }
@@ -2,7 +2,7 @@ import type { EndpointFlow, FlowNode, PageStoryboard, ScanResult } from "@anlyx/
2
2
  import type { ReplayLiteState } from "../replay/use-replay-lite.js";
3
3
  type InspectorPanelProps = {
4
4
  data: ScanResult;
5
- activeView: "structure" | "frontend" | "process";
5
+ activeView: "flowStory" | "structure" | "frontend" | "process";
6
6
  collapsed: boolean;
7
7
  selectedFlow: EndpointFlow | undefined;
8
8
  selectedNode: FlowNode | undefined;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AnalysisEvidenceList } from "./AnalysisEvidenceList.js";
2
3
  import { StatusBadge } from "./StatusBadge.js";
3
4
  export function InspectorPanel({ data, activeView, collapsed, selectedFlow, selectedNode, selectedPage, replayState, onToggleCollapsed }) {
4
5
  if (collapsed) {
@@ -8,11 +9,25 @@ export function InspectorPanel({ data, activeView, collapsed, selectedFlow, sele
8
9
  return (_jsxs("aside", { className: "anlyx-inspector", role: "complementary", "aria-label": "Inspector", children: [_jsxs("div", { className: "anlyx-panel-heading", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Inspector" }), _jsx("h2", { children: "Frontend Page" })] }), _jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Collapse inspector panel", onClick: onToggleCollapsed, children: "Collapse" })] }), selectedPage ? (_jsxs("div", { className: "anlyx-inspector-stack", children: [_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Details", children: [_jsx("h3", { children: "Details" }), _jsx(Field, { label: "Route", value: selectedPage.route }), _jsx(Field, { label: "File path", value: selectedPage.filePath ?? "Manual or unknown" }), _jsxs("div", { className: "anlyx-field", children: [_jsx("span", { className: "anlyx-field__label", children: "Capture status" }), _jsx(StatusBadge, { tone: selectedPage.captureStatus, children: selectedPage.captureStatus })] }), _jsx(Field, { label: "Screenshots", value: String(selectedPage.screenshots.length) }), _jsx(Field, { label: "API calls", value: String(selectedPage.apiCalls.length) })] }), selectedPage.errorMessage ? (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Capture error", children: [_jsx("h3", { children: "Capture error" }), _jsx("p", { children: selectedPage.errorMessage })] })) : null] })) : (_jsx("p", { className: "anlyx-empty", children: "No page selected" }))] }));
9
10
  }
10
11
  const linkedPages = selectedNode ? findLinkedPages(data, selectedNode.id) : [];
11
- return (_jsxs("aside", { className: "anlyx-inspector", role: "complementary", "aria-label": "Inspector", children: [_jsxs("div", { className: "anlyx-panel-heading", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Inspector" }), _jsx("h2", { children: activeView === "process" ? "Process Step" : "Backend Node" })] }), _jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Collapse inspector panel", onClick: onToggleCollapsed, children: "Collapse" })] }), selectedNode ? (_jsxs("div", { className: "anlyx-inspector-stack", children: [activeView === "process" ? (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Replay state", children: [_jsx("h3", { children: "Process replay" }), _jsx(Field, { label: "Active Step", value: replayState.phase }), _jsx(Field, { label: "Step", value: String(replayState.currentStepIndex + 1) }), _jsx(Field, { label: "Active Node", value: replayState.activeNodeId ?? "none" }), _jsx(Field, { label: "Active Edge", value: formatActiveEdge(replayState) }), _jsx("p", { className: "anlyx-inspector-note", children: "Source: scanned static flow graph" })] })) : null, _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Details", children: [_jsx("h3", { children: "Details" }), _jsx(Field, { label: "Type", value: selectedNode.type }), _jsx(Field, { label: "Label", value: selectedNode.label }), _jsx(Field, { label: "File path", value: selectedNode.filePath ?? "Unknown" }), _jsx(Field, { label: "Line number", value: formatLineNumber(selectedNode.lineNumber) })] }), selectedNode.metadata ? (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Metadata", children: [_jsxs("div", { className: "anlyx-inspector-group__heading", children: [_jsx("h3", { children: "Metadata" }), _jsx("button", { className: "anlyx-copy-button", type: "button", onClick: () => copyToClipboard(JSON.stringify(selectedNode.metadata, null, 2)), children: "Copy" })] }), _jsx("pre", { className: "anlyx-metadata", children: JSON.stringify(selectedNode.metadata, null, 2) })] })) : null, _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Confidence", children: [_jsx("h3", { children: "Confidence" }), _jsx(StatusBadge, { tone: selectedNode.confidence ?? "unknown", label: "confidence", children: selectedNode.confidence ?? "unknown" })] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Linked pages", children: [_jsx("h3", { children: "Linked pages" }), linkedPages.length > 0 ? (_jsx("ul", { children: linkedPages.map((page) => (_jsx("li", { children: page.route }, page.id))) })) : (_jsx("p", { children: "None" }))] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Sub flows", children: [_jsx("h3", { children: "Sub flows" }), _jsxs("p", { children: [selectedFlow?.subFlows.length ?? 0, " collapsed"] })] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "DB tables", children: [_jsx("h3", { children: "DB tables" }), _jsx("p", { children: findDatabaseLabel(selectedFlow) ?? "None" })] })] })) : (_jsx("p", { className: "anlyx-empty", children: "No node selected" }))] }));
12
+ const calls = selectedNode ? findCalls(selectedFlow, selectedNode.id) : [];
13
+ return (_jsxs("aside", { className: "anlyx-inspector", role: "complementary", "aria-label": "Inspector", children: [_jsxs("div", { className: "anlyx-panel-heading", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Inspector" }), _jsx("h2", { children: activeView === "process" ? "Process Step" : "Flow Evidence" })] }), _jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Collapse inspector panel", onClick: onToggleCollapsed, children: "Collapse" })] }), selectedNode ? (_jsxs("div", { className: "anlyx-inspector-stack", children: [activeView === "process" ? (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Replay state", children: [_jsx("h3", { children: "Process replay" }), _jsx(Field, { label: "Active Step", value: replayState.phase }), _jsx(Field, { label: "Step", value: String(replayState.currentStepIndex + 1) }), _jsx(Field, { label: "Active Node", value: replayState.activeNodeId ?? "none" }), _jsx(Field, { label: "Active Edge", value: formatActiveEdge(replayState) }), _jsx("p", { className: "anlyx-inspector-note", children: "Source: scanned static flow graph" })] })) : null, _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Details", children: [_jsx("h3", { children: "Details" }), _jsx(Field, { label: "Type", value: selectedNode.type }), _jsx(Field, { label: "Label", value: selectedNode.label }), _jsxs("div", { className: "anlyx-field", children: [_jsx("span", { className: "anlyx-field__label", children: "Status" }), _jsx(StatusBadge, { tone: selectedNode.status ?? "unknown", children: selectedNode.status ?? "unknown" })] }), _jsx(Field, { label: "File path", value: selectedNode.filePath ?? "Unknown" }), _jsx(Field, { label: "Line number", value: formatLineNumber(selectedNode.lineNumber) }), selectedNode.metadata ? (_jsx(Field, { label: "Generated by", value: formatGeneratedBy(selectedNode.metadata) })) : null] }), _jsx(TimingGroup, { timing: selectedNode.timing }), selectedNode.request ? (_jsx(JsonGroup, { title: "Request shape", ariaLabel: "Request shape", value: selectedNode.request })) : null, selectedNode.response ? (_jsx(JsonGroup, { title: "Response shape", ariaLabel: "Response shape", value: selectedNode.response })) : null, _jsx(AnalysisEvidenceList, { node: selectedNode }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Calls", children: [_jsx("h3", { children: "Calls" }), calls.length > 0 ? (_jsx("ul", { className: "anlyx-call-list", children: calls.map((call) => (_jsxs("li", { children: [_jsx("span", { children: call.label }), _jsx(StatusBadge, { tone: call.confidence, children: call.confidence })] }, call.id))) })) : (_jsx("p", { children: "No outgoing calls detected for this node." }))] }), selectedNode.metadata ? (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Metadata", children: [_jsxs("div", { className: "anlyx-inspector-group__heading", children: [_jsx("h3", { children: "Metadata" }), _jsx("button", { className: "anlyx-copy-button", type: "button", onClick: () => copyToClipboard(JSON.stringify(selectedNode.metadata, null, 2)), children: "Copy" })] }), _jsx("pre", { className: "anlyx-metadata", children: JSON.stringify(selectedNode.metadata, null, 2) })] })) : null, _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Confidence", children: [_jsx("h3", { children: "Confidence" }), _jsx(StatusBadge, { tone: selectedNode.confidence ?? "unknown", label: "confidence", children: selectedNode.confidence ?? "unknown" })] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Linked pages", children: [_jsx("h3", { children: "Linked pages" }), linkedPages.length > 0 ? (_jsx("ul", { children: linkedPages.map((page) => (_jsx("li", { children: page.route }, page.id))) })) : (_jsx("p", { children: "None" }))] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Sub flows", children: [_jsx("h3", { children: "Sub flows" }), _jsxs("p", { children: [selectedFlow?.subFlows.length ?? 0, " collapsed"] })] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "DB tables", children: [_jsx("h3", { children: "DB tables" }), _jsx("p", { children: findDatabaseLabel(selectedFlow) ?? "None" })] })] })) : (_jsx("p", { className: "anlyx-empty", children: "No node selected" }))] }));
12
14
  }
13
15
  function Field({ label, value }) {
14
16
  return (_jsxs("div", { className: "anlyx-field", children: [_jsx("span", { className: "anlyx-field__label", children: label }), _jsx("span", { className: "anlyx-field__value", children: value })] }));
15
17
  }
18
+ function TimingGroup({ timing }) {
19
+ if (!timing || timing.kind === "unknown") {
20
+ return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Timing", children: [_jsx("h3", { children: "Timing" }), _jsx(Field, { label: "Kind", value: "Unknown" })] }));
21
+ }
22
+ if (timing.kind === "measured") {
23
+ return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Timing", children: [_jsx("h3", { children: "Timing" }), _jsx(Field, { label: "Kind", value: "Measured" }), _jsx(Field, { label: "Duration", value: `${timing.durationMs}ms` }), _jsx(Field, { label: "Evidence", value: timing.evidenceId })] }));
24
+ }
25
+ return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Timing", children: [_jsx("h3", { children: "Timing" }), _jsx(Field, { label: "Kind", value: "estimate" }), timing.durationMs !== undefined ? (_jsx(Field, { label: "Duration", value: `${timing.durationMs}ms` })) : null, _jsx(Field, { label: "Reason", value: timing.reason })] }));
26
+ }
27
+ function JsonGroup({ title, ariaLabel, value }) {
28
+ const formatted = JSON.stringify(value, null, 2);
29
+ return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": ariaLabel, children: [_jsxs("div", { className: "anlyx-inspector-group__heading", children: [_jsx("h3", { children: title }), _jsx("button", { className: "anlyx-copy-button", type: "button", onClick: () => copyToClipboard(formatted), children: "Copy" })] }), _jsx("pre", { className: "anlyx-metadata anlyx-shape-json", children: formatted })] }));
30
+ }
16
31
  function formatLineNumber(lineNumber) {
17
32
  return lineNumber === undefined ? "Unknown" : String(lineNumber);
18
33
  }
@@ -25,9 +40,39 @@ function formatActiveEdge(replayState) {
25
40
  function copyToClipboard(value) {
26
41
  void navigator.clipboard?.writeText(value);
27
42
  }
43
+ function formatGeneratedBy(metadata) {
44
+ const generatedBy = metadata.generatedBy;
45
+ if (!isRecord(generatedBy)) {
46
+ return "Unknown";
47
+ }
48
+ const name = typeof generatedBy.name === "string" ? generatedBy.name : undefined;
49
+ const type = typeof generatedBy.type === "string" ? generatedBy.type : undefined;
50
+ const version = typeof generatedBy.version === "string" ? generatedBy.version : undefined;
51
+ return [name, type, version].filter(Boolean).join(" · ") || "Unknown";
52
+ }
53
+ function isRecord(value) {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
28
56
  function findLinkedPages(data, nodeId) {
29
57
  return data.pages.filter((page) => page.apiCalls.some((apiCall) => apiCall.endpointId === nodeId));
30
58
  }
31
59
  function findDatabaseLabel(flow) {
32
60
  return flow?.nodes.find((node) => node.type === "database")?.label;
33
61
  }
62
+ function findCalls(flow, nodeId) {
63
+ if (!flow) {
64
+ return [];
65
+ }
66
+ const allNodes = [...flow.nodes, ...flow.subFlows.flatMap((subFlow) => subFlow.nodes)];
67
+ const allEdges = [...flow.edges, ...flow.subFlows.flatMap((subFlow) => subFlow.edges)];
68
+ return allEdges
69
+ .filter((edge) => edge.from === nodeId)
70
+ .map((edge) => {
71
+ const target = allNodes.find((node) => node.id === edge.to);
72
+ return {
73
+ id: edge.id,
74
+ label: target?.label ?? edge.to,
75
+ confidence: edge.confidence ?? "unknown"
76
+ };
77
+ });
78
+ }
@@ -7,5 +7,13 @@ export function PageStoryboardView({ data, page, onViewProcessFlow }) {
7
7
  const linkedEndpoints = page
8
8
  ? data.endpoints.filter((endpoint) => page.apiCalls.some((apiCall) => apiCall.endpointId === endpoint.id))
9
9
  : [];
10
- return (_jsxs("main", { className: "anlyx-workspace", children: [_jsxs("header", { className: "anlyx-workspace-header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Connected Frontend" }), _jsx("h1", { children: page ? page.route : "Page Storyboard" })] }), _jsxs("div", { className: "anlyx-workspace-actions", children: [_jsx("button", { className: "anlyx-toolbar-button", type: "button", disabled: linkedEndpoints.length === 0, onClick: () => onViewProcessFlow("process"), children: "View Process Flow" }), page ? _jsx(StatusBadge, { tone: page.captureStatus, children: page.captureStatus }) : null] })] }), _jsx("section", { className: "anlyx-page-storyboard", role: "region", "aria-label": "Page Storyboard", children: page ? (_jsxs(_Fragment, { children: [_jsxs("section", { className: "anlyx-page-summary", "aria-label": "Selected page summary", children: [_jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "Route" }), _jsx("h2", { children: page.route })] }), _jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "File" }), _jsx("p", { children: page.filePath ?? "Unknown" })] }), _jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "Status" }), _jsx(StatusBadge, { tone: page.captureStatus, children: page.captureStatus })] }), _jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "Linked endpoints" }), _jsx("p", { children: linkedEndpoints.length })] })] }), _jsx(CaptureStatusEmptyState, { status: page.captureStatus, ...(page.errorMessage ? { reason: page.errorMessage } : {}) }), _jsxs("div", { className: "anlyx-storyboard-grid", children: [_jsx(PageStoryboardCard, { page: page }), _jsxs("div", { className: "anlyx-storyboard-side", children: [_jsx(ApiCallList, { apiCalls: page.apiCalls }), _jsxs("section", { className: "anlyx-storyboard-panel", "aria-label": "Page to endpoint relationship", children: [_jsxs("div", { className: "anlyx-storyboard-section-heading", children: [_jsx("h2", { children: "Page to Endpoint" }), _jsx("span", { children: linkedEndpoints.length })] }), linkedEndpoints.length > 0 ? (_jsxs("div", { className: "anlyx-relationship-diagram", children: [_jsxs("div", { className: "anlyx-relationship-source", children: [_jsx("strong", { children: page.route }), _jsx("span", { children: "Frontend Page" })] }), _jsx("ul", { className: "anlyx-relationship-list", children: linkedEndpoints.map((endpoint) => (_jsxs("li", { children: [_jsx("span", { className: "anlyx-relationship-line", "aria-hidden": "true" }), _jsx(StatusBadge, { tone: endpoint.method, children: endpoint.method }), _jsx("span", { children: endpoint.path })] }, endpoint.id))) })] })) : (_jsx("p", { className: "anlyx-empty-inline", children: "No backend endpoint was linked during capture." }))] })] })] })] })) : (_jsx("div", { className: "anlyx-storyboard-empty", children: _jsx("p", { children: "No pages available yet." }) })) })] }));
10
+ const linkedApiCallCount = page?.apiCalls.filter((apiCall) => apiCall.endpointId).length ?? 0;
11
+ const unmatchedApiCallCount = page ? page.apiCalls.length - linkedApiCallCount : 0;
12
+ return (_jsxs("main", { className: "anlyx-workspace", children: [_jsxs("header", { className: "anlyx-workspace-header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Connected Frontend" }), _jsx("h1", { children: page ? page.route : "Page Storyboard" })] }), _jsxs("div", { className: "anlyx-workspace-actions", children: [_jsx("button", { className: "anlyx-toolbar-button", type: "button", disabled: linkedEndpoints.length === 0, onClick: () => onViewProcessFlow("process"), children: "View Process Flow" }), page ? _jsx(StatusBadge, { tone: page.captureStatus, children: page.captureStatus }) : null] })] }), _jsx("section", { className: "anlyx-page-storyboard", role: "region", "aria-label": "Page Storyboard", children: page ? (_jsxs(_Fragment, { children: [_jsxs("section", { className: "anlyx-page-summary", "aria-label": "Selected page summary", children: [_jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "Route" }), _jsx("h2", { children: page.route })] }), _jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "File" }), _jsx("p", { children: page.filePath ?? "Unknown" })] }), _jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "Status" }), _jsx(StatusBadge, { tone: page.captureStatus, children: page.captureStatus })] }), _jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "Linked endpoints" }), _jsx("p", { children: linkedEndpoints.length })] })] }), _jsx(CaptureStatusEmptyState, { status: page.captureStatus, ...(page.errorMessage ? { reason: page.errorMessage } : {}) }), _jsx(PageEvidenceBoard, { linkedApiCallCount: linkedApiCallCount, linkedEndpointCount: linkedEndpoints.length, page: page, unmatchedApiCallCount: unmatchedApiCallCount }), _jsxs("div", { className: "anlyx-storyboard-grid", children: [_jsx(PageStoryboardCard, { page: page }), _jsxs("div", { className: "anlyx-storyboard-side", children: [_jsx(ApiCallList, { apiCalls: page.apiCalls, endpoints: data.endpoints }), _jsxs("section", { className: "anlyx-storyboard-panel", "aria-label": "Page to endpoint relationship", children: [_jsxs("div", { className: "anlyx-storyboard-section-heading", children: [_jsx("h2", { children: "Page to Endpoint" }), _jsx("span", { children: linkedEndpoints.length })] }), linkedEndpoints.length > 0 ? (_jsxs("div", { className: "anlyx-relationship-diagram", children: [_jsxs("div", { className: "anlyx-relationship-source", children: [_jsx("strong", { children: page.route }), _jsx("span", { children: "Frontend Page" })] }), _jsx("ul", { className: "anlyx-relationship-list", children: linkedEndpoints.map((endpoint) => (_jsxs("li", { children: [_jsx("span", { className: "anlyx-relationship-line", "aria-hidden": "true" }), _jsx(StatusBadge, { tone: endpoint.method, children: endpoint.method }), _jsx("span", { children: endpoint.path })] }, endpoint.id))) })] })) : (_jsx("p", { className: "anlyx-empty-inline", children: "No backend endpoint was linked during capture." }))] })] })] })] })) : (_jsx("div", { className: "anlyx-storyboard-empty", children: _jsx("p", { children: "No pages available yet." }) })) })] }));
13
+ }
14
+ function PageEvidenceBoard({ linkedApiCallCount, linkedEndpointCount, page, unmatchedApiCallCount }) {
15
+ return (_jsxs("section", { className: "anlyx-page-evidence-board", role: "region", "aria-label": "Page execution evidence", children: [_jsxs("div", { className: "anlyx-page-evidence-board__intro", children: [_jsx("span", { children: "Capture proof" }), _jsx("strong", { children: page.captureStatus === "success" ? "Observed page run" : "Incomplete page run" })] }), _jsx(EvidenceMetric, { label: "Screenshots", value: String(page.screenshots.length), detail: page.screenshots.length > 0 ? "visual segments captured" : "waiting for capture" }), _jsx(EvidenceMetric, { label: "API evidence", value: String(page.apiCalls.length), detail: `${linkedApiCallCount} linked` }), _jsx(EvidenceMetric, { label: "Backend linkage", value: String(linkedEndpointCount), detail: `${unmatchedApiCallCount} unmatched` })] }));
16
+ }
17
+ function EvidenceMetric({ detail, label, value }) {
18
+ return (_jsxs("div", { className: "anlyx-page-evidence-metric", children: [_jsx("span", { children: label }), _jsx("strong", { children: value }), _jsx("em", { children: detail })] }));
11
19
  }
@@ -3,7 +3,14 @@ import { EndpointMapCanvas } from "./EndpointMapCanvas.js";
3
3
  import { ProcessTimeline } from "./ProcessTimeline.js";
4
4
  import { ReplayControls } from "./ReplayControls.js";
5
5
  export function ProcessFlowView({ endpoint, flow, replayState, replaySteps, replayLoop, replayDisabled, replaySpeed, selectedNodeId, onSelectNode, onPause, onPlay, onRestart, onToggleLoop, onSpeedChange, onViewStructure }) {
6
- return (_jsxs("div", { className: "anlyx-process-view", children: [_jsx(ReplayControls, { disabled: replayDisabled, loop: replayLoop, speed: replaySpeed, state: replayState, steps: replaySteps, unavailableReason: "Process Flow is unavailable because this endpoint has no scanned main path.", onPause: onPause, onPlay: onPlay, onRestart: onRestart, onSpeedChange: onSpeedChange, onToggleLoop: onToggleLoop }), _jsx(EndpointMapCanvas, { eyebrow: "Request Process Flow", endpoint: endpoint, flow: flow, replayState: replayState, selectedNodeId: selectedNodeId, title: endpoint
6
+ const replayStepLabels = buildReplayStepLabels(flow);
7
+ return (_jsxs("div", { className: "anlyx-process-view", children: [_jsx(ReplayControls, { disabled: replayDisabled, loop: replayLoop, speed: replaySpeed, state: replayState, stepLabels: replayStepLabels, steps: replaySteps, unavailableReason: "Process Flow is unavailable because this endpoint has no scanned main path.", onPause: onPause, onPlay: onPlay, onRestart: onRestart, onSpeedChange: onSpeedChange, onToggleLoop: onToggleLoop }), _jsx(EndpointMapCanvas, { eyebrow: "Request Process Flow", endpoint: endpoint, flow: flow, replayState: replayState, selectedNodeId: selectedNodeId, title: endpoint
7
8
  ? `${endpoint.method} ${endpoint.path}`
8
9
  : "Request process flow from scanned graph", toolbar: _jsx("button", { className: "anlyx-toolbar-button", type: "button", onClick: onViewStructure, children: "View on Structure" }), variant: "process", onSelectNode: onSelectNode }), _jsx(ProcessTimeline, { flow: flow, state: replayState, steps: replaySteps })] }));
9
10
  }
11
+ function buildReplayStepLabels(flow) {
12
+ if (!flow) {
13
+ return {};
14
+ }
15
+ return Object.fromEntries(flow.nodes.map((node) => [node.id, node.label]));
16
+ }
@@ -3,6 +3,7 @@ import type { ReplayStep } from "../replay/build-replay-steps.js";
3
3
  export type ReplayControlsProps = {
4
4
  state: ReplayLiteState;
5
5
  steps: ReplayStep[];
6
+ stepLabels?: Record<string, string>;
6
7
  loop: boolean;
7
8
  disabled?: boolean;
8
9
  speed: number;
@@ -13,4 +14,4 @@ export type ReplayControlsProps = {
13
14
  onToggleLoop: () => void;
14
15
  onSpeedChange: (speed: number) => void;
15
16
  };
16
- export declare function ReplayControls({ state, steps, loop, speed, disabled, unavailableReason, onPlay, onPause, onRestart, onToggleLoop, onSpeedChange }: ReplayControlsProps): JSX.Element;
17
+ export declare function ReplayControls({ state, steps, stepLabels, loop, speed, disabled, unavailableReason, onPlay, onPause, onRestart, onToggleLoop, onSpeedChange }: ReplayControlsProps): JSX.Element;
@@ -1,6 +1,33 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Pause, Play, RefreshCw, Repeat2 } from "lucide-react";
3
- export function ReplayControls({ state, steps, loop, speed, disabled = false, unavailableReason, onPlay, onPause, onRestart, onToggleLoop, onSpeedChange }) {
3
+ export function ReplayControls({ state, steps, stepLabels = {}, loop, speed, disabled = false, unavailableReason, onPlay, onPause, onRestart, onToggleLoop, onSpeedChange }) {
4
4
  const currentStepNumber = state.phase === "idle" ? 0 : Math.min(state.currentStepIndex + 1, steps.length);
5
- return (_jsxs("section", { className: "anlyx-replay", "aria-label": "Process Flow controls", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Replay from scanned flow graph" }), _jsxs("div", { className: "anlyx-replay__buttons", "aria-label": "Process Flow actions", children: [_jsxs("button", { className: "anlyx-replay__button--primary", type: "button", disabled: disabled || state.isPlaying, onClick: onPlay, children: [_jsx(Play, { size: 14, strokeWidth: 2.5 }), _jsx("span", { children: "Play" })] }), _jsxs("button", { type: "button", disabled: disabled || !state.isPlaying, onClick: onPause, children: [_jsx(Pause, { size: 14, strokeWidth: 2.5 }), _jsx("span", { children: "Pause" })] }), _jsxs("button", { type: "button", disabled: disabled, onClick: onRestart, children: [_jsx(RefreshCw, { size: 14, strokeWidth: 2.5 }), _jsx("span", { children: "Restart" })] }), _jsxs("button", { type: "button", "aria-pressed": loop, disabled: disabled, onClick: onToggleLoop, children: [_jsx(Repeat2, { size: 14, strokeWidth: 2.5 }), _jsxs("span", { children: ["Loop ", loop ? "on" : "off"] })] })] })] }), _jsxs("div", { className: "anlyx-replay__state", children: [_jsxs("span", { children: ["Step ", currentStepNumber, "/", steps.length] }), _jsxs("span", { children: ["Phase: ", state.phase] }), _jsxs("label", { children: ["Speed", _jsxs("select", { "aria-label": "Replay speed", disabled: disabled, value: speed, onChange: (event) => onSpeedChange(Number(event.currentTarget.value)), children: [_jsx("option", { value: 1100, children: "0.75x" }), _jsx("option", { value: 800, children: "1x" }), _jsx("option", { value: 520, children: "1.5x" })] })] }), _jsx("span", { children: "Main Flow only" })] }), disabled && unavailableReason ? (_jsx("p", { className: "anlyx-replay__empty", children: unavailableReason })) : null] }));
5
+ const currentStep = state.phase === "idle" || state.phase === "complete"
6
+ ? undefined
7
+ : steps[state.currentStepIndex];
8
+ const focusLabel = currentStep ? formatStepNode(currentStep.nodeId, stepLabels) : "Ready";
9
+ const edgeLabel = formatStepEdge(currentStep, stepLabels);
10
+ const phaseLabel = formatReplayPhase(state.phase);
11
+ return (_jsxs("section", { className: "anlyx-replay", "aria-label": "Process Flow controls", children: [_jsxs("div", { className: "anlyx-replay__top", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Replay from scanned flow graph" }), _jsxs("div", { className: "anlyx-replay__buttons", "aria-label": "Process Flow actions", children: [_jsxs("button", { className: "anlyx-replay__button--primary", type: "button", disabled: disabled || state.isPlaying, onClick: onPlay, children: [_jsx(Play, { size: 14, strokeWidth: 2.5 }), _jsx("span", { children: "Play" })] }), _jsxs("button", { type: "button", disabled: disabled || !state.isPlaying, onClick: onPause, children: [_jsx(Pause, { size: 14, strokeWidth: 2.5 }), _jsx("span", { children: "Pause" })] }), _jsxs("button", { type: "button", disabled: disabled, onClick: onRestart, children: [_jsx(RefreshCw, { size: 14, strokeWidth: 2.5 }), _jsx("span", { children: "Restart" })] }), _jsxs("button", { type: "button", "aria-pressed": loop, disabled: disabled, onClick: onToggleLoop, children: [_jsx(Repeat2, { size: 14, strokeWidth: 2.5 }), _jsxs("span", { children: ["Loop ", loop ? "on" : "off"] })] })] })] }), _jsxs("div", { className: "anlyx-replay__state", children: [_jsxs("span", { children: ["Step ", currentStepNumber, "/", steps.length] }), _jsxs("span", { children: ["Phase: ", state.phase] }), _jsxs("label", { children: ["Speed", _jsxs("select", { "aria-label": "Replay speed", disabled: disabled, value: speed, onChange: (event) => onSpeedChange(Number(event.currentTarget.value)), children: [_jsx("option", { value: 1100, children: "0.75x" }), _jsx("option", { value: 800, children: "1x" }), _jsx("option", { value: 520, children: "1.5x" })] })] }), _jsx("span", { children: "Main Flow only" })] })] }), _jsxs("div", { className: "anlyx-replay__focus", role: "group", "aria-label": "Replay focus", children: [_jsxs("div", { children: [_jsx("span", { className: "anlyx-replay__phase", children: phaseLabel }), _jsx("strong", { children: focusLabel }), _jsx("p", { children: edgeLabel })] }), _jsx("ol", { className: "anlyx-replay__rail", "aria-label": "Replay step rail", children: steps.map((step, index) => (_jsx("li", { className: index === state.currentStepIndex ? "anlyx-replay__rail-step--active" : "", "aria-current": index === state.currentStepIndex ? "step" : undefined, title: `${formatReplayPhase(step.phase)}: ${formatStepNode(step.nodeId, stepLabels)}`, children: _jsx("span", { children: String(index + 1).padStart(2, "0") }) }, `${step.phase}:${step.nodeId}:${step.index}`))) })] }), disabled && unavailableReason ? (_jsx("p", { className: "anlyx-replay__empty", children: unavailableReason })) : null] }));
12
+ }
13
+ function formatReplayPhase(phase) {
14
+ switch (phase) {
15
+ case "request":
16
+ return "Request travel";
17
+ case "response":
18
+ return "Response return";
19
+ case "complete":
20
+ return "Response delivered";
21
+ case "idle":
22
+ return "Ready to replay";
23
+ }
24
+ }
25
+ function formatStepNode(nodeId, labels) {
26
+ return labels[nodeId] ?? nodeId;
27
+ }
28
+ function formatStepEdge(step, labels) {
29
+ if (!step?.fromNodeId || !step.toNodeId) {
30
+ return "Waiting at the first visible node in the scanned main path.";
31
+ }
32
+ return `${formatStepNode(step.fromNodeId, labels)} -> ${formatStepNode(step.toNodeId, labels)}`;
6
33
  }
@@ -1,11 +1,11 @@
1
1
  import type { Endpoint, PageStoryboard, ScanResult } from "@anlyx/core";
2
2
  type SidebarProps = {
3
3
  data: ScanResult;
4
- activeView: "structure" | "frontend" | "process";
4
+ activeView: "flowStory" | "structure" | "frontend" | "process";
5
5
  collapsed: boolean;
6
6
  selectedEndpointId: string | undefined;
7
7
  selectedPageId: string | undefined;
8
- onSelectView: (view: "structure" | "frontend" | "process") => void;
8
+ onSelectView: (view: "flowStory" | "structure" | "frontend" | "process") => void;
9
9
  onToggleCollapsed: () => void;
10
10
  onSelectEndpoint: (endpoint: Endpoint) => void;
11
11
  onSelectPage: (page: PageStoryboard) => void;
@@ -1,10 +1,22 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { PanelLeftClose, PanelLeftOpen, Search } from "lucide-react";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, PanelLeftClose, PanelLeftOpen, Search } from "lucide-react";
3
3
  import { EndpointList } from "./EndpointList.js";
4
4
  import { PageList } from "./PageList.js";
5
5
  export function Sidebar({ data, activeView, collapsed, selectedEndpointId, selectedPageId, onSelectView, onToggleCollapsed, onSelectEndpoint, onSelectPage }) {
6
6
  if (collapsed) {
7
7
  return (_jsxs("aside", { className: "anlyx-sidebar anlyx-sidebar--collapsed", "aria-label": "Primary navigation", children: [_jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Expand navigation panel", onClick: onToggleCollapsed, children: _jsx(PanelLeftOpen, { size: 15, strokeWidth: 2.4 }) }), _jsx("span", { className: "anlyx-collapsed-label", children: "Nav" })] }));
8
8
  }
9
- return (_jsxs("aside", { className: "anlyx-sidebar", "aria-label": "Primary navigation", children: [_jsxs("div", { className: "anlyx-brand", children: [_jsx("div", { className: "anlyx-brand__mark", "aria-hidden": "true", children: "A" }), _jsxs("div", { children: [_jsx("div", { className: "anlyx-brand__name", children: "Anlyx" }), _jsx("div", { className: "anlyx-brand__project", children: data.projectName })] }), _jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Collapse navigation panel", onClick: onToggleCollapsed, children: _jsx(PanelLeftClose, { size: 15, strokeWidth: 2.4 }) })] }), _jsxs("nav", { className: "anlyx-tabs", "aria-label": "Views", children: [_jsx("button", { className: activeView === "structure" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("structure"), children: "Structure" }), _jsx("button", { className: activeView === "frontend" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("frontend"), children: "Connected Frontend" }), _jsx("button", { className: activeView === "process" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("process"), children: "Process Flow" })] }), _jsxs("label", { className: "anlyx-search", children: [_jsx("span", { className: "anlyx-search__label", children: "Search" }), _jsxs("span", { className: "anlyx-search__control", children: [_jsx(Search, { size: 14, strokeWidth: 2.4 }), _jsx("input", { placeholder: "Search endpoints or pages", type: "search" })] })] }), _jsx("div", { className: "anlyx-sidebar__list-region", children: activeView === "frontend" ? (_jsx(PageList, { pages: data.pages, selectedPageId: selectedPageId, onSelectPage: onSelectPage })) : (_jsx(EndpointList, { endpoints: data.endpoints, selectedEndpointId: selectedEndpointId, onSelectEndpoint: onSelectEndpoint })) })] }));
9
+ return (_jsxs("aside", { className: "anlyx-sidebar", "aria-label": "Primary navigation", children: [_jsxs("div", { className: "anlyx-brand", children: [_jsx("div", { className: "anlyx-brand__mark", "aria-hidden": "true", children: "A" }), _jsxs("div", { children: [_jsx("div", { className: "anlyx-brand__name", children: "Anlyx" }), _jsx("div", { className: "anlyx-brand__project", children: "Interaction flow map" })] }), _jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Collapse navigation panel", onClick: onToggleCollapsed, children: _jsx(PanelLeftClose, { size: 15, strokeWidth: 2.4 }) })] }), _jsxs("button", { className: "anlyx-project-select", type: "button", "aria-label": `Project ${data.projectName}`, children: [_jsx(Box, { size: 15, strokeWidth: 2.4 }), _jsx("span", { children: data.projectName })] }), _jsxs("nav", { className: "anlyx-tabs", "aria-label": "Views", children: [_jsx("button", { className: activeView === "flowStory" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("flowStory"), children: "Flow Story" }), _jsx("button", { className: activeView === "structure" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("structure"), children: "Structure" }), _jsx("button", { className: activeView === "frontend" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("frontend"), children: "Captures" }), _jsx("button", { className: activeView === "process" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("process"), children: "Process" })] }), _jsxs("label", { className: "anlyx-search", children: [_jsx("span", { className: "anlyx-search__label", children: "Search" }), _jsxs("span", { className: "anlyx-search__control", children: [_jsx(Search, { size: 14, strokeWidth: 2.4 }), _jsx("input", { placeholder: "Search pages, endpoints, or services", type: "search" })] })] }), _jsxs("div", { className: "anlyx-sidebar__list-region", children: [_jsx(PageList, { pages: data.pages, selectedPageId: selectedPageId, onSelectPage: onSelectPage }), _jsx(EndpointList, { endpoints: data.endpoints, selectedEndpointId: selectedEndpointId, onSelectEndpoint: onSelectEndpoint }), _jsx(BackendServiceList, { data: data })] })] }));
10
+ }
11
+ function BackendServiceList({ data }) {
12
+ const services = [
13
+ ...new Map(data.flows
14
+ .flatMap((flow) => [...flow.nodes, ...flow.subFlows.flatMap((subFlow) => subFlow.nodes)])
15
+ .filter((node) => ["service", "repository", "mapper", "validator", "utility"].includes(node.type))
16
+ .map((node) => [node.id, node])).values()
17
+ ];
18
+ if (services.length === 0) {
19
+ return _jsx(_Fragment, {});
20
+ }
21
+ return (_jsxs("section", { className: "anlyx-sidebar-section", "aria-labelledby": "anlyx-services-heading", children: [_jsx("div", { className: "anlyx-section-heading", id: "anlyx-services-heading", children: "Backend Services" }), _jsx("ul", { className: "anlyx-list anlyx-list--compact", "aria-label": "Backend service list", children: services.map((node) => (_jsxs("li", { className: "anlyx-service-row", children: [_jsx(Box, { size: 14, strokeWidth: 2.3 }), _jsx("span", { children: node.label })] }, node.id))) })] }));
10
22
  }
@@ -1,7 +1,7 @@
1
- import type { CaptureStatus, ConfidenceLevel, HttpMethod } from "@anlyx/core";
1
+ import type { BridgeFlowStatus, CaptureStatus, ConfidenceLevel, HttpMethod } from "@anlyx/core";
2
2
  type StatusBadgeProps = {
3
3
  children: string;
4
- tone?: CaptureStatus | ConfidenceLevel | HttpMethod | "neutral";
4
+ tone?: CaptureStatus | ConfidenceLevel | HttpMethod | BridgeFlowStatus | "neutral";
5
5
  label?: string;
6
6
  };
7
7
  export declare function StatusBadge({ children, tone, label }: StatusBadgeProps): JSX.Element;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,9 @@ export type { ReplayControlsProps } from "./components/ReplayControls.js";
5
5
  export type { AnlyxFlowEdgeData, AnlyxFlowNodeData, AnlyxFlowRole, AnlyxReactFlowEdge, AnlyxReactFlowNode, ReactFlowModel } from "./flow/build-react-flow-model.js";
6
6
  export type { ReplayPhase, ReplayStep } from "./replay/build-replay-steps.js";
7
7
  export type { ReplayLiteState, UseReplayLiteOptions, UseReplayLiteResult } from "./replay/use-replay-lite.js";
8
+ export type { WorkspaceAppProps } from "./workspace/WorkspaceApp.js";
8
9
  export { ViewerApp } from "./viewer/ViewerApp.js";
10
+ export { WorkspaceApp } from "./workspace/WorkspaceApp.js";
9
11
  export { AnlyxAppShell } from "./components/AnlyxAppShell.js";
10
12
  export { ApiCallList } from "./components/ApiCallList.js";
11
13
  export { CaptureStatusEmptyState } from "./components/CaptureStatusEmptyState.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { ViewerApp } from "./viewer/ViewerApp.js";
2
+ export { WorkspaceApp } from "./workspace/WorkspaceApp.js";
2
3
  export { AnlyxAppShell } from "./components/AnlyxAppShell.js";
3
4
  export { ApiCallList } from "./components/ApiCallList.js";
4
5
  export { CaptureStatusEmptyState } from "./components/CaptureStatusEmptyState.js";
package/dist/mock-data.js CHANGED
@@ -36,7 +36,15 @@ export const mockScanResult = scanResultSchema.parse({
36
36
  id: "endpoint:get:/api/public/benefits/{id}",
37
37
  type: "endpoint",
38
38
  label: "GET /api/public/benefits/{id}",
39
- confidence: "high"
39
+ confidence: "high",
40
+ evidence: [
41
+ {
42
+ label: "Matched Spring mapping",
43
+ detail: '@GetMapping("/api/public/benefits/{id}")',
44
+ source: "spring-endpoint-scanner",
45
+ confidence: "high"
46
+ }
47
+ ]
40
48
  },
41
49
  {
42
50
  id: "controller:PublicBenefitController#getDetail",
@@ -44,25 +52,63 @@ export const mockScanResult = scanResultSchema.parse({
44
52
  label: "PublicBenefitController#getDetail",
45
53
  filePath: "backend/src/main/java/com/zup/benefit/PublicBenefitController.java",
46
54
  lineNumber: 24,
47
- confidence: "unknown"
55
+ confidence: "unknown",
56
+ evidence: [
57
+ {
58
+ label: "Controller method detected",
59
+ detail: "PublicBenefitController#getDetail",
60
+ source: "spring-flow-scanner",
61
+ confidence: "unknown"
62
+ }
63
+ ]
48
64
  },
49
65
  {
50
66
  id: "service:PublicBenefitService#getBenefitDetail",
51
67
  type: "service",
52
68
  label: "PublicBenefitService#getBenefitDetail",
53
- confidence: "high"
69
+ confidence: "high",
70
+ evidence: [
71
+ {
72
+ label: "Matched from controller field call",
73
+ detail: "publicBenefitService.getBenefitDetail(...)",
74
+ source: "spring-flow-scanner",
75
+ confidence: "high"
76
+ },
77
+ {
78
+ label: "Resolved service class by field type",
79
+ detail: "PublicBenefitService",
80
+ source: "spring-flow-scanner",
81
+ confidence: "high"
82
+ }
83
+ ]
54
84
  },
55
85
  {
56
86
  id: "repository:BenefitRepository#findById",
57
87
  type: "repository",
58
88
  label: "BenefitRepository#findById",
59
- confidence: "high"
89
+ confidence: "high",
90
+ evidence: [
91
+ {
92
+ label: "Repository call detected in method body",
93
+ detail: "benefitRepository.findById(...)",
94
+ source: "spring-flow-scanner",
95
+ confidence: "high"
96
+ }
97
+ ]
60
98
  },
61
99
  {
62
100
  id: "database:benefits",
63
101
  type: "database",
64
102
  label: "benefits",
65
103
  confidence: "high",
104
+ evidence: [
105
+ {
106
+ label: "Resolved repository entity",
107
+ detail: "Benefit -> benefits",
108
+ source: "spring-flow-scanner",
109
+ confidence: "high"
110
+ }
111
+ ],
66
112
  metadata: {
67
113
  tableName: "benefits"
68
114
  }
@@ -0,0 +1,2 @@
1
+ import { type EdgeProps } from "@xyflow/react";
2
+ export declare function AnlyxFlowEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, data }: EdgeProps): JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { BaseEdge, getSmoothStepPath } from "@xyflow/react";
3
+ export function AnlyxFlowEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, data }) {
4
+ const [edgePath] = getSmoothStepPath({
5
+ sourceX,
6
+ sourceY,
7
+ sourcePosition,
8
+ targetX,
9
+ targetY,
10
+ targetPosition,
11
+ borderRadius: 14
12
+ });
13
+ const tone = (data?.tone ?? "blue").toLowerCase();
14
+ return (_jsx(BaseEdge, { id: id, className: `anlyx-flow-rf-edge anlyx-flow-rf-edge--${tone}`, path: edgePath, ...(markerEnd ? { markerEnd } : {}) }));
15
+ }
@@ -0,0 +1,13 @@
1
+ import { type NodeProps } from "@xyflow/react";
2
+ export type AnlyxFlowNodeData = {
3
+ kind: "api" | "controller" | "service" | "repository" | "database" | "auth" | "result";
4
+ label: string;
5
+ value: string;
6
+ sub?: string;
7
+ badge: string;
8
+ accent: "blue" | "green" | "amber" | "violet" | "gray";
9
+ fullValue?: string;
10
+ step?: string;
11
+ state?: "taken" | "blocked" | "scanned";
12
+ };
13
+ export declare function AnlyxFlowNode({ data }: NodeProps): JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Handle, Position } from "@xyflow/react";
3
+ import { Box, Code2, Database, Globe2, Layers3, LockKeyhole, ShieldCheck } from "lucide-react";
4
+ import { Badge, Card, Tooltip } from "./ui.js";
5
+ export function AnlyxFlowNode({ data }) {
6
+ const nodeData = data;
7
+ const Icon = getIcon(nodeData.kind);
8
+ const state = nodeData.state ?? "taken";
9
+ return (_jsxs(Card, { className: `anlyx-flow-rf-node anlyx-flow-rf-node--${nodeData.accent} anlyx-flow-rf-node--${state}`, children: [_jsx(Handle, { className: "anlyx-flow-rf-handle", position: Position.Left, type: "target" }), _jsxs("div", { className: "anlyx-flow-rf-node__top", children: [_jsx("span", { className: "anlyx-flow-rf-node__icon", "aria-hidden": "true", children: _jsx(Icon, { size: 14, strokeWidth: 2.25 }) }), _jsx("span", { className: "anlyx-flow-rf-node__label", children: nodeData.label }), nodeData.step ? _jsx("span", { className: "anlyx-flow-rf-node__step", children: nodeData.step }) : null] }), _jsx(Tooltip, { content: nodeData.fullValue ?? nodeData.value, children: _jsx("p", { className: "anlyx-flow-rf-node__value", children: nodeData.value }) }), nodeData.sub ? _jsx("p", { className: "anlyx-flow-rf-node__sub", children: nodeData.sub }) : null, _jsx(Badge, { tone: nodeData.accent === "violet" ? "violet" : nodeData.accent, children: nodeData.badge }), _jsx(Handle, { className: "anlyx-flow-rf-handle", position: Position.Right, type: "source" })] }));
10
+ }
11
+ function getIcon(kind) {
12
+ switch (kind) {
13
+ case "api":
14
+ return Globe2;
15
+ case "controller":
16
+ return Code2;
17
+ case "service":
18
+ return Layers3;
19
+ case "repository":
20
+ return Box;
21
+ case "database":
22
+ return Database;
23
+ case "auth":
24
+ return LockKeyhole;
25
+ case "result":
26
+ return ShieldCheck;
27
+ }
28
+ }
@@ -0,0 +1,2 @@
1
+ import type { FlowDrawerProps } from "./types.js";
2
+ export declare function FlowDrawer({ selectedEvent, events, latestAction, scannedHints, loadError }: FlowDrawerProps): JSX.Element;