@principal-ai/principal-view-react 0.7.34 → 0.7.35

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/dist/index.d.ts CHANGED
@@ -14,8 +14,6 @@ export { NodeInfoPanel } from './components/NodeInfoPanel';
14
14
  export type { NodeInfoPanelProps } from './components/NodeInfoPanel';
15
15
  export { ConfigurationSelector } from './components/ConfigurationSelector';
16
16
  export type { ConfigurationSelectorProps } from './components/ConfigurationSelector';
17
- export { TestEventPanel } from './components/TestEventPanel';
18
- export type { TestEventPanelProps } from './components/TestEventPanel';
19
17
  export { GenericNode } from './nodes/GenericNode';
20
18
  export type { GenericNodeProps } from './nodes/GenericNode';
21
19
  export { CustomNode } from './nodes/CustomNode';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EAEvB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,2CAA2C,CAAC;AAGnD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAErF,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,YAAY,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAGvE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EAEvB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,2CAA2C,CAAC;AAGnD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -10,7 +10,6 @@ export { GraphRenderer } from './components/GraphRenderer';
10
10
  export { EdgeInfoPanel } from './components/EdgeInfoPanel';
11
11
  export { NodeInfoPanel } from './components/NodeInfoPanel';
12
12
  export { ConfigurationSelector } from './components/ConfigurationSelector';
13
- export { TestEventPanel } from './components/TestEventPanel';
14
13
  // Export node/edge renderers
15
14
  export { GenericNode } from './nodes/GenericNode';
16
15
  export { CustomNode } from './nodes/CustomNode';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,oBAAoB;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQ3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAG7D,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mBAAmB;AACnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,oBAAoB;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQ3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mBAAmB;AACnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/principal-view-react",
3
- "version": "0.7.34",
3
+ "version": "0.7.35",
4
4
  "description": "React components for graph-based principal view framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,7 +16,7 @@
16
16
  "dependencies": {
17
17
  "@principal-ade/industry-theme": "0.1.7",
18
18
  "@principal-ai/codebase-composition": "^0.2.41",
19
- "@principal-ai/principal-view-core": "^0.7.12",
19
+ "@principal-ai/principal-view-core": "0.10.4",
20
20
  "@xyflow/react": "12.0.0",
21
21
  "elkjs": "0.9.0",
22
22
  "js-yaml": "4.1.1",
@@ -1,7 +1,7 @@
1
- import { describe, expect, test, beforeEach, beforeAll, mock } from 'bun:test';
1
+ import { describe, expect, test, mock } from 'bun:test';
2
2
  import React, { createRef } from 'react';
3
- import { render, fireEvent, act } from '@testing-library/react';
4
- import { GraphRenderer, type GraphRendererHandle, type PendingChanges } from './GraphRenderer';
3
+ import { render } from '@testing-library/react';
4
+ import { GraphRenderer, type GraphRendererHandle } from './GraphRenderer';
5
5
  import type { ExtendedCanvas } from '@principal-ai/principal-view-core/browser';
6
6
  import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
7
7
  import { Window } from 'happy-dom';
package/src/index.ts CHANGED
@@ -46,9 +46,6 @@ export type { NodeInfoPanelProps } from './components/NodeInfoPanel';
46
46
  export { ConfigurationSelector } from './components/ConfigurationSelector';
47
47
  export type { ConfigurationSelectorProps } from './components/ConfigurationSelector';
48
48
 
49
- export { TestEventPanel } from './components/TestEventPanel';
50
- export type { TestEventPanelProps } from './components/TestEventPanel';
51
-
52
49
  // Export node/edge renderers
53
50
  export { GenericNode } from './nodes/GenericNode';
54
51
  export type { GenericNodeProps } from './nodes/GenericNode';
@@ -23,192 +23,6 @@ const meta = {
23
23
  export default meta;
24
24
  type Story = StoryObj<typeof meta>;
25
25
 
26
- /**
27
- * Canvas showing a single node with ALL fields populated and labeled
28
- */
29
- const allFieldsCanvas: ExtendedCanvas = {
30
- nodes: [
31
- {
32
- id: 'all-fields-node',
33
- type: 'text',
34
- x: 200,
35
- y: 150,
36
- width: 160,
37
- height: 120,
38
- text: 'Display Label (from dataSchema)',
39
- color: '#6366f1', // color field
40
- pv: {
41
- nodeType: 'fully-populated',
42
- shape: 'rectangle', // shape field
43
- icon: 'Server', // icon field
44
- fill: '#6366f1', // pv.fill (takes priority over node.color)
45
- stroke: '#4f46e5', // pv.stroke (border color)
46
- dataSchema: {
47
- name: { type: 'string', displayInLabel: true },
48
- description: { type: 'string', displayInLabel: true },
49
- status: { type: 'string', displayInLabel: false },
50
- },
51
- states: {
52
- idle: { color: '#94a3b8', icon: 'Circle', label: 'Idle' },
53
- active: { color: '#22c55e', icon: 'CheckCircle', label: 'Active' },
54
- error: { color: '#ef4444', icon: 'XCircle', label: 'Error' },
55
- },
56
- },
57
- },
58
- ],
59
- edges: [],
60
- pv: {
61
- version: '1.0.0',
62
- name: 'All Fields Audit',
63
- edgeTypes: {},
64
- },
65
- };
66
-
67
- /**
68
- * Canvas showing nodes with each field labeled for reference
69
- */
70
- const labeledFieldsCanvas: ExtendedCanvas = {
71
- nodes: [
72
- // Reference node with annotations
73
- {
74
- id: 'labeled-node',
75
- type: 'text',
76
- x: 300,
77
- y: 200,
78
- width: 180,
79
- height: 140,
80
- text: 'API Gateway',
81
- color: '#3b82f6',
82
- pv: {
83
- nodeType: 'server',
84
- shape: 'rectangle',
85
- icon: 'Server',
86
- fill: '#3b82f6',
87
- stroke: '#2563eb',
88
- dataSchema: {
89
- name: { type: 'string', displayInLabel: true },
90
- },
91
- states: {
92
- processing: { color: '#3b82f6', icon: 'Loader', label: 'Processing' },
93
- },
94
- },
95
- },
96
- // Icon label
97
- {
98
- id: 'icon-label',
99
- type: 'text',
100
- x: 100,
101
- y: 140,
102
- width: 120,
103
- height: 30,
104
- text: 'ICON',
105
- color: '#f97316',
106
- pv: {
107
- nodeType: 'label',
108
- shape: 'rectangle',
109
- fill: '#f97316',
110
- dataSchema: {},
111
- },
112
- },
113
- // Label field annotation
114
- {
115
- id: 'label-annotation',
116
- type: 'text',
117
- x: 100,
118
- y: 200,
119
- width: 120,
120
- height: 30,
121
- text: 'LABEL',
122
- color: '#f97316',
123
- pv: {
124
- nodeType: 'label',
125
- shape: 'rectangle',
126
- fill: '#f97316',
127
- dataSchema: {},
128
- },
129
- },
130
- // State badge annotation
131
- {
132
- id: 'state-annotation',
133
- type: 'text',
134
- x: 100,
135
- y: 260,
136
- width: 120,
137
- height: 30,
138
- text: 'STATE BADGE',
139
- color: '#f97316',
140
- pv: {
141
- nodeType: 'label',
142
- shape: 'rectangle',
143
- fill: '#f97316',
144
- dataSchema: {},
145
- },
146
- },
147
- // Border/stroke annotation
148
- {
149
- id: 'stroke-annotation',
150
- type: 'text',
151
- x: 540,
152
- y: 200,
153
- width: 140,
154
- height: 30,
155
- text: 'STROKE/BORDER',
156
- color: '#f97316',
157
- pv: {
158
- nodeType: 'label',
159
- shape: 'rectangle',
160
- fill: '#f97316',
161
- dataSchema: {},
162
- },
163
- },
164
- ],
165
- edges: [
166
- {
167
- id: 'icon-pointer',
168
- fromNode: 'icon-label',
169
- toNode: 'labeled-node',
170
- fromSide: 'right',
171
- toSide: 'left',
172
- pv: { edgeType: 'pointer' },
173
- },
174
- {
175
- id: 'label-pointer',
176
- fromNode: 'label-annotation',
177
- toNode: 'labeled-node',
178
- fromSide: 'right',
179
- toSide: 'left',
180
- pv: { edgeType: 'pointer' },
181
- },
182
- {
183
- id: 'state-pointer',
184
- fromNode: 'state-annotation',
185
- toNode: 'labeled-node',
186
- fromSide: 'right',
187
- toSide: 'left',
188
- pv: { edgeType: 'pointer' },
189
- },
190
- {
191
- id: 'stroke-pointer',
192
- fromNode: 'stroke-annotation',
193
- toNode: 'labeled-node',
194
- fromSide: 'left',
195
- toSide: 'right',
196
- pv: { edgeType: 'pointer' },
197
- },
198
- ],
199
- pv: {
200
- version: '1.0.0',
201
- name: 'Labeled Fields',
202
- edgeTypes: {
203
- pointer: {
204
- style: 'dashed',
205
- color: '#f97316',
206
- directed: true,
207
- },
208
- },
209
- },
210
- };
211
-
212
26
  /**
213
27
  * Canvas showing all visual variations of node fields
214
28
  */
@@ -1,171 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import { renderNarrative } from '@principal-ai/principal-view-core/browser';
3
- import type { NarrativeTemplate, OtelEvent } from '@principal-ai/principal-view-core/browser';
4
- import { useTheme } from '@principal-ade/industry-theme';
5
-
6
- export interface NarrativeRendererProps {
7
- /** Narrative template to use for rendering */
8
- template: NarrativeTemplate;
9
-
10
- /** OTEL events to render */
11
- events: OtelEvent[];
12
-
13
- /** Optional CSS class name */
14
- className?: string;
15
-
16
- /** Optional custom style */
17
- style?: React.CSSProperties;
18
-
19
- /** Show metadata panel */
20
- showMetadata?: boolean;
21
- }
22
-
23
- /**
24
- * Renders OTEL events as a human-readable narrative using a template
25
- */
26
- export const NarrativeRenderer: React.FC<NarrativeRendererProps> = ({
27
- template,
28
- events,
29
- className,
30
- style,
31
- showMetadata = false,
32
- }) => {
33
- const { theme } = useTheme();
34
-
35
- // Render the narrative
36
- const result = useMemo(() => {
37
- try {
38
- return renderNarrative(template, events);
39
- } catch (error) {
40
- return {
41
- text: `Error rendering narrative: ${error instanceof Error ? error.message : 'Unknown error'}`,
42
- scenarioId: 'error',
43
- metadata: {
44
- eventCount: events.length,
45
- spanCount: 0,
46
- logCount: 0,
47
- },
48
- };
49
- }
50
- }, [template, events]);
51
-
52
- // Parse narrative text to add syntax highlighting
53
- const renderHighlightedText = (text: string) => {
54
- const lines = text.split('\n');
55
-
56
- return lines.map((line, idx) => {
57
- // Determine line style based on content
58
- let lineStyle: React.CSSProperties = {};
59
- let content = line;
60
-
61
- // Status indicators (✅ ❌ ⚠️ 📋)
62
- if (/^[✅❌⚠️📋]/.test(line)) {
63
- lineStyle = {
64
- fontWeight: 'bold',
65
- fontSize: '16px',
66
- marginTop: idx > 0 ? '8px' : '0',
67
- marginBottom: '4px',
68
- };
69
- }
70
- // Separators (━━━━)
71
- else if (/^━+/.test(line)) {
72
- lineStyle = {
73
- color: theme.colors.border,
74
- opacity: 0.6,
75
- };
76
- }
77
- // Arrow items (→)
78
- else if (/^(\s*)→/.test(line)) {
79
- const indent = line.match(/^(\s*)/)?.[1] || '';
80
- lineStyle = {
81
- color: theme.colors.text,
82
- fontWeight: indent.length === 0 ? 'bold' : 'normal',
83
- marginTop: indent.length === 0 ? '12px' : '4px',
84
- };
85
- }
86
- // Bullet items (•)
87
- else if (/^\s+•/.test(line)) {
88
- lineStyle = {
89
- color: theme.colors.textMuted,
90
- paddingLeft: '8px',
91
- };
92
- }
93
- // Section headers (UPPERCASE at start)
94
- else if (/^[A-Z\s]+:/.test(line)) {
95
- lineStyle = {
96
- fontWeight: 'bold',
97
- marginTop: '8px',
98
- color: theme.colors.text,
99
- };
100
- }
101
-
102
- return (
103
- <div key={idx} style={lineStyle}>
104
- {content}
105
- </div>
106
- );
107
- });
108
- };
109
-
110
- return (
111
- <div
112
- className={className}
113
- style={{
114
- width: '100%',
115
- height: '100%',
116
- display: 'flex',
117
- flexDirection: 'column',
118
- ...style,
119
- }}
120
- >
121
- {/* Narrative Text */}
122
- <div
123
- style={{
124
- flex: 1,
125
- overflow: 'auto',
126
- padding: '20px',
127
- fontFamily: theme.fonts.monospace,
128
- fontSize: '14px',
129
- lineHeight: '1.6',
130
- color: theme.colors.text,
131
- backgroundColor: theme.colors.background,
132
- whiteSpace: 'pre-wrap',
133
- wordWrap: 'break-word',
134
- }}
135
- >
136
- {renderHighlightedText(result.text)}
137
- </div>
138
-
139
- {/* Metadata Panel (optional) */}
140
- {showMetadata && (
141
- <div
142
- style={{
143
- borderTop: `1px solid ${theme.colors.border}`,
144
- padding: '12px 20px',
145
- backgroundColor: theme.colors.surface,
146
- fontSize: '12px',
147
- color: theme.colors.textMuted,
148
- fontFamily: theme.fonts.monospace,
149
- }}
150
- >
151
- <div style={{ marginBottom: '4px' }}>
152
- <strong style={{ color: theme.colors.text }}>Template:</strong> {template.name}
153
- </div>
154
- <div style={{ marginBottom: '4px' }}>
155
- <strong style={{ color: theme.colors.text }}>Scenario:</strong> {result.scenarioId}
156
- </div>
157
- <div>
158
- <strong style={{ color: theme.colors.text }}>Events:</strong> {result.metadata.eventCount} total
159
- ({result.metadata.spanCount} spans, {result.metadata.logCount} logs)
160
- </div>
161
- {result.metadata.timeRange && (
162
- <div style={{ marginTop: '4px' }}>
163
- <strong style={{ color: theme.colors.text }}>Duration:</strong>{' '}
164
- {Number(result.metadata.timeRange.end) - Number(result.metadata.timeRange.start)}ms
165
- </div>
166
- )}
167
- </div>
168
- )}
169
- </div>
170
- );
171
- };
@@ -1,588 +0,0 @@
1
- import React, { useState, useMemo } from 'react';
2
- import { useTheme } from '@principal-ade/industry-theme';
3
- import { HelpCircle } from 'lucide-react';
4
- import yaml from 'js-yaml';
5
- import type { NarrativeTemplate, JsonValue } from '@principal-ai/principal-view-core/browser';
6
- import { NarrativeRenderer } from './NarrativeRenderer';
7
- import { convertToOtelEvents } from '../utils/narrative-converter';
8
-
9
- interface SpanEvent {
10
- time: number;
11
- name: string;
12
- attributes: Record<string, string | number | boolean>;
13
- }
14
-
15
- interface TestSpan {
16
- id: string;
17
- name: string;
18
- startTime: number;
19
- endTime?: number;
20
- duration?: number;
21
- attributes: Record<string, string | number | boolean>;
22
- events: SpanEvent[];
23
- status: 'OK' | 'ERROR';
24
- errorMessage?: string;
25
- }
26
-
27
- // OTEL Log types
28
- export type OtelSeverity = 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
29
-
30
- export interface OtelLog {
31
- timestamp: number;
32
- severity: OtelSeverity;
33
- body: string | Record<string, unknown>;
34
- resource: Record<string, string | number>;
35
- attributes?: Record<string, JsonValue>;
36
- traceId?: string;
37
- spanId?: string;
38
- }
39
-
40
- // Timeline item (event or log)
41
- interface TimelineItem {
42
- type: 'event' | 'log';
43
- time: number;
44
- // For events
45
- name?: string;
46
- attributes?: Record<string, JsonValue>;
47
- // For logs
48
- severity?: OtelSeverity;
49
- body?: string | Record<string, unknown>;
50
- resource?: Record<string, string | number>;
51
- }
52
-
53
- // View mode type
54
- export type ViewMode = 'raw' | 'narrative';
55
-
56
- export interface TestEventPanelProps {
57
- spans: TestSpan[];
58
- logs?: OtelLog[]; // Optional for backward compatibility
59
- currentSpanIndex: number;
60
- currentEventIndex: number;
61
- highlightedPhase?: string; // 'setup' | 'execution' | 'assertion'
62
- onSpanIndexChange?: (index: number) => void;
63
-
64
- // Narrative view props
65
- viewMode?: ViewMode;
66
- narrativeTemplate?: NarrativeTemplate;
67
- onViewModeChange?: (mode: ViewMode) => void;
68
- showNarrativeMetadata?: boolean;
69
- }
70
-
71
- // Helper functions for log severity
72
- function getSeverityColor(severity: OtelSeverity): string {
73
- const colors = {
74
- TRACE: '#6b7280',
75
- DEBUG: '#60a5fa',
76
- INFO: '#4ade80',
77
- WARN: '#fbbf24',
78
- ERROR: '#f87171',
79
- FATAL: '#dc2626',
80
- };
81
- return colors[severity] || '#9ca3af';
82
- }
83
-
84
- function getSeverityIcon(severity: OtelSeverity): string {
85
- const icons = {
86
- TRACE: '○',
87
- DEBUG: '◐',
88
- INFO: '●',
89
- WARN: '⚠',
90
- ERROR: '✕',
91
- FATAL: '☠',
92
- };
93
- return icons[severity] || '•';
94
- }
95
-
96
- export const TestEventPanel: React.FC<TestEventPanelProps> = ({
97
- spans,
98
- logs = [],
99
- currentSpanIndex,
100
- currentEventIndex,
101
- highlightedPhase,
102
- onSpanIndexChange,
103
- viewMode = 'raw',
104
- narrativeTemplate,
105
- onViewModeChange,
106
- showNarrativeMetadata = false,
107
- }) => {
108
- const { theme } = useTheme();
109
- const [showHelp, setShowHelp] = useState(false);
110
-
111
- const currentSpan = spans[currentSpanIndex];
112
-
113
- // Convert current span to OtelEvents for narrative rendering
114
- const otelEvents = useMemo(() => {
115
- if (!currentSpan || viewMode !== 'narrative') return [];
116
- return convertToOtelEvents(currentSpan, logs);
117
- }, [currentSpan, logs, viewMode]);
118
-
119
- const handlePrevTest = () => {
120
- if (currentSpanIndex > 0 && onSpanIndexChange) {
121
- onSpanIndexChange(currentSpanIndex - 1);
122
- }
123
- };
124
-
125
- const handleNextTest = () => {
126
- if (currentSpanIndex < spans.length - 1 && onSpanIndexChange) {
127
- onSpanIndexChange(currentSpanIndex + 1);
128
- }
129
- };
130
-
131
- // Build interleaved timeline
132
- const timeline = useMemo(() => {
133
- if (!currentSpan) return [];
134
-
135
- const items: TimelineItem[] = [
136
- // Span events
137
- ...currentSpan.events.slice(0, currentEventIndex + 1).map((event) => ({
138
- type: 'event' as const,
139
- time: event.time,
140
- name: event.name,
141
- attributes: event.attributes,
142
- })),
143
-
144
- // Correlated logs (matching current span's traceId)
145
- ...logs
146
- .filter((log) => log.traceId === currentSpan.id)
147
- .map((log) => ({
148
- type: 'log' as const,
149
- time: typeof log.timestamp === 'number' ? log.timestamp : new Date(log.timestamp).getTime(),
150
- severity: log.severity,
151
- body: log.body,
152
- resource: log.resource,
153
- attributes: log.attributes,
154
- })),
155
- ].sort((a, b) => a.time - b.time);
156
-
157
- return items;
158
- }, [currentSpan, currentEventIndex, logs]);
159
-
160
- return (
161
- <div
162
- style={{
163
- width: '100%',
164
- height: '100%',
165
- backgroundColor: theme.colors.background,
166
- color: theme.colors.text,
167
- fontFamily: theme.fonts.monospace,
168
- fontSize: '14px',
169
- boxSizing: 'border-box',
170
- display: 'flex',
171
- flexDirection: 'column',
172
- }}
173
- >
174
- {/* Static Header */}
175
- <div
176
- style={{
177
- padding: '20px 20px 0 20px',
178
- backgroundColor: theme.colors.background,
179
- borderBottom: `1px solid ${theme.colors.border}`,
180
- flexShrink: 0,
181
- }}
182
- >
183
- {/* Test Navigation - replacing title */}
184
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
185
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
186
- <button
187
- onClick={handlePrevTest}
188
- disabled={currentSpanIndex === 0}
189
- style={{
190
- padding: '4px 12px',
191
- background: theme.colors.surface,
192
- border: `1px solid ${theme.colors.border}`,
193
- borderRadius: '4px',
194
- color: currentSpanIndex === 0 ? theme.colors.textMuted : theme.colors.text,
195
- cursor: currentSpanIndex === 0 ? 'not-allowed' : 'pointer',
196
- fontSize: '14px',
197
- opacity: currentSpanIndex === 0 ? 0.5 : 1,
198
- }}
199
- >
200
- ← Prev
201
- </button>
202
- <div style={{ fontSize: '14px', fontWeight: 'bold' }}>
203
- Test {currentSpanIndex + 1} of {spans.length}
204
- </div>
205
- <button
206
- onClick={handleNextTest}
207
- disabled={currentSpanIndex === spans.length - 1}
208
- style={{
209
- padding: '4px 12px',
210
- background: theme.colors.surface,
211
- border: `1px solid ${theme.colors.border}`,
212
- borderRadius: '4px',
213
- color: currentSpanIndex === spans.length - 1 ? theme.colors.textMuted : theme.colors.text,
214
- cursor: currentSpanIndex === spans.length - 1 ? 'not-allowed' : 'pointer',
215
- fontSize: '14px',
216
- opacity: currentSpanIndex === spans.length - 1 ? 0.5 : 1,
217
- }}
218
- >
219
- Next →
220
- </button>
221
- </div>
222
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
223
- <div style={{ fontSize: '13px', color: theme.colors.textMuted }}>
224
- <span style={{ color: '#4ade80' }}>All Passed ✓</span>
225
- </div>
226
- <button
227
- onClick={() => setShowHelp(true)}
228
- style={{
229
- background: 'transparent',
230
- border: 'none',
231
- cursor: 'pointer',
232
- padding: '4px',
233
- display: 'flex',
234
- alignItems: 'center',
235
- color: theme.colors.textMuted,
236
- }}
237
- onMouseEnter={(e) => {
238
- e.currentTarget.style.color = theme.colors.text;
239
- }}
240
- onMouseLeave={(e) => {
241
- e.currentTarget.style.color = theme.colors.textMuted;
242
- }}
243
- >
244
- <HelpCircle size={20} />
245
- </button>
246
- </div>
247
- </div>
248
-
249
- {/* View Mode Toggle */}
250
- {narrativeTemplate && onViewModeChange && (
251
- <div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
252
- <button
253
- onClick={() => onViewModeChange('raw')}
254
- style={{
255
- padding: '6px 12px',
256
- background: viewMode === 'raw' ? theme.colors.primary : theme.colors.surface,
257
- border: `1px solid ${theme.colors.border}`,
258
- borderRadius: '4px',
259
- color: viewMode === 'raw' ? '#ffffff' : theme.colors.text,
260
- cursor: 'pointer',
261
- fontSize: '13px',
262
- fontWeight: viewMode === 'raw' ? 'bold' : 'normal',
263
- }}
264
- >
265
- Raw Events
266
- </button>
267
- <button
268
- onClick={() => onViewModeChange('narrative')}
269
- style={{
270
- padding: '6px 12px',
271
- background: viewMode === 'narrative' ? theme.colors.primary : theme.colors.surface,
272
- border: `1px solid ${theme.colors.border}`,
273
- borderRadius: '4px',
274
- color: viewMode === 'narrative' ? '#ffffff' : theme.colors.text,
275
- cursor: 'pointer',
276
- fontSize: '13px',
277
- fontWeight: viewMode === 'narrative' ? 'bold' : 'normal',
278
- }}
279
- >
280
- Narrative
281
- </button>
282
- </div>
283
- )}
284
-
285
- <div style={{ fontSize: '13px', color: theme.colors.textMuted, marginBottom: '15px' }}>
286
- Test: {currentSpan?.name || 'Loading...'}
287
- </div>
288
- </div>
289
-
290
- {/* Help Modal */}
291
- {showHelp && (
292
- <div
293
- style={{
294
- position: 'fixed',
295
- top: 0,
296
- left: 0,
297
- right: 0,
298
- bottom: 0,
299
- backgroundColor: 'rgba(0, 0, 0, 0.7)',
300
- display: 'flex',
301
- alignItems: 'center',
302
- justifyContent: 'center',
303
- zIndex: 9999,
304
- }}
305
- onClick={() => setShowHelp(false)}
306
- >
307
- <div
308
- style={{
309
- backgroundColor: theme.colors.background,
310
- color: theme.colors.text,
311
- padding: '24px',
312
- borderRadius: '8px',
313
- maxWidth: '600px',
314
- border: `1px solid ${theme.colors.border}`,
315
- }}
316
- onClick={(e) => e.stopPropagation()}
317
- >
318
- <div style={{ fontWeight: 'bold', fontSize: '18px', marginBottom: '16px' }}>
319
- How to Read This Panel
320
- </div>
321
- <div style={{ fontSize: '14px', marginBottom: '16px', lineHeight: '1.6' }}>
322
- <p style={{ marginBottom: '12px' }}>
323
- <strong>Timeline shows both events and logs:</strong>
324
- </p>
325
- <ul style={{ marginLeft: '20px', marginBottom: '16px' }}>
326
- <li style={{ marginBottom: '8px' }}>
327
- <span style={{ color: '#f59e0b' }}>🟧 Events</span> - Structured lifecycle points
328
- </li>
329
- <li style={{ marginBottom: '8px' }}>
330
- <span style={{ color: '#4ade80' }}>● Logs</span> - Standalone log records (color = severity)
331
- </li>
332
- <li style={{ marginBottom: '8px' }}>
333
- <span style={{ color: '#60a5fa' }}>Blue = Test file</span>
334
- </li>
335
- <li>
336
- <span style={{ color: '#4ade80' }}>Green → Code under test</span>
337
- </li>
338
- </ul>
339
- </div>
340
- <button
341
- onClick={() => setShowHelp(false)}
342
- style={{
343
- padding: '8px 16px',
344
- backgroundColor: theme.colors.primary,
345
- color: '#ffffff',
346
- border: 'none',
347
- borderRadius: '4px',
348
- cursor: 'pointer',
349
- fontSize: '14px',
350
- fontWeight: 500,
351
- }}
352
- >
353
- Got it
354
- </button>
355
- </div>
356
- </div>
357
- )}
358
-
359
- {/* Scrollable Content */}
360
- <div
361
- style={{
362
- flex: 1,
363
- overflow: 'auto',
364
- padding: viewMode === 'narrative' ? '0' : '20px',
365
- }}
366
- >
367
- {/* Narrative View */}
368
- {viewMode === 'narrative' && narrativeTemplate && currentSpan ? (
369
- <NarrativeRenderer
370
- template={narrativeTemplate}
371
- events={otelEvents}
372
- showMetadata={showNarrativeMetadata}
373
- />
374
- ) : viewMode === 'narrative' && !narrativeTemplate ? (
375
- <div
376
- style={{
377
- padding: '40px 20px',
378
- textAlign: 'center',
379
- color: theme.colors.textMuted,
380
- }}
381
- >
382
- <div style={{ fontSize: '16px', marginBottom: '12px' }}>ⓘ No narrative template available</div>
383
- <div style={{ fontSize: '14px', lineHeight: '1.6' }}>
384
- Create a narrative template to see a human-readable
385
- <br />
386
- summary of this test execution.
387
- </div>
388
- <button
389
- onClick={() => onViewModeChange?.('raw')}
390
- style={{
391
- marginTop: '20px',
392
- padding: '8px 16px',
393
- background: theme.colors.primary,
394
- color: '#ffffff',
395
- border: 'none',
396
- borderRadius: '4px',
397
- cursor: 'pointer',
398
- fontSize: '14px',
399
- }}
400
- >
401
- View Raw Events
402
- </button>
403
- </div>
404
- ) : null}
405
-
406
- {/* Raw Events View (Timeline) */}
407
- {viewMode === 'raw' && currentSpan && (
408
- <div>
409
- {timeline.map((item, idx) => {
410
- if (item.type === 'event') {
411
- // SPAN EVENT RENDERING
412
- const filepath = item.attributes?.['code.filepath'] as string;
413
- const lineno = item.attributes?.['code.lineno'] as number;
414
- const isCodeUnderTest = filepath && filepath !== 'GraphConverter.test.ts';
415
-
416
- // Determine which phase this event belongs to
417
- const eventPhase = item.name?.split('.')[0]; // 'setup', 'execution', 'assertion'
418
- const isHighlighted = highlightedPhase === eventPhase;
419
-
420
- return (
421
- <div
422
- key={idx}
423
- style={{
424
- marginBottom: '12px',
425
- paddingBottom: '12px',
426
- paddingLeft: '12px',
427
- borderBottom: idx < timeline.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
428
- borderLeft: '3px solid #f59e0b',
429
- opacity: highlightedPhase && !isHighlighted ? 0.4 : 1,
430
- transition: 'opacity 0.2s ease',
431
- transform: isHighlighted ? 'scale(1.02)' : 'scale(1)',
432
- backgroundColor: isHighlighted ? theme.colors.surface : 'transparent',
433
- padding: isHighlighted ? '8px 8px 8px 12px' : '0 0 12px 12px',
434
- borderRadius: '4px',
435
- }}
436
- >
437
- <div
438
- style={{
439
- display: 'flex',
440
- justifyContent: 'space-between',
441
- alignItems: 'center',
442
- marginBottom: '4px',
443
- gap: '8px',
444
- }}
445
- >
446
- <div style={{ color: '#f59e0b', fontSize: '13px', fontWeight: 'bold', flexShrink: 0 }}>
447
- EVENT: {item.name}
448
- </div>
449
- {filepath && (
450
- <div
451
- style={{
452
- fontSize: '12px',
453
- color: isCodeUnderTest ? '#4ade80' : '#60a5fa',
454
- fontFamily: 'monospace',
455
- background: isCodeUnderTest ? '#064e3b' : '#1e3a8a',
456
- padding: '2px 6px',
457
- borderRadius: '3px',
458
- flexShrink: 1,
459
- minWidth: 0,
460
- overflow: 'hidden',
461
- textOverflow: 'ellipsis',
462
- whiteSpace: 'nowrap',
463
- }}
464
- >
465
- {isCodeUnderTest && '→ '}
466
- {filepath}:{lineno}
467
- </div>
468
- )}
469
- </div>
470
- <pre
471
- style={{
472
- background: theme.colors.surface,
473
- padding: '8px',
474
- borderRadius: '4px',
475
- margin: 0,
476
- fontSize: '13px',
477
- lineHeight: '1.5',
478
- overflow: 'auto',
479
- maxWidth: '100%',
480
- }}
481
- >
482
- {yaml.dump(
483
- Object.fromEntries(
484
- Object.entries(item.attributes || {}).filter(
485
- ([key]) => key !== 'code.filepath' && key !== 'code.lineno'
486
- )
487
- ),
488
- { indent: 2, lineWidth: -1 }
489
- )}
490
- </pre>
491
- </div>
492
- );
493
- } else {
494
- // OTEL LOG RENDERING
495
- const serviceName = item.resource?.['service.name'];
496
- const severityColor = getSeverityColor(item.severity!);
497
-
498
- return (
499
- <div
500
- key={idx}
501
- style={{
502
- marginBottom: '12px',
503
- paddingBottom: '12px',
504
- paddingLeft: '12px',
505
- borderBottom: idx < timeline.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
506
- borderLeft: `3px solid ${severityColor}`,
507
- }}
508
- >
509
- <div
510
- style={{
511
- display: 'flex',
512
- justifyContent: 'space-between',
513
- alignItems: 'center',
514
- marginBottom: '4px',
515
- }}
516
- >
517
- <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
518
- <span style={{ fontSize: '16px' }}>{getSeverityIcon(item.severity!)}</span>
519
- <span
520
- style={{
521
- color: severityColor,
522
- fontSize: '13px',
523
- fontWeight: 'bold',
524
- }}
525
- >
526
- LOG: {item.severity}
527
- </span>
528
- </div>
529
- {serviceName && (
530
- <div
531
- style={{
532
- fontSize: '12px',
533
- color: '#9ca3af',
534
- background: '#1e293b',
535
- padding: '2px 6px',
536
- borderRadius: '3px',
537
- }}
538
- >
539
- {serviceName}
540
- </div>
541
- )}
542
- </div>
543
-
544
- {/* Log body */}
545
- <div
546
- style={{
547
- background: theme.colors.surface,
548
- padding: '8px',
549
- borderRadius: '4px',
550
- marginBottom: item.attributes && Object.keys(item.attributes).length > 0 ? '8px' : '0',
551
- fontSize: '13px',
552
- }}
553
- >
554
- {typeof item.body === 'string' ? (
555
- item.body
556
- ) : (
557
- <pre style={{ margin: 0, fontSize: '13px', lineHeight: '1.5' }}>
558
- {yaml.dump(item.body, { indent: 2, lineWidth: -1 })}
559
- </pre>
560
- )}
561
- </div>
562
-
563
- {/* Log attributes */}
564
- {item.attributes && Object.keys(item.attributes).length > 0 && (
565
- <pre
566
- style={{
567
- background: theme.colors.surface,
568
- padding: '8px',
569
- borderRadius: '4px',
570
- margin: 0,
571
- fontSize: '12px',
572
- lineHeight: '1.5',
573
- opacity: 0.8,
574
- }}
575
- >
576
- {yaml.dump(item.attributes, { indent: 2, lineWidth: -1 })}
577
- </pre>
578
- )}
579
- </div>
580
- );
581
- }
582
- })}
583
- </div>
584
- )}
585
- </div>
586
- </div>
587
- );
588
- };
@@ -1,157 +0,0 @@
1
- /**
2
- * Utilities for converting TestEventPanel data to OtelEvent format
3
- * for use with the narrative renderer
4
- */
5
-
6
- import type { OtelEvent } from '@principal-ai/principal-view-core/browser';
7
- import type { OtelLog, OtelSeverity } from '../components/TestEventPanel';
8
-
9
- // TestEventPanel types (matching TestEventPanel.tsx)
10
- interface SpanEvent {
11
- time: number;
12
- name: string;
13
- attributes: Record<string, string | number | boolean>;
14
- }
15
-
16
- interface TestSpan {
17
- id: string;
18
- name: string;
19
- startTime: number;
20
- endTime?: number;
21
- duration?: number;
22
- attributes: Record<string, string | number | boolean>;
23
- events: SpanEvent[];
24
- status: 'OK' | 'ERROR';
25
- errorMessage?: string;
26
- }
27
-
28
- /**
29
- * Convert OtelSeverity to severity number (OpenTelemetry spec)
30
- */
31
- function severityToNumber(severity: OtelSeverity): number {
32
- const severityMap: Record<OtelSeverity, number> = {
33
- TRACE: 1,
34
- DEBUG: 5,
35
- INFO: 9,
36
- WARN: 13,
37
- ERROR: 17,
38
- FATAL: 21,
39
- };
40
- return severityMap[severity] || 9;
41
- }
42
-
43
- /**
44
- * Convert TestSpan events to OtelEvent spans
45
- */
46
- function convertSpanEvents(span: TestSpan): OtelEvent[] {
47
- return span.events.map((event) => ({
48
- name: event.name,
49
- timestamp: event.time,
50
- type: 'span' as const,
51
- spanId: span.id,
52
- traceId: span.id, // Use span ID as trace ID for single-span tests
53
- attributes: event.attributes,
54
- }));
55
- }
56
-
57
- /**
58
- * Convert OtelLogs to OtelEvent logs
59
- */
60
- function convertLogs(logs: OtelLog[]): OtelEvent[] {
61
- return logs.map((log) => {
62
- // Filter out null values from attributes (OtelAttributeValue doesn't include null)
63
- const filterNullValues = (obj: Record<string, any> | undefined) => {
64
- if (!obj) return {};
65
- return Object.fromEntries(
66
- Object.entries(obj).filter(([, value]) => value !== null)
67
- );
68
- };
69
-
70
- return {
71
- name: 'log',
72
- timestamp: typeof log.timestamp === 'number' ? log.timestamp : new Date(log.timestamp).getTime(),
73
- type: 'log' as const,
74
- spanId: log.spanId,
75
- traceId: log.traceId,
76
- severityText: log.severity,
77
- severityNumber: severityToNumber(log.severity),
78
- body: typeof log.body === 'string' ? log.body : JSON.stringify(log.body),
79
- attributes: {
80
- ...filterNullValues(log.attributes),
81
- ...filterNullValues(log.resource),
82
- },
83
- };
84
- });
85
- }
86
-
87
- /**
88
- * Convert TestSpan and OtelLogs to OtelEvent array
89
- *
90
- * @param span - The test span to convert
91
- * @param logs - Optional OTEL logs to include
92
- * @returns Array of OtelEvents in chronological order
93
- */
94
- export function convertToOtelEvents(span: TestSpan, logs: OtelLog[] = []): OtelEvent[] {
95
- const spanEvents = convertSpanEvents(span);
96
-
97
- // Filter logs for this specific span
98
- const spanLogs = logs.filter((log) => log.spanId === span.id || log.traceId === span.id);
99
- const logEvents = convertLogs(spanLogs);
100
-
101
- // Combine and sort by timestamp
102
- const allEvents = [...spanEvents, ...logEvents].sort((a, b) => {
103
- const aTime = typeof a.timestamp === 'number' ? a.timestamp : new Date(a.timestamp).getTime();
104
- const bTime = typeof b.timestamp === 'number' ? b.timestamp : new Date(b.timestamp).getTime();
105
- return aTime - bTime;
106
- });
107
-
108
- return allEvents;
109
- }
110
-
111
- /**
112
- * Convert all TestSpans to OtelEvents
113
- *
114
- * @param spans - Array of test spans
115
- * @param logs - Optional OTEL logs to include
116
- * @returns Array of OtelEvents for all spans
117
- */
118
- export function convertAllSpansToOtelEvents(spans: TestSpan[], logs: OtelLog[] = []): OtelEvent[] {
119
- const allEvents: OtelEvent[] = [];
120
-
121
- for (const span of spans) {
122
- const spanEvents = convertToOtelEvents(span, logs);
123
- allEvents.push(...spanEvents);
124
- }
125
-
126
- // Sort all events chronologically
127
- return allEvents.sort((a, b) => {
128
- const aTime = typeof a.timestamp === 'number' ? a.timestamp : new Date(a.timestamp).getTime();
129
- const bTime = typeof b.timestamp === 'number' ? b.timestamp : new Date(b.timestamp).getTime();
130
- return aTime - bTime;
131
- });
132
- }
133
-
134
- /**
135
- * Extract test result attributes from TestSpan
136
- * Useful for narrative templates that check assertions
137
- */
138
- export function extractTestAttributes(span: TestSpan): Record<string, unknown> {
139
- // Find assertion.complete event
140
- const assertionComplete = span.events.find((e) => e.name === 'assertion.complete');
141
-
142
- if (assertionComplete) {
143
- return {
144
- 'assertions.passed': assertionComplete.attributes['assertions.passed'] || 0,
145
- 'assertions.failed': assertionComplete.attributes['assertions.failed'] || 0,
146
- 'test.status': span.status,
147
- 'test.name': span.name,
148
- ...span.attributes,
149
- };
150
- }
151
-
152
- return {
153
- 'test.status': span.status,
154
- 'test.name': span.name,
155
- ...span.attributes,
156
- };
157
- }
@@ -1,172 +0,0 @@
1
- /**
2
- * Utilities for loading and managing narrative templates
3
- */
4
-
5
- import type { NarrativeTemplate } from '@principal-ai/principal-view-core/browser';
6
-
7
- /**
8
- * Load a narrative template from a URL or file path
9
- *
10
- * Note: In most cases, you should import templates directly:
11
- * import template from './my-template.narrative.json';
12
- *
13
- * This function is useful when you need to dynamically load templates.
14
- *
15
- * @param path - Path to the narrative template JSON file
16
- * @returns Promise resolving to the narrative template, or null if not found
17
- */
18
- export async function loadNarrativeTemplate(path: string): Promise<NarrativeTemplate | null> {
19
- try {
20
- const response = await fetch(path);
21
- if (!response.ok) {
22
- console.warn(`Narrative template not found: ${path}`);
23
- return null;
24
- }
25
- const template = await response.json();
26
- return template as NarrativeTemplate;
27
- } catch (error) {
28
- console.warn(`Failed to load narrative template from ${path}:`, error);
29
- return null;
30
- }
31
- }
32
-
33
- /**
34
- * Auto-discover narrative template for a given canvas path
35
- *
36
- * Example:
37
- * Canvas: "/path/to/graph-converter.otel.canvas"
38
- * Narrative: "/path/to/graph-converter.narrative.json"
39
- *
40
- * @param canvasPath - Path to the .otel.canvas file
41
- * @returns Promise resolving to the narrative template, or null if not found
42
- */
43
- export async function discoverNarrativeTemplate(canvasPath: string): Promise<NarrativeTemplate | null> {
44
- const narrativePath = canvasPath.replace('.otel.canvas', '.narrative.json');
45
- return loadNarrativeTemplate(narrativePath);
46
- }
47
-
48
- /**
49
- * Validate a narrative template has required fields
50
- *
51
- * @param template - Template to validate
52
- * @returns True if valid, false otherwise
53
- */
54
- export function validateNarrativeTemplate(template: unknown): template is NarrativeTemplate {
55
- if (!template || typeof template !== 'object') {
56
- return false;
57
- }
58
-
59
- const t = template as Partial<NarrativeTemplate>;
60
-
61
- // Check required fields
62
- if (!t.version || !t.canvas || !t.name || !t.mode || !t.scenarioSelection || !t.scenarios) {
63
- return false;
64
- }
65
-
66
- // Check scenarios array
67
- if (!Array.isArray(t.scenarios) || t.scenarios.length === 0) {
68
- return false;
69
- }
70
-
71
- // Check each scenario has required fields
72
- for (const scenario of t.scenarios) {
73
- if (!scenario.id || !scenario.priority || !scenario.condition || !scenario.template) {
74
- return false;
75
- }
76
- }
77
-
78
- return true;
79
- }
80
-
81
- /**
82
- * Get a user-friendly error message for invalid templates
83
- *
84
- * @param template - Template to check
85
- * @returns Error message, or null if valid
86
- */
87
- export function getNarrativeTemplateError(template: unknown): string | null {
88
- if (!template || typeof template !== 'object') {
89
- return 'Template must be a valid JSON object';
90
- }
91
-
92
- const t = template as Partial<NarrativeTemplate>;
93
-
94
- if (!t.version) return 'Missing required field: version';
95
- if (!t.canvas) return 'Missing required field: canvas';
96
- if (!t.name) return 'Missing required field: name';
97
- if (!t.mode) return 'Missing required field: mode';
98
- if (!t.scenarioSelection) return 'Missing required field: scenarioSelection';
99
- if (!t.scenarios) return 'Missing required field: scenarios';
100
-
101
- if (!Array.isArray(t.scenarios)) {
102
- return 'Field "scenarios" must be an array';
103
- }
104
-
105
- if (t.scenarios.length === 0) {
106
- return 'Template must have at least one scenario';
107
- }
108
-
109
- for (let i = 0; i < t.scenarios.length; i++) {
110
- const scenario = t.scenarios[i];
111
- if (!scenario.id) return `Scenario ${i}: missing required field "id"`;
112
- if (scenario.priority === undefined) return `Scenario ${i}: missing required field "priority"`;
113
- if (!scenario.condition) return `Scenario ${i}: missing required field "condition"`;
114
- if (!scenario.template) return `Scenario ${i}: missing required field "template"`;
115
- }
116
-
117
- return null;
118
- }
119
-
120
- /**
121
- * React hook for loading narrative templates
122
- *
123
- * @param templatePath - Path to template, or null to skip loading
124
- * @returns {template, loading, error}
125
- */
126
- export function useNarrativeTemplate(templatePath: string | null) {
127
- const [template, setTemplate] = React.useState<NarrativeTemplate | null>(null);
128
- const [loading, setLoading] = React.useState<boolean>(false);
129
- const [error, setError] = React.useState<string | null>(null);
130
-
131
- React.useEffect(() => {
132
- if (!templatePath) {
133
- setTemplate(null);
134
- setLoading(false);
135
- setError(null);
136
- return;
137
- }
138
-
139
- setLoading(true);
140
- setError(null);
141
-
142
- loadNarrativeTemplate(templatePath)
143
- .then((loadedTemplate) => {
144
- if (loadedTemplate) {
145
- const validationError = getNarrativeTemplateError(loadedTemplate);
146
- if (validationError) {
147
- setError(validationError);
148
- setTemplate(null);
149
- } else {
150
- setTemplate(loadedTemplate);
151
- setError(null);
152
- }
153
- } else {
154
- setError('Template not found');
155
- setTemplate(null);
156
- }
157
- })
158
- .catch((err) => {
159
- setError(err instanceof Error ? err.message : 'Unknown error');
160
- setTemplate(null);
161
- })
162
- .finally(() => {
163
- setLoading(false);
164
- });
165
- }, [templatePath]);
166
-
167
- return { template, loading, error };
168
- }
169
-
170
- // Note: React import is expected to be available in the React package
171
- // If this causes issues, we can remove the hook and export it separately
172
- import React from 'react';