jev-devtools 0.5.3 → 0.5.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.
- package/README.md +109 -103
- package/lib/flow/index.d.ts +6 -3
- package/lib/flow/index.js +3 -2
- package/lib/server/recorder.js +11 -9
- package/lib/src/CaptureFields.d.ts +11 -0
- package/lib/src/CaptureFields.js +42 -0
- package/lib/src/ChoiceProbabilities.d.ts +3 -1
- package/lib/src/ChoiceProbabilities.js +2 -2
- package/lib/src/JevDevtools.d.ts +1 -1
- package/lib/src/JevDevtools.js +21 -6
- package/lib/src/Questions.js +10 -5
- package/lib/src/TraceDiagram.js +45 -18
- package/lib/src/capture-view.d.ts +17 -0
- package/lib/src/capture-view.js +266 -0
- package/lib/src/condition-inputs.js +9 -4
- package/lib/src/display-redaction.js +15 -8
- package/lib/src/flow-condition.js +2 -1
- package/lib/src/flow-fixture.js +6 -4
- package/lib/src/flow-model.js +3 -3
- package/lib/src/flow-parser.d.ts +6 -0
- package/lib/src/flow-parser.js +191 -0
- package/lib/src/flow-stream.js +12 -31
- package/lib/src/inspector-styles.js +1 -1
- package/lib/src/inspector.css +1 -0
- package/lib/src/protocol.d.ts +7 -0
- package/lib/src/trace-parser.d.ts +7 -0
- package/lib/src/trace-parser.js +27 -0
- package/lib/src/trace-store.js +16 -24
- package/lib/src/transport-validation.d.ts +20 -0
- package/lib/src/transport-validation.js +45 -0
- package/package.json +1 -1
- package/skills/setup-jev-devtools/SKILL.md +2 -2
- package/skills/setup-jev-devtools/references/flows.md +38 -2
- package/skills/setup-jev-devtools/references/verify.md +42 -33
package/README.md
CHANGED
|
@@ -1,157 +1,163 @@
|
|
|
1
1
|
# Jev DevTools
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/jev-devtools/v/0.5.5)
|
|
4
4
|
|
|
5
|
-
A React
|
|
5
|
+
A React inspector for TypeSafe Jev requests captured through a Node server adapter. Inspect request inputs, returned answers and probabilities, retries, and errors. Use the optional Flow API to connect requests and application steps in a diagram.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[Website](https://jevtools.dev/) · [Documentation](https://docs.jevtools.dev/) · [Quick start](https://docs.jevtools.dev/getting-started/) · [API reference](https://docs.jevtools.dev/api/reference/) · [Release history](https://docs.jevtools.dev/changelog/)
|
|
10
|
-
|
|
11
|
-
Hosted docs update separately; use the installed declarations and bundled skill for your exact version, and the [repository changelog](https://github.com/diegoag64/jev-devtools/blob/main/CHANGELOG.md) for release-candidate status.
|
|
7
|
+
[Website](https://jevtools.dev/) · [Documentation](https://docs.jevtools.dev/) · [API reference](https://docs.jevtools.dev/api/reference/) · [For AI agents](#for-ai-agents)
|
|
12
8
|
|
|
13
9
|
## Demo
|
|
14
10
|
|
|
15
|
-

|
|
12
|
+
|
|
13
|
+
The recording uses sample responses. Try the [sample apps and cookbooks](https://docs.jevtools.dev/examples/) without an API key. Cookbook application code and fixtures live in the repository and are not bundled in npm.
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
## Install
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
Use matching React and React DOM **18.x or 19.x**, Node **22.12+**, TypeSafe SDK **0.7.x**, and ESM imports. TypeScript consumers need **5.4+** and compatible React and Node types. React and React DOM are required peers even in a server-only workspace. See [compatibility and public imports](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md#compatibility-and-public-imports) for exact ranges and compiler settings.
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
Install in each workspace that imports the package:
|
|
22
20
|
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
https://raw.githubusercontent.com/diegoag64/jev-devtools/main/packages/jev-devtools/skills/setup-jev-devtools/SKILL.md
|
|
26
|
-
Choose the recipe for this app's framework, wire the server capture and root
|
|
27
|
-
drawer, and verify with a labeled sample request. Preserve existing app behavior.
|
|
21
|
+
```sh
|
|
22
|
+
npm install jev-devtools @typesafe-ai/sdk
|
|
28
23
|
```
|
|
29
24
|
|
|
30
|
-
|
|
25
|
+
Connect one React drawer, one shared Node adapter, and an instrumented client created with `inspector.createClient()`. Only calls through that client are captured. The drawer and adapter default to disabled. Enable both for development and keep API keys on the server.
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
Choose the setup guide for your app:
|
|
33
28
|
|
|
34
|
-
|
|
29
|
+
- [React with Node](https://docs.jevtools.dev/getting-started/) connects an existing Express server and a Vite React app, with proxy configuration and capture checks.
|
|
30
|
+
- [Next.js App Router](https://docs.jevtools.dev/guides/nextjs/) uses `getJevDevtools` for a shared adapter, `handleNodeRoute` in an optional catch-all route, and a client component for the drawer. Externalize `@typesafe-ai/sdk` and keep `jev-devtools` bundled. The package does not require a Next.js dependency.
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
Mount the drawer once near your app's root. It supplies its own styles inside a Shadow DOM boundary and requires no CSS import, Tailwind, or shadcn. The browser entry is safe for server rendering. Pan the Flow canvas by dragging, zoom by scrolling or pinching, or select **Fit**.
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
pnpm add jev-devtools @typesafe-ai/sdk
|
|
40
|
-
```
|
|
34
|
+
### Runtime and access
|
|
41
35
|
|
|
42
|
-
|
|
36
|
+
The inspector routes and application client must share one adapter in the same Node process. Keep `jev-devtools/server` and `jev-devtools/flow` on Node. Edge runtimes and history aggregation across distributed or serverless instances are unsupported.
|
|
43
37
|
|
|
44
|
-
|
|
38
|
+
The drawer receives same-origin server-sent events, starting with a snapshot and then changed traces. Older client and server combinations can use full snapshots. Keep both on the same package version to inspect all supported flow steps. History is bounded to 50 calls in memory by default.
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
# From the jev-devtools repository root:
|
|
48
|
-
npm ci
|
|
49
|
-
npm run pack:devtools
|
|
50
|
-
# From the consuming app:
|
|
51
|
-
npm install /absolute/path/to/jev-devtools-0.5.3.tgz
|
|
52
|
-
```
|
|
40
|
+
Bind local development servers to `127.0.0.1`. Without `authorize`, Node middleware accepts only loopback connections. Web Request handlers cannot inspect the connection's address. Shared access and tunnels require real [authorization and access controls](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md#history-and-access). Match the adapter's `origin` to the browser URL, and proxy the inspector endpoints when the frontend and backend use different ports.
|
|
53
41
|
|
|
54
|
-
|
|
42
|
+
### Capture privacy
|
|
55
43
|
|
|
56
|
-
|
|
57
|
-
import { JevDevtools } from "jev-devtools";
|
|
44
|
+
Captures contain bounded, redacted copies of request context and answers. Application and SDK data remain unchanged. Diagrams show captured data and application-defined routing, not hidden model reasoning.
|
|
58
45
|
|
|
59
|
-
|
|
60
|
-
```
|
|
46
|
+
Known credential fields and custom server redactions cannot be revealed in the drawer. Headers and client options are not captured. Arbitrary secrets in prose or unusual field names still need application-specific server redaction. See the [redaction policy and limits](https://github.com/diegoag64/jev-devtools/blob/main/docs/integration.md#redaction-policy).
|
|
61
47
|
|
|
62
|
-
|
|
48
|
+
`redactAdditionalSensitiveData` defaults to `true` and masks supported email, phone, postal address, SSN, and card-number fields in the drawer. Set it to `false` on `JevDevtools` to disable that display masking, including in existing views. **Display masking does not remove captured data.** The underlying values remain in capture, stream, and browser memory. Use server-side redaction for data that must not reach the browser.
|
|
63
49
|
|
|
64
|
-
|
|
65
|
-
import { createJevDevtools } from "jev-devtools/server";
|
|
50
|
+
### Versions and local builds
|
|
66
51
|
|
|
67
|
-
|
|
68
|
-
enabled: process.env.NODE_ENV === "development",
|
|
69
|
-
origin: "http://localhost:3000",
|
|
70
|
-
});
|
|
71
|
-
const jev = inspector.createClient({ apiKey: process.env.TYPESAFE_API_KEY });
|
|
72
|
-
app.use(inspector.middleware); // Mount at root, before fallback handlers.
|
|
73
|
-
// Call jev.systemOne(...) in your application's handlers.
|
|
74
|
-
// Call inspector.dispose() on shutdown.
|
|
75
|
-
```
|
|
52
|
+
**Package version: 0.5.5.** [See release notes for verified publication status](https://github.com/diegoag64/jev-devtools/releases/tag/v0.5.5). APIs remain experimental before 1.0. Check the [changelog](https://github.com/diegoag64/jev-devtools/blob/main/CHANGELOG.md) for compatibility and release status. Verification uses sample responses through the real TypeSafe SDK, not live inference.
|
|
76
53
|
|
|
77
|
-
|
|
54
|
+
Hosted docs update separately. Prefer installed declarations and the bundled setup skill when the site's version differs from your package.
|
|
78
55
|
|
|
79
|
-
|
|
56
|
+
<details>
|
|
57
|
+
<summary>Try changes from a local checkout</summary>
|
|
80
58
|
|
|
81
|
-
|
|
59
|
+
Pack the Jev DevTools repository, then install the archive in your app:
|
|
82
60
|
|
|
83
|
-
|
|
61
|
+
```sh
|
|
62
|
+
# From the jev-devtools repository root:
|
|
63
|
+
npm ci
|
|
64
|
+
npm run pack:devtools
|
|
65
|
+
|
|
66
|
+
# From the consuming app:
|
|
67
|
+
npm install /absolute/path/to/jev-devtools/jev-devtools-0.5.5.tgz
|
|
68
|
+
```
|
|
84
69
|
|
|
85
|
-
|
|
70
|
+
Keep the drawer and server adapter on the same build.
|
|
86
71
|
|
|
87
|
-
|
|
72
|
+
</details>
|
|
88
73
|
|
|
89
|
-
|
|
74
|
+
<a id="connected-jev-flows-with-jev-devtoolsflow"></a>
|
|
90
75
|
|
|
91
|
-
##
|
|
76
|
+
## Connect requests with flows
|
|
92
77
|
|
|
93
|
-
Define
|
|
78
|
+
Define a flow on Node, then run it for each input. Typed answer references can feed later requests, conditions, and outputs. The API also supports server tasks, parallel steps, bounded collections, conditional paths that rejoin, and stateful loops.
|
|
94
79
|
|
|
95
|
-
|
|
80
|
+
This example asks whether a message is ready for review. An application-defined threshold selects an outcome. Reuse the instrumented `jev` client and shared `inspector` from your server integration. Running this example with a live client makes an inference request:
|
|
96
81
|
|
|
97
82
|
```ts
|
|
83
|
+
import { noul } from "@typesafe-ai/sdk";
|
|
98
84
|
import { defineJevFlow } from "jev-devtools/flow";
|
|
99
85
|
|
|
100
86
|
const review = defineJevFlow<{ message: string }>("review", (flow) => {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
questions: {
|
|
106
|
-
ready: { type: "noul", instructions: "Is this ready for review?" },
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
{ title: "Assess readiness" },
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
return flow.branch(
|
|
113
|
-
"route",
|
|
114
|
-
{
|
|
115
|
-
when: decision.answer("ready").noul.gte(0.9), // Your application's policy.
|
|
116
|
-
then: (path) =>
|
|
117
|
-
path.output(
|
|
118
|
-
"ready",
|
|
119
|
-
{ probability: decision.answer("ready").noul },
|
|
120
|
-
{
|
|
121
|
-
title: "Ready for review",
|
|
122
|
-
description: "Return readiness to the application.",
|
|
123
|
-
},
|
|
124
|
-
),
|
|
125
|
-
else: (path) =>
|
|
126
|
-
path.output("manual-review", undefined, {
|
|
127
|
-
title: "Needs manual review",
|
|
128
|
-
}),
|
|
87
|
+
const readiness = flow.jev("readiness", {
|
|
88
|
+
state: flow.input,
|
|
89
|
+
questions: {
|
|
90
|
+
ready: noul("Does this message include enough detail to review?"),
|
|
129
91
|
},
|
|
130
|
-
|
|
131
|
-
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return flow.branch("route", {
|
|
95
|
+
// Example application policy, not a threshold supplied by Jev.
|
|
96
|
+
when: readiness.answer("ready").noul.gte(0.9),
|
|
97
|
+
then: (path) => path.output("ready"),
|
|
98
|
+
else: (path) => path.output("needs-detail"),
|
|
99
|
+
});
|
|
132
100
|
});
|
|
133
101
|
|
|
134
102
|
const result = await review.run(
|
|
135
|
-
{ message: "Please review
|
|
136
|
-
{
|
|
137
|
-
client: jev,
|
|
138
|
-
devtools: inspector,
|
|
139
|
-
},
|
|
103
|
+
{ message: "Please review the attached proposal." },
|
|
104
|
+
{ client: jev, devtools: inspector },
|
|
140
105
|
);
|
|
141
|
-
|
|
106
|
+
console.log(result.outcome); // "ready" or "needs-detail", depending on the answer.
|
|
142
107
|
```
|
|
143
108
|
|
|
144
|
-
|
|
109
|
+
Open **Jev DevTools**, select **Flows**, and open the run. Inspect the returned probability and the condition that selected the path. Untaken paths remain visible. The flow returns an outcome. Your application decides what to do with it.
|
|
110
|
+
|
|
111
|
+
Runs support SDK retries and cancellation, with bounded in-memory inspector history. They do not provide durable workflow execution. Learn the API in [Flows from scratch](https://docs.jevtools.dev/guides/flow-basics/) or look up contracts in the [flow reference](https://github.com/diegoag64/jev-devtools/blob/main/docs/flows.md).
|
|
112
|
+
|
|
113
|
+
## Replay and compare runs
|
|
114
|
+
|
|
115
|
+
Replay and comparison require **0.5.0 or later**. Choose **Copy fixture** on a captured run to save its answers as JSON. `replayClient(fixture)` from `jev-devtools/flow` supplies those answers without Jev inference or an API key. Tasks run again and may call outside services.
|
|
116
|
+
|
|
117
|
+
`compareWithFixture` compares answers, routing, and item steps with a saved fixture. It runs the flow with the supplied client and can make live calls. See the [Flow API reference](https://docs.jevtools.dev/api/flow/) for fixture requirements and complete examples.
|
|
118
|
+
|
|
119
|
+
<a id="set-up-with-your-coding-agent"></a>
|
|
120
|
+
|
|
121
|
+
## For AI agents
|
|
145
122
|
|
|
146
|
-
|
|
123
|
+
Start with the `jev-devtools` version installed in the consuming app. Follow that app's project instructions and use its existing package manager. Choose guidance for the task:
|
|
147
124
|
|
|
148
|
-
|
|
125
|
+
| Task | Instructions |
|
|
126
|
+
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| Install or troubleshoot the inspector | Read `node_modules/jev-devtools/skills/setup-jev-devtools/SKILL.md`. The [setup skill](https://github.com/diegoag64/jev-devtools/blob/main/packages/jev-devtools/skills/setup-jev-devtools/SKILL.md) selects the framework recipe and capture checks. |
|
|
128
|
+
| Build a flow | Read `references/flows.md` inside the installed setup skill. The [flow recipe](https://github.com/diegoag64/jev-devtools/blob/main/packages/jev-devtools/skills/setup-jev-devtools/references/flows.md) covers builders, execution, capture, replay, and limits. |
|
|
129
|
+
| Convert existing Jev logic or check whether it fits Flow | Read [port-jev-to-flow](https://github.com/diegoag64/jev-devtools/blob/main/.agents/skills/port-jev-to-flow/SKILL.md) and its referenced guidance. It maps source behavior to public Flow APIs and records adaptations or unsupported behavior. |
|
|
130
|
+
| Look up an API contract | Read `lib/src/index.d.ts`, `lib/server/index.d.ts`, or `lib/flow/index.d.ts` inside the installed package. Use the [API reference](https://docs.jevtools.dev/api/reference/) for explanations and examples. |
|
|
149
131
|
|
|
150
|
-
|
|
132
|
+
The setup skill and its references ship in npm. **The porting skill is repository-only.** Read it at `.agents/skills/port-jev-to-flow/SKILL.md` in a Jev DevTools checkout, or use its [raw Markdown](https://raw.githubusercontent.com/diegoag64/jev-devtools/main/.agents/skills/port-jev-to-flow/SKILL.md). Resolve its `references/` links beside that file. For offline use, copy the whole `port-jev-to-flow` folder, including its references. Cookbook paths in the skill refer to the Jev DevTools repository.
|
|
133
|
+
|
|
134
|
+
Check online guidance against the installed declarations before using an API. Read declaration files from disk, but import application code only through `jev-devtools`, `jev-devtools/server`, and `jev-devtools/flow`. The latter two run on Node. Internal files and `jev-devtools/package.json` are not application import paths.
|
|
135
|
+
|
|
136
|
+
Use this prompt to set up an app after installing the package:
|
|
151
137
|
|
|
152
138
|
```text
|
|
153
|
-
Read node_modules/jev-devtools/skills/setup-jev-devtools/SKILL.md
|
|
154
|
-
|
|
155
|
-
Preserve
|
|
156
|
-
|
|
139
|
+
Read node_modules/jev-devtools/skills/setup-jev-devtools/SKILL.md
|
|
140
|
+
from the package installed for this app, then follow its framework recipe.
|
|
141
|
+
Preserve authentication, redaction, SDK options, and application behavior.
|
|
142
|
+
Keep credentials on the server. Share one adapter between application calls
|
|
143
|
+
and inspector routes in the same Node process.
|
|
144
|
+
Verify a labeled sample in the drawer, run the app's typecheck and build,
|
|
145
|
+
and check that capture and display are disabled in production.
|
|
146
|
+
Report what passed, whether inference was sample or live, and any gaps.
|
|
157
147
|
```
|
|
148
|
+
|
|
149
|
+
For a port, provide the source code or recipe and ask the agent to use `port-jev-to-flow`. Preserve question wording, request order and concurrency, thresholds, and external effects. Report unsupported behavior before changing it. Verify with labeled samples first. Live inference, publication, and deployment require separate authorization.
|
|
150
|
+
|
|
151
|
+
npm does not register skills with coding tools automatically. Point the agent to the files explicitly, or install each complete skill folder using the tool's supported mechanism. Use [llms.txt](https://raw.githubusercontent.com/diegoag64/jev-devtools/main/llms.txt) to find repository guidance and [the hosted Markdown index](https://docs.jevtools.dev/llms.txt) for docs pages. The [contributor instructions](https://github.com/diegoag64/jev-devtools/blob/main/AGENTS.md) apply when editing Jev DevTools itself.
|
|
152
|
+
|
|
153
|
+
## Documentation and contributing
|
|
154
|
+
|
|
155
|
+
Use the [documentation](https://docs.jevtools.dev/) for guides and API examples, and the [repository](https://github.com/diegoag64/jev-devtools/) for sample apps and cookbooks. See [Contributing](https://github.com/diegoag64/jev-devtools/blob/main/CONTRIBUTING.md) for repository setup, verification, and the pull request workflow. Merging, npm publication, and deployment each require explicit authorization.
|
|
156
|
+
|
|
157
|
+
## Credits and license
|
|
158
|
+
|
|
159
|
+
Inspired by the TanStack Query DevTools workflow, with a custom drawer and TanStack Virtual for the Questions list. The TanStack DevTools shell is not included. Diagram styling is based on [Cathryn Lavery's diagram-design](https://github.com/cathrynlavery/diagram-design).
|
|
160
|
+
|
|
161
|
+
Jev DevTools is an independent, open-source community project. It is not affiliated with TypeSafe AI. TypeSafe branding belongs to its respective owner.
|
|
162
|
+
|
|
163
|
+
Project code is MIT licensed. See the bundled `LICENSE` and `THIRD_PARTY_NOTICES.txt`, or read the repository's [license](https://github.com/diegoag64/jev-devtools/blob/main/LICENSE) and [third-party notices](https://github.com/diegoag64/jev-devtools/blob/main/THIRD_PARTY_NOTICES.txt).
|
package/lib/flow/index.d.ts
CHANGED
|
@@ -21,14 +21,17 @@ type Ast = {
|
|
|
21
21
|
args: Ast[];
|
|
22
22
|
};
|
|
23
23
|
declare const valueTypeBrand: unique symbol;
|
|
24
|
+
type Scalar = string | number | boolean | null;
|
|
25
|
+
type ExpressionFields<T> = T extends readonly unknown[] ? Pick<T, number | "length" | Extract<keyof T, `${number}`>> : T;
|
|
24
26
|
/** Inspectable expression, not an application callback. Use get() for input fields. */
|
|
25
27
|
export declare class FlowExpression<T> {
|
|
26
28
|
readonly ast: Ast;
|
|
27
29
|
protected readonly [valueTypeBrand]: T;
|
|
28
30
|
constructor(ast: Ast, owner?: object);
|
|
29
|
-
/** Select an own field
|
|
30
|
-
get<K extends keyof T & (string | number)>(this: FlowExpression<object>, key: K): FlowExpression<T[K]>;
|
|
31
|
-
|
|
31
|
+
/** Select an own object field, array index, or array length. Missing values fail at runtime. */
|
|
32
|
+
get<K extends keyof T & keyof ExpressionFields<T> & (string | number)>(this: FlowExpression<object>, key: K): FlowExpression<T[K]>;
|
|
33
|
+
/** Compare matching scalar values; numbers must be finite at runtime. */
|
|
34
|
+
eq(this: FlowExpression<Scalar>, value: T | FlowExpression<T>): FlowExpression<boolean>;
|
|
32
35
|
lt(this: FlowExpression<number>, value: number | FlowExpression<number>): FlowExpression<boolean>;
|
|
33
36
|
gt(this: FlowExpression<number>, value: number | FlowExpression<number>): FlowExpression<boolean>;
|
|
34
37
|
gte(this: FlowExpression<number>, value: number | FlowExpression<number>): FlowExpression<boolean>;
|
package/lib/flow/index.js
CHANGED
|
@@ -49,10 +49,11 @@ export class FlowExpression {
|
|
|
49
49
|
freezeAst(ast);
|
|
50
50
|
Object.freeze(this);
|
|
51
51
|
}
|
|
52
|
-
/** Select an own field
|
|
52
|
+
/** Select an own object field, array index, or array length. Missing values fail at runtime. */
|
|
53
53
|
get(key) {
|
|
54
54
|
return expression({ op: "get", from: this.ast, key: String(key) }, [this]);
|
|
55
55
|
}
|
|
56
|
+
/** Compare matching scalar values; numbers must be finite at runtime. */
|
|
56
57
|
eq(value) {
|
|
57
58
|
return operation("eq", [this, value]);
|
|
58
59
|
}
|
|
@@ -214,7 +215,7 @@ function conditionRule(ast, requests, tasks) {
|
|
|
214
215
|
? path.length === 3 && ["noul", "choice", "score"].includes(path[2])
|
|
215
216
|
? path[1]
|
|
216
217
|
: path.slice(1).join(" ")
|
|
217
|
-
: path.at(-1) ?? "Input",
|
|
218
|
+
: (path.at(-1) ?? "Input"),
|
|
218
219
|
...(base.op === "ref" && requests.has(base.id)
|
|
219
220
|
? { stepId: base.id, path }
|
|
220
221
|
: base.op === "ref" && tasks.has(base.id)
|
package/lib/server/recorder.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { Image } from "@typesafe-ai/sdk";
|
|
4
|
+
import { projectTraceCapture } from "../src/capture-view.js";
|
|
4
5
|
// Whole field names only: tokenCount, sessionId, apiKeyName, etc. stay useful.
|
|
5
6
|
// Keep the documented lists in docs/integration.md aligned with these policies.
|
|
6
7
|
const credentialKeys = new Set([
|
|
@@ -227,14 +228,10 @@ export class JevRecorder {
|
|
|
227
228
|
captured = capture({ ...request, model: request.model ?? client.defaultModel }, warnings, this.options.redact);
|
|
228
229
|
}
|
|
229
230
|
catch {
|
|
230
|
-
captured =
|
|
231
|
-
model: client.defaultModel,
|
|
232
|
-
state: "[capture unavailable]",
|
|
233
|
-
questions: {},
|
|
234
|
-
};
|
|
231
|
+
captured = null;
|
|
235
232
|
warnings.add("Input snapshot failed; request continues normally.");
|
|
236
233
|
}
|
|
237
|
-
this.traces.set(id, {
|
|
234
|
+
this.traces.set(id, projectTraceCapture({
|
|
238
235
|
version: 1,
|
|
239
236
|
id,
|
|
240
237
|
...(typeof meta === "function" ? meta() : meta),
|
|
@@ -244,7 +241,7 @@ export class JevRecorder {
|
|
|
244
241
|
request: captured,
|
|
245
242
|
attempts: [],
|
|
246
243
|
captureWarnings: [...warnings],
|
|
247
|
-
});
|
|
244
|
+
}));
|
|
248
245
|
const trace = this.traces.get(id);
|
|
249
246
|
try {
|
|
250
247
|
this.flowContext.getStore()?.onTrace(id);
|
|
@@ -291,8 +288,13 @@ export class JevRecorder {
|
|
|
291
288
|
t.durationMs = performance.now() - started;
|
|
292
289
|
t.requestId =
|
|
293
290
|
response.headers.get("x-typesafe-request-id") ?? undefined;
|
|
294
|
-
t
|
|
295
|
-
|
|
291
|
+
Object.assign(t, projectTraceCapture({
|
|
292
|
+
...t,
|
|
293
|
+
result,
|
|
294
|
+
captureWarnings: [
|
|
295
|
+
...new Set([...t.captureWarnings, ...warnings]),
|
|
296
|
+
],
|
|
297
|
+
}));
|
|
296
298
|
});
|
|
297
299
|
}, fail)
|
|
298
300
|
.catch(() => {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Trace } from "./protocol.js";
|
|
2
|
+
/** Missing paths come from shape checks, never from redaction marker strings. */
|
|
3
|
+
export declare function CaptureFields({ trace, path, }: {
|
|
4
|
+
trace: Trace;
|
|
5
|
+
path: readonly string[];
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
/** A partial box has readable coordinates but cannot define an overlay. */
|
|
8
|
+
export declare function PartialBox({ trace, name }: {
|
|
9
|
+
trace: Trace;
|
|
10
|
+
name: string;
|
|
11
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { capturedValue, captureFieldUnavailable, unavailableCaptureFields, } from "./capture-view.js";
|
|
3
|
+
const labels = {
|
|
4
|
+
request: "Request",
|
|
5
|
+
result: "Response",
|
|
6
|
+
questions: "Questions",
|
|
7
|
+
answers: "Answers",
|
|
8
|
+
score: "Score",
|
|
9
|
+
noul: "Probability of true",
|
|
10
|
+
confidence: "Confidence",
|
|
11
|
+
choice: "Choice",
|
|
12
|
+
probabilities: "Probabilities",
|
|
13
|
+
legend: "Legend",
|
|
14
|
+
box: "Box",
|
|
15
|
+
type: "Type",
|
|
16
|
+
model: "Model",
|
|
17
|
+
state: "Context",
|
|
18
|
+
};
|
|
19
|
+
const label = (parts) => parts
|
|
20
|
+
.map((part) => (Object.hasOwn(labels, part) ? labels[part] : part))
|
|
21
|
+
.join(" · ");
|
|
22
|
+
/** Missing paths come from shape checks, never from redaction marker strings. */
|
|
23
|
+
export function CaptureFields({ trace, path, }) {
|
|
24
|
+
const fields = captureFieldUnavailable(trace, path)
|
|
25
|
+
? [path]
|
|
26
|
+
: unavailableCaptureFields(trace, path);
|
|
27
|
+
return (_jsx(_Fragment, { children: fields.map((field) => (_jsxs("p", { className: "muted", children: [label(field.slice(path.length).length
|
|
28
|
+
? field.slice(path.length)
|
|
29
|
+
: path.slice(-1)), " ", "unavailable in this capture."] }, JSON.stringify(field)))) }));
|
|
30
|
+
}
|
|
31
|
+
/** A partial box has readable coordinates but cannot define an overlay. */
|
|
32
|
+
export function PartialBox({ trace, name }) {
|
|
33
|
+
if (trace.result?.answers[name]?.box)
|
|
34
|
+
return null;
|
|
35
|
+
const value = capturedValue(trace, ["result", "answers", name, "box"]);
|
|
36
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
37
|
+
return null;
|
|
38
|
+
const coordinates = Object.entries(value).flatMap(([key, value]) => typeof value === "number" && Number.isFinite(value)
|
|
39
|
+
? [`${key} ${value.toFixed(2)}`]
|
|
40
|
+
: []);
|
|
41
|
+
return coordinates.length ? (_jsxs("p", { className: "muted", children: [coordinates.join(" · "), " \u00B7 Box overlay unavailable"] })) : null;
|
|
42
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
import type { AnswerView } from "./protocol.js";
|
|
2
|
-
export declare function ChoiceProbabilities({ answer }: {
|
|
3
|
+
export declare function ChoiceProbabilities({ answer, captureNotice, }: {
|
|
3
4
|
answer: AnswerView;
|
|
5
|
+
captureNotice?: ReactNode;
|
|
4
6
|
}): import("react").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
|
-
export function ChoiceProbabilities({ answer }) {
|
|
3
|
+
export function ChoiceProbabilities({ answer, captureNotice, }) {
|
|
4
4
|
const [search, setSearch] = useState("");
|
|
5
5
|
const [page, setPage] = useState(0);
|
|
6
6
|
const rows = useMemo(() => Object.entries(answer.probabilities ?? {}).sort((a, b) => b[1] - a[1]), [answer.probabilities]);
|
|
@@ -13,7 +13,7 @@ export function ChoiceProbabilities({ answer }) {
|
|
|
13
13
|
return (_jsxs("div", { className: "lab-choice-browser", role: "region", "aria-label": "All Choice probabilities", "data-diagram-scroll": "", children: [_jsxs("div", { className: "lab-choice-controls", children: [_jsxs("label", { children: ["Search categories", _jsx("input", { autoFocus: true, type: "search", value: search, onChange: (event) => {
|
|
14
14
|
setSearch(event.target.value);
|
|
15
15
|
setPage(0);
|
|
16
|
-
} })] }), _jsxs("span", { role: "status", children: [filtered.length, " of ", rows.length, " categories \u00B7 highest probability first"] })] }), _jsxs("div", { className: "lab-choice-table-scroll", tabIndex: 0, "aria-label": "Probability table", children: [_jsxs("table", { children: [_jsxs("caption", { children: ["Captured probabilities. Values are not renormalized. Returned choice: ", answer.choice ?? "unavailable", "."] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Category" }), _jsx("th", { scope: "col", children: "Probability" }), _jsx("th", { scope: "col", children: "Exact value (0\u20131)" })] }) }), _jsx("tbody", { children: filtered
|
|
16
|
+
} })] }), _jsxs("span", { role: "status", children: [filtered.length, " of ", rows.length, " categories \u00B7 highest probability first"] })] }), _jsxs("div", { className: "lab-choice-table-scroll", tabIndex: 0, "aria-label": "Probability table", children: [_jsxs("table", { children: [_jsxs("caption", { children: [captureNotice, "Captured probabilities. Values are not renormalized. Returned choice: ", answer.choice ?? "unavailable", "."] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Category" }), _jsx("th", { scope: "col", children: "Probability" }), _jsx("th", { scope: "col", children: "Exact value (0\u20131)" })] }) }), _jsx("tbody", { children: filtered
|
|
17
17
|
.slice(current * 20, (current + 1) * 20)
|
|
18
18
|
.map(([label, value]) => (_jsxs("tr", { children: [_jsxs("th", { scope: "row", children: [label, label === answer.choice && (_jsx("span", { className: "lab-choice-returned", children: "Returned choice" }))] }), _jsxs("td", { children: [Number((value * 100).toPrecision(6)), "%"] }), _jsx("td", { children: _jsx("code", { children: String(value) }) })] }, label))) })] }), !filtered.length && _jsx("p", { children: "No matching categories." })] }), _jsxs("div", { className: "lab-choice-pagination", children: [_jsx("button", { disabled: current === 0, onClick: () => setPage(current - 1), children: "Previous categories" }), _jsxs("span", { "aria-live": "polite", children: ["Page ", current + 1, " of ", pages] }), _jsx("button", { disabled: current + 1 >= pages, onClick: () => setPage(current + 1), children: "Next categories" })] })] }));
|
|
19
19
|
}
|
package/lib/src/JevDevtools.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Trace } from "./protocol.js";
|
|
|
3
3
|
export declare const RequestDetail: import("react").MemoExoticComponent<({ trace, }: {
|
|
4
4
|
trace?: Trace;
|
|
5
5
|
}) => import("react").JSX.Element>;
|
|
6
|
-
export declare function JevPanel({ traces, flows, connection, onClear, }: {
|
|
6
|
+
export declare function JevPanel({ traces: incomingTraces, flows, connection, onClear, }: {
|
|
7
7
|
traces: Trace[];
|
|
8
8
|
flows?: FlowHistory;
|
|
9
9
|
connection: string;
|
package/lib/src/JevDevtools.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CaptureFields } from "./CaptureFields.js";
|
|
3
|
+
import { capturedValue, captureFieldUnavailable, traceCaptureView, } from "./capture-view.js";
|
|
2
4
|
import { emptyFlows } from "./flow-stream.js";
|
|
3
5
|
import { lazy, memo, startTransition, Suspense, useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
6
|
import { TraceDiagram } from "./TraceDiagram.js";
|
|
@@ -18,24 +20,37 @@ const RequestRow = memo(function RequestRow({ trace, selected, onSelect, }) {
|
|
|
18
20
|
return (_jsxs("button", { className: `request-row ${selected ? "selected" : ""}`, "aria-pressed": selected, onClick: () => onSelect(trace.id), children: [_jsx("span", { className: `status-dot ${trace.status}` }), _jsxs("span", { children: [_jsx("strong", { children: trace.label }), _jsxs("small", { children: [new Date(trace.startedAt).toLocaleTimeString(), " \u00B7 ", trace.source, " \u00B7", " ", ms(trace.durationMs)] })] }), _jsx("span", { className: "row-status", children: trace.status })] }));
|
|
19
21
|
});
|
|
20
22
|
export const RequestDetail = memo(function RequestDetail({ trace, }) {
|
|
23
|
+
trace = trace && traceCaptureView(trace);
|
|
24
|
+
const tokens = (field) => {
|
|
25
|
+
if (!trace)
|
|
26
|
+
return "—";
|
|
27
|
+
const value = capturedValue(trace, ["result", "usage", field]);
|
|
28
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
29
|
+
? value
|
|
30
|
+
: captureFieldUnavailable(trace, ["result", "usage", field])
|
|
31
|
+
? "Unavailable"
|
|
32
|
+
: "—";
|
|
33
|
+
};
|
|
21
34
|
// Keep tab selection across request switches and clear, as before. Local tab
|
|
22
35
|
// changes no longer rerender the history list.
|
|
23
36
|
const [tab, setTab] = useState("Flow");
|
|
24
|
-
return (_jsx("section", { className: "request-detail", children: trace ? (_jsxs(_Fragment, { children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: [trace.source, " response \u00B7 ", trace.status] }), _jsx("h2", { children: trace.label })] }), _jsxs("div", { className: "metrics", children: [_jsxs("span", { children: [ms(trace.durationMs), _jsx("small", { children: "Total duration" })] }), _jsxs("span", { children: [trace.attempts.length, _jsx("small", { children: "Attempts" })] }), _jsxs("span", { children: [trace.result?.
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
return (_jsx("section", { className: "request-detail", children: trace ? (_jsxs(_Fragment, { children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: [trace.source, " response \u00B7 ", trace.status] }), _jsx("h2", { children: trace.label })] }), _jsxs("div", { className: "metrics", children: [_jsxs("span", { children: [ms(trace.durationMs), _jsx("small", { children: "Total duration" })] }), _jsxs("span", { children: [trace.attempts.length, _jsx("small", { children: "Attempts" })] }), _jsxs("span", { children: [`${tokens("input_tokens")} / ${tokens("output_tokens")}`, _jsx("small", { children: "Tokens in / out" })] })] })] }), _jsxs("div", { className: "detail-meta", children: [_jsx("code", { children: trace.result?.model ||
|
|
38
|
+
trace.request.model ||
|
|
39
|
+
"Model unavailable in this capture" }), _jsx("code", { children: trace.requestId ||
|
|
27
40
|
(trace.status === "pending"
|
|
28
41
|
? "Request ID pending"
|
|
29
|
-
: "No request ID") })
|
|
42
|
+
: "No request ID") }), ["request", "result"].map((section) => !captureFieldUnavailable(trace, [section]) &&
|
|
43
|
+
captureFieldUnavailable(trace, [section, "model"]) ? (_jsxs("p", { className: "muted", children: [section === "request" ? "Request" : "Response", " model unavailable in this capture."] }, section)) : null)] }), _jsx("nav", { className: "detail-tabs", "aria-label": "Request views", children: ["Flow", "Questions", "Context", "Transport", "Raw"].map((name) => (_jsx("button", { "aria-pressed": tab === name, onClick: () => {
|
|
30
44
|
// Keep the current view while its chunk loads, but don't
|
|
31
45
|
// defer subsequent opens once the component is available.
|
|
32
46
|
if (name === "Questions" && !questionsLoaded)
|
|
33
47
|
startTransition(() => setTab(name));
|
|
34
48
|
else
|
|
35
49
|
setTab(name);
|
|
36
|
-
}, children: name }, name))) }), _jsx("div", { className: `detail-body ${tab === "Flow" ? "detail-body-flow" : tab === "Questions" ? "detail-body-questions" : ""}`, children: _jsxs(Suspense, { fallback: _jsx("p", { className: "muted", role: "status", children: "Loading questions\u2026" }), children: [trace.error && (_jsxs("div", { role: "alert", className: "error-banner", children: [_jsx("strong", { children: trace.error.name }), " ", trace.error.message] })), trace.captureWarnings.map((w) => (_jsx("p", { className: "warning", children: w }, w))), tab === "Flow" && _jsx(TraceDiagram, { trace: trace }, trace.id), tab === "Questions" && (_jsx(Questions, { trace: trace }, trace.id)), tab === "Context" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "muted", children: "Captured before dispatch without changing SDK data. Known credential fields remain redacted; additional display masking follows the component prop. Captures are size limited." }), findImage(trace.request.state) && (_jsx("img", { className: "context-image", src: findImage(trace.request.state), alt: "Captured context" })), _jsx(JsonBlock, { value: trace.request.state }), trace.request.assets && (_jsx(JsonBlock, { value: trace.request.assets }))] })), tab === "Transport" && (_jsxs(_Fragment, { children: [_jsx("h3", { children: "HTTP attempts" }), _jsx("p", { className: "muted", children: "Attempt duration ends at response headers. Total duration includes SDK processing and retry waits." }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Attempt" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: trace.attempts.map((a) => (_jsxs("tr", { children: [_jsx("td", { children: a.number }), _jsx("td", { children: a.error || a.status || "Pending" }), _jsx("td", { children: ms(a.durationMs) })] }, a.number))) })] }), !trace.attempts.length && _jsx("p", { children: "No HTTP attempt recorded." })] })), tab === "Raw" && _jsx(JsonBlock, { value: trace })] }) })] })) : (_jsxs("div", { className: "empty-detail", children: [_jsx("span", { className: "empty-symbol", children: "\u21B3" }), _jsx("h2", { children: "Follow a Jev request" }), _jsxs("p", { children: ["Context, parallel questions, typed answers.", _jsx("br", {}), "Run a sample above to see the complete flow."] })] })) }));
|
|
50
|
+
}, children: name }, name))) }), _jsx("div", { className: `detail-body ${tab === "Flow" ? "detail-body-flow" : tab === "Questions" ? "detail-body-questions" : ""}`, children: _jsxs(Suspense, { fallback: _jsx("p", { className: "muted", role: "status", children: "Loading questions\u2026" }), children: [trace.error && (_jsxs("div", { role: "alert", className: "error-banner", children: [_jsx("strong", { children: trace.error.name }), " ", trace.error.message] })), trace.captureWarnings.map((w) => (_jsx("p", { className: "warning", children: w }, w))), tab === "Flow" && _jsx(TraceDiagram, { trace: trace }, trace.id), tab === "Questions" && (_jsx(Questions, { trace: trace }, trace.id)), tab === "Context" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "muted", children: "Captured before dispatch without changing SDK data. Known credential fields remain redacted; additional display masking follows the component prop. Captures are size limited." }), _jsx(CaptureFields, { trace: trace, path: ["request", "state"] }), !captureFieldUnavailable(trace, ["request", "state"]) && (_jsxs(_Fragment, { children: [findImage(trace.request.state) && (_jsx("img", { className: "context-image", src: findImage(trace.request.state), alt: "Captured context" })), _jsx(JsonBlock, { value: trace.request.state })] })), trace.request.assets && (_jsx(JsonBlock, { value: trace.request.assets }))] })), tab === "Transport" && (_jsxs(_Fragment, { children: [_jsx("h3", { children: "HTTP attempts" }), _jsx("p", { className: "muted", children: "Attempt duration ends at response headers. Total duration includes SDK processing and retry waits." }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Attempt" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: trace.attempts.map((a) => (_jsxs("tr", { children: [_jsx("td", { children: a.number }), _jsx("td", { children: a.error || a.status || "Pending" }), _jsx("td", { children: ms(a.durationMs) })] }, a.number))) })] }), !trace.attempts.length && _jsx("p", { children: "No HTTP attempt recorded." })] })), tab === "Raw" && _jsx(JsonBlock, { value: trace })] }) })] })) : (_jsxs("div", { className: "empty-detail", children: [_jsx("span", { className: "empty-symbol", children: "\u21B3" }), _jsx("h2", { children: "Follow a Jev request" }), _jsxs("p", { children: ["Context, parallel questions, typed answers.", _jsx("br", {}), "Run a sample above to see the complete flow."] })] })) }));
|
|
37
51
|
});
|
|
38
|
-
export function JevPanel({ traces, flows = emptyFlows, connection, onClear, }) {
|
|
52
|
+
export function JevPanel({ traces: incomingTraces, flows = emptyFlows, connection, onClear, }) {
|
|
53
|
+
const traces = useMemo(() => incomingTraces.map(traceCaptureView), [incomingTraces]);
|
|
39
54
|
const [view, setView] = useState("requests");
|
|
40
55
|
const [selectedRun, setSelectedRun] = useState();
|
|
41
56
|
const run = flows.runs.find((r) => r.id === selectedRun) ?? flows.runs[0];
|
package/lib/src/Questions.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CaptureFields, PartialBox } from "./CaptureFields.js";
|
|
3
|
+
import { captureFieldUnavailable, captureQuestionEntries, capturedValue, traceCaptureView, } from "./capture-view.js";
|
|
2
4
|
import { memo, useCallback, useEffect, useLayoutEffect, useId, useMemo, useRef, useState, } from "react";
|
|
3
5
|
import { defaultRangeExtractor, measureElement, observeElementRect, useVirtualizer, } from "@tanstack/react-virtual";
|
|
4
6
|
import { Answer, findImage, JsonBlock } from "./TraceContent.js";
|
|
5
|
-
const QuestionContent = memo(function QuestionContent({ name, question, answer, image, legendOpen, onLegendChange, }) {
|
|
6
|
-
return (_jsxs(_Fragment, { children: [_jsxs("header", { children: [_jsx("h3", { children: name }), _jsx("span", { className: "type", children: question.type })] }), _jsx(JsonBlock, { value:
|
|
7
|
+
const QuestionContent = memo(function QuestionContent({ trace, name, question, answer, image, legendOpen, onLegendChange, }) {
|
|
8
|
+
return (_jsxs(_Fragment, { children: [_jsxs("header", { children: [_jsx("h3", { children: name }), _jsx("span", { className: "type", children: question.type })] }), _jsx(CaptureFields, { trace: trace, path: ["request", "questions", name] }), _jsx(JsonBlock, { value: capturedValue(trace, ["request", "questions", name]) }), _jsx(CaptureFields, { trace: trace, path: ["result", "answers", name] }), _jsx(PartialBox, { trace: trace, name: name }), !captureFieldUnavailable(trace, ["result", "answers", name]) && (_jsx(Answer, { answer: answer, image: image, legendOpen: legendOpen, onLegendToggle: (open) => onLegendChange(name, open) }))] }));
|
|
7
9
|
});
|
|
8
10
|
/** Variable-height rows retain full answers; only offscreen DOM is omitted. */
|
|
9
11
|
export function Questions({ trace }) {
|
|
12
|
+
trace = traceCaptureView(trace);
|
|
10
13
|
const helpId = useId();
|
|
11
|
-
const entries = useMemo(() =>
|
|
14
|
+
const entries = useMemo(() => captureQuestionEntries(trace), [trace]);
|
|
12
15
|
const image = useMemo(() => findImage(trace.request.state), [trace.request.state]);
|
|
13
16
|
const viewport = useRef(null);
|
|
14
17
|
const sizes = useRef(new Map());
|
|
@@ -96,7 +99,9 @@ export function Questions({ trace }) {
|
|
|
96
99
|
// Scroll-range updates must not rebuild the full question picker.
|
|
97
100
|
const navigation = useMemo(() => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "questions-navigation", children: [_jsxs("label", { children: ["Jump to question", _jsx("select", { "aria-label": "Jump to question", value: Math.min(destination, entries.length - 1), onChange: (event) => goTo(Number(event.target.value)), children: entries.map(([name, q], index) => (_jsxs("option", { value: index, children: [index + 1, ". ", name, " \u00B7 ", q.type] }, name))) })] }), _jsx("button", { "aria-label": "Previous question", disabled: destination === 0, onClick: () => goTo(destination - 1), children: "Previous" }), _jsx("button", { "aria-label": "Next question", disabled: destination >= entries.length - 1, onClick: () => goTo(destination + 1), children: "Next" }), _jsxs("span", { children: [entries.length, " question", entries.length === 1 ? "" : "s"] })] }), _jsx("p", { className: "questions-help muted", id: helpId, children: "\u2191 / \u2193 previous / next \u00B7 Home / End first / last when the list is focused." })] })), [entries, destination, goTo, helpId]);
|
|
98
101
|
if (!entries.length)
|
|
99
|
-
return
|
|
102
|
+
return captureFieldUnavailable(trace, ["request", "questions"]) ? (_jsx(CaptureFields, { trace: trace, path: captureFieldUnavailable(trace, ["request"])
|
|
103
|
+
? ["request"]
|
|
104
|
+
: ["request", "questions"] })) : (_jsx("p", { className: "muted", children: "No questions available in this capture." }));
|
|
100
105
|
return (_jsxs("div", { className: "questions-list", children: [navigation, _jsx("div", { className: "questions-viewport", ref: viewport, tabIndex: 0, role: "region", "aria-label": "Questions and answers", "aria-describedby": helpId, onScroll: (event) => {
|
|
101
106
|
const offset = event.currentTarget.scrollTop;
|
|
102
107
|
const time = performance.now();
|
|
@@ -169,6 +174,6 @@ export function Questions({ trace }) {
|
|
|
169
174
|
const preview = seeking && focused !== item.index;
|
|
170
175
|
return (_jsx("div", { "data-index": item.index, "data-question-key": name, "data-preview": preview || undefined, style: preview
|
|
171
176
|
? { height: sizes.current.get(name) || 600 }
|
|
172
|
-
: undefined, ref: virtualizer.measureElement, className: "questions-row", role: "listitem", "aria-posinset": item.index + 1, "aria-setsize": entries.length, children: _jsx("article", { className: "question-node standalone", tabIndex: -1, "aria-busy": preview || undefined, "aria-label": `Question ${item.index + 1}: ${name}`, children: preview ? (_jsxs("div", { className: "question-scroll-preview", children: [_jsxs("header", { children: [_jsx("h3", { children: name }), _jsx("span", { className: "type", children: question.type })] }), _jsx("p", { className: "muted", children: "Pause scrolling to inspect this answer." })] })) : (_jsx(QuestionContent, { name: name, question: question, answer: trace.result?.answers[name], image: image, legendOpen: !!legends.get(name), onLegendChange: onLegendChange })) }) }, item.key));
|
|
177
|
+
: undefined, ref: virtualizer.measureElement, className: "questions-row", role: "listitem", "aria-posinset": item.index + 1, "aria-setsize": entries.length, children: _jsx("article", { className: "question-node standalone", tabIndex: -1, "aria-busy": preview || undefined, "aria-label": `Question ${item.index + 1}: ${name}`, children: preview ? (_jsxs("div", { className: "question-scroll-preview", children: [_jsxs("header", { children: [_jsx("h3", { children: name }), _jsx("span", { className: "type", children: question.type })] }), _jsx("p", { className: "muted", children: "Pause scrolling to inspect this answer." })] })) : (_jsx(QuestionContent, { trace: trace, name: name, question: question, answer: trace.result?.answers[name], image: image, legendOpen: !!legends.get(name), onLegendChange: onLegendChange })) }) }, item.key));
|
|
173
178
|
}) }) })] }));
|
|
174
179
|
}
|