> ## Documentation Index
> Fetch the complete documentation index at: https://docs.transluce.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Viewing results

> Explore jobs in the experiment viewer, report results, and write custom renderers.

## The experiment viewer

Start the viewer from your project:

```bash theme={null}
uv run fxtr view
```

It serves the viewer at `http://127.0.0.1:8000/` and opens it in your browser (`--no-open` only
prints the URL). Pick a job from the menu above the minimap, or open a job's link directly; the
launcher prints one like `http://127.0.0.1:8000/#job=<job id>`. A job that's still running updates
live.

For each job, the viewer shows:

* the tree of workflow invocations, each a list of **cards** for its steps and child workflows,
  showing the function's docstring, how it was mapped, and its progress;
* the arrays each operation received and produced, with links to the entities they reference;
* links from each card to the function's code in your editor;
* your project's `README.md`, linked from the side rail.

Everything is shown with default renderers until you write your own, and simple dataclass entities
display well without any.

## Report results

Have the root workflow return one **report** entity that references every result worth
presenting. The launcher then gets the whole run from one result, and a results overview can read
everything from it:

```python theme={null}
@dataclass(frozen=True)
class Report(DataclassEntity, fxtr_type="org.example.my_experiment.Report.v1"):
    scores: BoundID[Array[float]]
    summary: BoundID[Array[float]]


@workflow(name="my_experiment.run")
async def experiment(context: WorkflowContext, ...) -> Report:
    """Score every answer and summarize the scores per model."""
    scores = context.run_step("scores", score, {...}, map_over=["model", "task"])
    summary = context.run_step("summary", summarize, {"scores": scores}, map_over=["model"])
    return Report(
        scores=await context.store(await scores.observe()),
        summary=await context.store(await summary.observe()),
    )
```

Observing inside the workflow is fine here: nothing computes from the report, and each array it
references is still a step output in the graph. In the launcher, load the report with
`await client.load(result.item(), as_type=Report)`, then each array with
`await client.load(report.scores)`.

## Custom renderers

Renderers are React components, written in TypeScript in your project's `views/` package, that
control how the viewer shows your entities and invocations. Write one when an entity holds a lot
of data, or when some of its fields deserve more emphasis than others.

### Register renderers

Register each renderer for a **slot** and a **subject**:

| Slot | Shows | Subject |
| - | - | - |
| `EntityPanel` | An entity in the detail pane | The entity's type ID |
| `EntityInlineLink` | An entity where it's referenced | The entity's type ID |
| `EntityHoverPreview` | An entity on hover | The entity's type ID |
| `InvocationCard` | A step or workflow's card | Its registered name |
| `InvocationPanel` | A full-pane view of an invocation | Its registered name |

Export the registrations from `views/src/bundle.ts`:

```ts views/src/bundle.ts theme={null}
import { EntityPanel, InvocationPanel, renderer, singleInvocation, type ViewBundle } from "fxtr-view";
import { DocumentPanel, Overview } from "./components";

const bundle: ViewBundle = {
  renderers: [
    renderer(EntityPanel, "org.example.my_experiment.Document.v1", "my-project.DocumentPanel", DocumentPanel),
    renderer(InvocationPanel, "my_experiment.run", "my-project.Overview", singleInvocation(Overview)),
  ],
};

export default bundle;
```

Give each renderer an ID prefixed with your project's name. Two renderers for the same slot and
subject with the same ID, including a clash with the viewer's own renderers, keep the whole
bundle from loading.

Inside a renderer, load an entity with `unwrap(useEntity(id))`. `singleInvocation(View)` loads the
invocation an `InvocationPanel` is given and passes it to `View`; its inputs and output are array
entity IDs to load in turn. The `fxtr-view-kit` package has shared building blocks for layouts and
field lists.

<Tip>
  Write every color as one of the viewer's design tokens, such as `var(--fg)`, `var(--bg)`, or
  `var(--select-accent)`, never a literal like `#fff`. The viewer has light and dark themes, and
  the tokens switch with it.
</Tip>

### Build and reload

```bash theme={null}
(cd views && pnpm install)   # once
uv run fxtr views build
```

This builds the package into its view bundle. The viewer loads the bundle when the page loads, so
reload the page after a rebuild. If the bundle is missing or fails to load, the viewer runs with
its default renderers and says why in the side rail.

## Results overviews

A **results overview** is an `InvocationPanel` renderer keyed on the root workflow's registered
name. The viewer offers it as a full-pane view of the run, in place of the trace, and it usually
reads the experiment's report entity.

Some guidance for designing one:

* **Start from the question.** Decide what someone most needs to see from this experiment. It may
  come from several steps, not just the last one.
* **Show distributions, not just averages**, when there's room, without making the chart too busy
  to read.
* **Keep axes consistent.** Charts of the same kind side by side should share ranges and
  category order.
* **For two or more categorical variables**, consider a table of X × Y, small multiples (a grid of
  tables or charts, one per value of a third variable), or a table with a small chart in each cell.
* **Link everything to its evidence.** Anything in a chart that corresponds to an entity, such as a
  verdict or a conversation, should be clickable, so readers can check the data behind it.
