Skip to content

Commit 3735fbb

Browse files
justjuanguiclaude
andcommitted
feat(ui): add structured explanation views for stats, formulas, and damage flows
New visualization system for evaluation explanations: - ExplanationView: Router component that detects explanation type - PipelineView: Stats with pipeline phases visualization - PhaseView: Individual phase display with nested group_total support - ContributionTable: Contribution display with template grouping - DamageFlowView: Damage flow with distribution bar and phase breakdown - FormulaView: Formula expression visualization - type-guards.ts: Type guards and detail extractors for node types Includes: - Proper parsing of contribution details with metadata extraction - Support for nested group_total calls within phases - Damage distribution calculated from actual damage values - Template group aggregation with filter info 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f901037 commit 3735fbb

13 files changed

+1765
-190
lines changed

src/lib/engine/types.ts

Lines changed: 211 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,224 @@ export type XNode = {
119119
children?: XNode[];
120120
};
121121

122-
// New WASM evaluation node structure
122+
// ============================================================================
123+
// Explanation Node Types (matching new WASM Explain API)
124+
// ============================================================================
125+
126+
/**
127+
* Base evaluation node structure returned by WASM Explain
128+
*/
123129
export type EvaluationNode = {
124130
type: string;
125131
name: string;
126132
value: number;
127133
children?: EvaluationNode[];
128-
meta?: Record<string, string | number>;
129-
details?: Record<string, unknown>;
134+
meta?: Record<string, string>; // String-only, primarily for errors
135+
details?: NodeDetails;
130136
};
131137

132-
// New explain payload structure returned by WASM
138+
/**
139+
* Union of all possible details structures by node type
140+
*/
141+
export type NodeDetails =
142+
| OutputDetails
143+
| StatDetails
144+
| PipelineDetails
145+
| ContributionDetails
146+
| AggregateDetails
147+
| DamageFlowDetails
148+
| DamageFlowPhaseDetails
149+
| BinaryOpDetails
150+
| UnaryOpDetails
151+
| ConditionalDetails
152+
| VariableDetails
153+
| FlagDetails
154+
| ClampDetails
155+
| FunctionDetails
156+
| FormulaCallDetails
157+
| GroupTotalDetails
158+
| Record<string, unknown>; // Fallback for unknown types
159+
160+
/**
161+
* Output node details (wrapper for formula/stat)
162+
*/
163+
export interface OutputDetails {
164+
type: "formula" | "stat";
165+
formula?: string;
166+
stat?: string;
167+
}
168+
169+
/**
170+
* Stat node details
171+
*/
172+
export interface StatDetails {
173+
template: string;
174+
}
175+
176+
/**
177+
* Pipeline node details
178+
*/
179+
export interface PipelineDetails {
180+
type: "pipeline";
181+
phase_count: number;
182+
final_phase_value: number;
183+
phase_values: Record<string, number>;
184+
pipeline_steps: string[];
185+
}
186+
187+
/**
188+
* Contribution node details
189+
*/
190+
export interface ContributionDetails {
191+
source: string;
192+
layer: string;
193+
original_value: number;
194+
is_start: boolean;
195+
has_condition: boolean;
196+
has_calculation: boolean;
197+
condition?: string;
198+
calculation?: string;
199+
transformed_value?: number;
200+
// meta_* fields are dynamically added with this prefix
201+
[key: `meta_${string}`]: unknown;
202+
}
203+
204+
/**
205+
* Aggregate function node details (sum_by, prod_by, etc.)
206+
*/
207+
export interface AggregateDetails {
208+
function: string;
209+
operation: string;
210+
filter_group: string;
211+
contribution_count: number;
212+
values: number[];
213+
has_transform: boolean;
214+
operation_result: string;
215+
filter_layer?: string;
216+
filter_unit?: string;
217+
filter_who?: string;
218+
}
219+
220+
/**
221+
* Damage flow node details
222+
*/
223+
export interface DamageFlowDetails {
224+
damage_flow: string;
225+
base_damage: number;
226+
skill_damage_modifier: number;
227+
total_damage: number;
228+
average_damage: number;
229+
distribution: Record<string, number>;
230+
damage: Record<string, number>; // actual final damage per type
231+
}
232+
233+
/**
234+
* Damage flow phase node details (phases 1-8)
235+
*/
236+
export interface DamageFlowPhaseDetails {
237+
phase: number;
238+
name: string;
239+
value?: number;
240+
distribution?: Record<string, number>;
241+
additional_damage?: Record<string, number>;
242+
final_damage?: Record<string, number>;
243+
character_additional?: Record<string, number>;
244+
total_damage?: number;
245+
average_damage?: number;
246+
}
247+
248+
/**
249+
* Binary operation node details
250+
*/
251+
export interface BinaryOpDetails {
252+
operator: string;
253+
left_value: number;
254+
right_value: number;
255+
operation: string;
256+
}
257+
258+
/**
259+
* Unary operation node details
260+
*/
261+
export interface UnaryOpDetails {
262+
operator: string;
263+
operand_value: number;
264+
operation: string;
265+
}
266+
267+
/**
268+
* Conditional (ternary) node details
269+
*/
270+
export interface ConditionalDetails {
271+
type: "ternary";
272+
condition_value: number;
273+
chosen_branch: "then" | "else";
274+
operation: string;
275+
}
276+
277+
/**
278+
* Variable reference node details
279+
*/
280+
export interface VariableDetails {
281+
variable: string;
282+
context: string;
283+
}
284+
285+
/**
286+
* Flag function node details
287+
*/
288+
export interface FlagDetails {
289+
function: "flag";
290+
flag: string;
291+
context: string;
292+
flag_state: boolean;
293+
operation: string;
294+
}
295+
296+
/**
297+
* Clamp function node details
298+
*/
299+
export interface ClampDetails {
300+
function: "clamp";
301+
original_value: number;
302+
min_value: number;
303+
max_value: number;
304+
operation: string;
305+
}
306+
307+
/**
308+
* Math function node details (min, max, sum, prod)
309+
*/
310+
export interface FunctionDetails {
311+
function: string;
312+
values: number[];
313+
operation: string;
314+
}
315+
316+
/**
317+
* Formula call node details
318+
*/
319+
export interface FormulaCallDetails {
320+
function: string;
321+
type: "formula_call";
322+
arg_count: number;
323+
named_args: number;
324+
}
325+
326+
/**
327+
* Group total function node details
328+
*/
329+
export interface GroupTotalDetails {
330+
function: "group_total";
331+
group: string;
332+
template: string;
333+
operation: string;
334+
self_var?: string;
335+
}
336+
337+
/**
338+
* Explain payload structure returned by WASM
339+
*/
133340
export type ExplainPayload = {
134341
debug: EvaluationNode;
135342
human: string[];

src/lib/ui/ExplanationView.svelte

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script lang="ts">
2+
import type { EvaluationNode } from "$lib/engine/types";
3+
import { parseExplanation } from "./phase/phase-parser";
4+
import PipelineView from "./phase/PipelineView.svelte";
5+
import FormulaView from "./phase/FormulaView.svelte";
6+
import DamageFlowView from "./phase/DamageFlowView.svelte";
7+
import XNodeTree from "./XNodeTree.svelte";
8+
9+
interface Props {
10+
node: EvaluationNode;
11+
}
12+
13+
const { node }: Props = $props();
14+
15+
const parsed = $derived(parseExplanation(node));
16+
</script>
17+
18+
<div class="space-y-3">
19+
<!-- Type indicator -->
20+
<div class="text-xs text-base-content/60">
21+
{#if parsed.rootType === "stat"}
22+
Stat with Pipeline
23+
{:else if parsed.rootType === "damage_flow"}
24+
Damage Flow
25+
{:else}
26+
Formula
27+
{/if}
28+
</div>
29+
30+
<!-- Content -->
31+
{#if parsed.rootType === "damage_flow" && parsed.damageFlow}
32+
<DamageFlowView damageFlow={parsed.damageFlow} name={parsed.name} />
33+
{:else if parsed.rootType === "stat" && parsed.pipeline}
34+
<PipelineView pipeline={parsed.pipeline} />
35+
{:else if parsed.rootType === "formula" && parsed.formula}
36+
<FormulaView
37+
formula={parsed.formula}
38+
name={parsed.name}
39+
value={parsed.value}
40+
/>
41+
{:else}
42+
<!-- Fallback to raw tree if no structured view available -->
43+
<XNodeTree {node} expandAll={false} />
44+
{/if}
45+
</div>

src/lib/ui/PhaseDebugView.svelte

Lines changed: 9 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,19 @@
11
<script lang="ts">
22
import { type EvaluationNode } from "$lib/engine/types";
3-
import { parsePhases } from "./phase/phase-parser";
4-
import PhaseWaterfallView from "./phase/PhaseWaterfallView.svelte";
5-
import PhaseDebugView from "./PhaseDebugView.svelte";
3+
import ExplanationView from "./ExplanationView.svelte";
64
75
interface Props {
86
node: EvaluationNode;
97
}
108
119
const { node }: Props = $props();
12-
13-
// Parse phases using the tree visitor utility
14-
const phases = $derived(parsePhases(node));
15-
16-
// Helper for formatting values in fallback view
17-
const fmt = (n: number) =>
18-
Number.isInteger(n) ? String(n) : n.toFixed(4).replace(/\.?0+$/, "");
19-
20-
const metaEntries = (m?: Record<string, string | number>) =>
21-
Object.entries(m ?? {});
2210
</script>
2311

24-
<div class="space-y-3">
25-
{#if phases.length === 0}
26-
<!-- Fallback: show original tree if no phases detected -->
27-
<div class="space-y-2">
28-
<div class="bg-base-100 border border-base-300 rounded-lg">
29-
<div class="p-3">
30-
<div class="flex items-center gap-2">
31-
<span class="font-mono text-sm font-medium">
32-
{node.name || "(unnamed)"}
33-
</span>
34-
{#if node.type}
35-
<span class="badge badge-ghost badge-xs">{node.type}</span>
36-
{/if}
37-
<span class="badge badge-outline badge-xs font-mono">
38-
= {fmt(node.value)}
39-
</span>
40-
</div>
41-
42-
{#if node.meta && metaEntries(node.meta).length}
43-
<div class="flex flex-wrap gap-1 text-xs mt-2">
44-
{#each metaEntries(node.meta) as [k, v] (k)}
45-
<span class="badge badge-ghost badge-xs">{k}: {v}</span>
46-
{/each}
47-
</div>
48-
{/if}
49-
50-
{#if node.children?.length}
51-
<div class="mt-2 space-y-1">
52-
{#each node.children as c (c.name || Math.random())}
53-
<PhaseDebugView node={c} />
54-
{/each}
55-
</div>
56-
{/if}
57-
</div>
58-
</div>
59-
</div>
60-
{:else}
61-
<!-- Render table view directly -->
62-
<PhaseWaterfallView {phases} />
63-
{/if}
64-
</div>
12+
<!--
13+
PhaseDebugView is now a wrapper around ExplanationView for backward compatibility.
14+
The new ExplanationView handles all explanation types:
15+
- Pipeline (stat with phases)
16+
- Damage Flow
17+
- Formula expressions
18+
-->
19+
<ExplanationView {node} />

0 commit comments

Comments
 (0)