@contraagent/ui 0.9.1 → 0.9.2
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/custom-elements.json +179 -3
- package/dist/adapters/filters.d.ts.map +1 -1
- package/dist/adapters/filters.js +3 -1
- package/dist/adapters/filters.js.map +1 -1
- package/dist/adapters/graph/compound.d.ts +2 -0
- package/dist/adapters/graph/compound.d.ts.map +1 -1
- package/dist/adapters/graph/compound.js +12 -1
- package/dist/adapters/graph/compound.js.map +1 -1
- package/dist/adapters/graph/layer.d.ts.map +1 -1
- package/dist/adapters/graph/layer.js +6 -1
- package/dist/adapters/graph/layer.js.map +1 -1
- package/dist/adapters/graph/matrix.d.ts.map +1 -1
- package/dist/adapters/graph/matrix.js +43 -9
- package/dist/adapters/graph/matrix.js.map +1 -1
- package/dist/adapters/lanes.d.ts.map +1 -1
- package/dist/adapters/lanes.js +26 -9
- package/dist/adapters/lanes.js.map +1 -1
- package/dist/adapters/panel-body.d.ts.map +1 -1
- package/dist/adapters/panel-body.js +15 -3
- package/dist/adapters/panel-body.js.map +1 -1
- package/dist/contraagent-ui.js +683 -683
- package/dist/elements/lists/ca-contracts.d.ts +4 -0
- package/dist/elements/lists/ca-contracts.d.ts.map +1 -1
- package/dist/elements/lists/ca-contracts.js +33 -13
- package/dist/elements/lists/ca-contracts.js.map +1 -1
- package/dist/elements/lists/ca-journal-table.d.ts +3 -0
- package/dist/elements/lists/ca-journal-table.d.ts.map +1 -1
- package/dist/elements/lists/ca-journal-table.js +18 -5
- package/dist/elements/lists/ca-journal-table.js.map +1 -1
- package/dist/elements/lists/ca-panel-body.d.ts +3 -0
- package/dist/elements/lists/ca-panel-body.d.ts.map +1 -1
- package/dist/elements/lists/ca-panel-body.js +11 -1
- package/dist/elements/lists/ca-panel-body.js.map +1 -1
- package/dist/elements/lists/ca-task-table.d.ts +2 -0
- package/dist/elements/lists/ca-task-table.d.ts.map +1 -1
- package/dist/elements/lists/ca-task-table.js +5 -1
- package/dist/elements/lists/ca-task-table.js.map +1 -1
- package/dist/elements/overview/ca-overview.d.ts.map +1 -1
- package/dist/elements/overview/ca-overview.js +3 -1
- package/dist/elements/overview/ca-overview.js.map +1 -1
- package/dist/elements/overview/ca-pipeline-graph.d.ts +10 -1
- package/dist/elements/overview/ca-pipeline-graph.d.ts.map +1 -1
- package/dist/elements/overview/ca-pipeline-graph.js +57 -17
- package/dist/elements/overview/ca-pipeline-graph.js.map +1 -1
- package/dist/elements/overview/pipeline-graph/inspector.js +2 -1
- package/dist/elements/overview/pipeline-graph/inspector.js.map +1 -1
- package/dist/elements/overview/pipeline-graph/strings.d.ts.map +1 -1
- package/dist/elements/overview/pipeline-graph/strings.js +15 -1
- package/dist/elements/overview/pipeline-graph/strings.js.map +1 -1
- package/dist/words/clock-format.d.ts +3 -0
- package/dist/words/clock-format.d.ts.map +1 -0
- package/dist/words/clock-format.js +19 -0
- package/dist/words/clock-format.js.map +1 -0
- package/dist/words/format.d.ts.map +1 -1
- package/dist/words/format.js +2 -17
- package/dist/words/format.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/filters.ts +4 -1
- package/src/adapters/graph/compound.ts +9 -1
- package/src/adapters/graph/layer.ts +4 -1
- package/src/adapters/graph/matrix.ts +38 -7
- package/src/adapters/lanes.ts +29 -11
- package/src/adapters/panel-body.ts +13 -3
- package/src/elements/lists/ca-contracts.ts +29 -11
- package/src/elements/lists/ca-journal-table.ts +19 -5
- package/src/elements/lists/ca-panel-body.ts +11 -1
- package/src/elements/lists/ca-task-table.ts +6 -1
- package/src/elements/overview/ca-overview.ts +3 -1
- package/src/elements/overview/ca-pipeline-graph.ts +54 -15
- package/src/elements/overview/pipeline-graph/inspector.ts +2 -1
- package/src/elements/overview/pipeline-graph/strings.ts +15 -1
- package/src/words/clock-format.ts +20 -0
- package/src/words/format.ts +2 -18
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A loop's passes: a loop box unrolled into units by (node, pass).
|
|
2
2
|
import type { Plan, TaskState } from '@contraagent/api';
|
|
3
3
|
import { arrange, type GraphOrder } from '@contraagent/elements/graph';
|
|
4
|
-
import {
|
|
4
|
+
import { passesAt, push, type FlowTask } from './flow-index.js';
|
|
5
5
|
import { arranged, flowMarks, project, shapeOf, taskMark, type Draft, type Projection } from './project.js';
|
|
6
6
|
import { SEVERITY, worstOf } from './states.js';
|
|
7
7
|
import { LOOP_SEGMENT, type LayerMark, type GraphNode, type LoopBox } from './types.js';
|
|
@@ -62,10 +62,39 @@ export function passMatrix(plan: Plan, path: readonly string[], box: string, pre
|
|
|
62
62
|
const rel = (t: FlowTask) => passesAt(t, d).slice(i0);
|
|
63
63
|
const leaves = (x: LoopBox): string[] => x.layers.flat().flatMap((id) => (byBox.has(id) ? leaves(byBox.get(id) as LoopBox) : [id]));
|
|
64
64
|
const tasksOf = (x: LoopBox): FlowTask[] => leaves(x).flatMap((id) => drafts.get(id)?.tasks ?? []);
|
|
65
|
-
|
|
66
|
-
const
|
|
65
|
+
// Two pass lists are the same (`cmpPasses`, of one length) exactly when their JSON is.
|
|
66
|
+
const passKey = (v: readonly number[]) => JSON.stringify(v);
|
|
67
|
+
// Each node's tasks by their passes, in task order.
|
|
68
|
+
const byPasses = new Map<string, Map<string, FlowTask[]>>();
|
|
69
|
+
const tasksAt = (id: string, at: readonly number[]): readonly FlowTask[] => {
|
|
70
|
+
let m = byPasses.get(id);
|
|
71
|
+
if (!m) {
|
|
72
|
+
m = new Map();
|
|
73
|
+
for (const t of drafts.get(id)?.tasks ?? []) push(m, passKey(rel(t)), t);
|
|
74
|
+
byPasses.set(id, m);
|
|
75
|
+
}
|
|
76
|
+
return m.get(passKey(at)) ?? [];
|
|
77
|
+
};
|
|
78
|
+
// The passes the tasks of a box ran after each list of earlier passes: by box, by the length of that list.
|
|
79
|
+
const nextPasses = new Map<LoopBox, Map<number, Map<string, Set<number>>>>();
|
|
80
|
+
const passesAfter = (x: LoopBox, prefix: readonly number[]): number[] => {
|
|
67
81
|
const k = prefix.length;
|
|
68
|
-
|
|
82
|
+
let byK = nextPasses.get(x);
|
|
83
|
+
if (!byK) nextPasses.set(x, (byK = new Map<number, Map<string, Set<number>>>()));
|
|
84
|
+
let byPrefix = byK.get(k);
|
|
85
|
+
if (!byPrefix) {
|
|
86
|
+
byPrefix = new Map();
|
|
87
|
+
for (const v of tasksOf(x).map(rel)) {
|
|
88
|
+
if (v.length <= k) continue;
|
|
89
|
+
const key = passKey(v.slice(0, k));
|
|
90
|
+
(byPrefix.get(key) ?? byPrefix.set(key, new Set()).get(key))?.add(v[k] as number);
|
|
91
|
+
}
|
|
92
|
+
byK.set(k, byPrefix);
|
|
93
|
+
}
|
|
94
|
+
return [...(byPrefix.get(passKey(prefix)) ?? [])].sort((a, c) => a - c);
|
|
95
|
+
};
|
|
96
|
+
const build = (x: LoopBox, prefix: number[]): ColTree[] => {
|
|
97
|
+
const passes = passesAfter(x, prefix);
|
|
69
98
|
return passes.flatMap((p): ColTree[] => {
|
|
70
99
|
const at = [...prefix, p];
|
|
71
100
|
const kids = x.layers.flat().flatMap((id): ColTree[] => {
|
|
@@ -75,7 +104,7 @@ export function passMatrix(plan: Plan, path: readonly string[], box: string, pre
|
|
|
75
104
|
return sub.length ? [{ type: 'loop', box: inner, kids: sub }] : [];
|
|
76
105
|
}
|
|
77
106
|
const node = byNode.get(id);
|
|
78
|
-
return node &&
|
|
107
|
+
return node && tasksAt(id, at).length ? [{ type: 'node', node, passes: at }] : [];
|
|
79
108
|
});
|
|
80
109
|
return kids.length ? [{ type: 'pass', box: x, pass: p, kids }] : [];
|
|
81
110
|
});
|
|
@@ -103,7 +132,7 @@ export function passMatrix(plan: Plan, path: readonly string[], box: string, pre
|
|
|
103
132
|
const rowUnits = [...graph.units.filter((u) => unitsIn.has(u)), ...(unitsIn.has('') ? [''] : [])];
|
|
104
133
|
const cellTasks = new Map<string, FlowTask[]>(); // `${column}\0${unit}`
|
|
105
134
|
columns.forEach((c, ci) => {
|
|
106
|
-
for (const t of
|
|
135
|
+
for (const t of tasksAt(c.node.id, c.passes)) push(cellTasks, `${ci}\u0000${t.n.unit || ''}`, t);
|
|
107
136
|
});
|
|
108
137
|
const markOf = (c: MatrixColumn, ts: readonly FlowTask[], unit: string): LayerMark | null => {
|
|
109
138
|
const x = drafts.get(c.node.id) as Draft;
|
|
@@ -111,12 +140,14 @@ export function passMatrix(plan: Plan, path: readonly string[], box: string, pre
|
|
|
111
140
|
const m = flowMarks(ts, d, x.name, ix.calls);
|
|
112
141
|
return unit ? (m.byUnit.get(unit) ?? m.single[0] ?? null) : (m.single[0] ?? null);
|
|
113
142
|
};
|
|
143
|
+
const mineOf = new Map<string, FlowTask[]>();
|
|
144
|
+
for (const t of mine) push(mineOf, t.n.unit || '', t);
|
|
114
145
|
const rows = rowUnits.map((unit): MatrixRow => {
|
|
115
146
|
const cells = columns.map((c, ci) => {
|
|
116
147
|
const ts = cellTasks.get(`${ci}\u0000${unit}`);
|
|
117
148
|
return ts?.length ? markOf(c, ts, unit) : null;
|
|
118
149
|
});
|
|
119
|
-
const passes = new Set(
|
|
150
|
+
const passes = new Set((mineOf.get(unit) ?? []).map((t) => passesAt(t, d).slice(0, i0 + 1).join('.'))).size;
|
|
120
151
|
return { unit, passes, state: worstOf(cells.filter((m): m is LayerMark => m !== null).map((m) => m.state)), cells };
|
|
121
152
|
});
|
|
122
153
|
return { box: b, columns, head, rows };
|
package/src/adapters/lanes.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { isLaunched, isPython, runRelative, type JournalRow, type TaskOutcome }
|
|
|
5
5
|
import type { Lane, LaneBar, LaneStyle } from '@contraagent/elements/lanes';
|
|
6
6
|
import { msg, str } from '@lit/localize';
|
|
7
7
|
import { getLocale } from '../core/i18n.js';
|
|
8
|
+
import { clockFormat } from '../words/clock-format.js';
|
|
8
9
|
import { facts, fmtDur, kindUnit } from '../words/format.js';
|
|
9
10
|
import { modelSwitch, notStartedCause, stopLabel } from '../words/index.js';
|
|
10
11
|
|
|
@@ -28,7 +29,8 @@ export interface TimelineLanes {
|
|
|
28
29
|
omitted: number;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
// The same text as `toLocaleTimeString` with these options, from a format made once.
|
|
33
|
+
const hhmmss = (t: number) => clockFormat(getLocale(), false).format(new Date(t));
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* The bars' looks and the legend: who ran it is an identity cue (Python
|
|
@@ -67,6 +69,28 @@ function rowLabel(e: JournalRow, end: number, outcomes: ReadonlyMap<string, Task
|
|
|
67
69
|
.join('\n');
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
type FinishedBar = { lane: string; bar: LaneBar & { end: number } };
|
|
73
|
+
|
|
74
|
+
/** The finished bars last made for a journal, and what else they were made from. */
|
|
75
|
+
const made = new WeakMap<readonly JournalRow[], { outcomes: ReadonlyMap<string, TaskOutcome>; run: string; locale: string; bars: FinishedBar[] }>();
|
|
76
|
+
|
|
77
|
+
/** Each finished row's bar, on its lane: made again only when the journal, the outcomes, the run or the language changed. */
|
|
78
|
+
function finishedBars(journal: readonly JournalRow[], outcomes: ReadonlyMap<string, TaskOutcome>, run: string): readonly FinishedBar[] {
|
|
79
|
+
const locale = getLocale();
|
|
80
|
+
const had = made.get(journal);
|
|
81
|
+
if (had && had.outcomes === outcomes && had.run === run && had.locale === locale) return had.bars;
|
|
82
|
+
const bars: FinishedBar[] = [];
|
|
83
|
+
journal.forEach((e, i) => {
|
|
84
|
+
if (!e.ts) return;
|
|
85
|
+
const end = Date.parse(e.ts);
|
|
86
|
+
if (Number.isNaN(end)) return;
|
|
87
|
+
const style: LaneStyleKey = !isLaunched(e) ? 'idle' : !e.ok ? 'bad' : isPython(e) ? 'py' : 'llm';
|
|
88
|
+
bars.push({ lane: e.kind || '?', bar: { id: `j${i}`, style, start: end - (e.wall_s || 0) * 1000, end, label: rowLabel(e, end, outcomes, run) } });
|
|
89
|
+
});
|
|
90
|
+
made.set(journal, { outcomes, run, locale, bars });
|
|
91
|
+
return bars;
|
|
92
|
+
}
|
|
93
|
+
|
|
70
94
|
/**
|
|
71
95
|
* Every finished task as a bar on its kind's lane, in order of first start,
|
|
72
96
|
* and the running ones (`end` null); finished tasks that ended more than six
|
|
@@ -78,17 +102,11 @@ export function lanesFromJournal(
|
|
|
78
102
|
outcomes: ReadonlyMap<string, TaskOutcome>,
|
|
79
103
|
run: string,
|
|
80
104
|
): TimelineLanes | null {
|
|
81
|
-
|
|
82
|
-
const bars: { lane: string; bar: LaneBar & { end: number } }[] = [];
|
|
83
|
-
journal.forEach((e, i) => {
|
|
84
|
-
if (!e.ts) return;
|
|
85
|
-
const end = Date.parse(e.ts);
|
|
86
|
-
if (Number.isNaN(end)) return;
|
|
87
|
-
const style: LaneStyleKey = !isLaunched(e) ? 'idle' : !e.ok ? 'bad' : isPython(e) ? 'py' : 'llm';
|
|
88
|
-
bars.push({ lane: e.kind || '?', bar: { id: `j${i}`, style, start: end - (e.wall_s || 0) * 1000, end, label: rowLabel(e, end, outcomes, run) } });
|
|
89
|
-
});
|
|
105
|
+
const bars = finishedBars(journal, outcomes, run);
|
|
90
106
|
if (!bars.length && !live.length) return null;
|
|
91
|
-
|
|
107
|
+
let newest = -Infinity;
|
|
108
|
+
for (const x of bars) newest = Math.max(newest, x.bar.end);
|
|
109
|
+
for (const a of live) newest = Math.max(newest, a.t0);
|
|
92
110
|
const vis: { lane: string; bar: LaneBar }[] = bars.filter((x) => x.bar.end >= newest - TIMELINE_WINDOW_MS);
|
|
93
111
|
const omitted = bars.length - vis.length;
|
|
94
112
|
live.forEach((a, i) => vis.push({ lane: a.kind, bar: { id: `live${i}`, style: 'run', start: a.t0, end: null, label: `${a.kind}\n${msg('running')}` } }));
|
|
@@ -19,15 +19,25 @@ export const rowTone = (r: BodyRow): Tone | undefined => (typeof r._tone === 'st
|
|
|
19
19
|
/** The formats of numbers: their columns are right-aligned. */
|
|
20
20
|
const NUMERIC = new Set(['usd', 'pct', 'duration', 'tokens', 'number']);
|
|
21
21
|
|
|
22
|
-
/** A cell as the text it is ordered by. */
|
|
23
|
-
const
|
|
22
|
+
/** A cell as the text it is ordered by; an object's is written once. */
|
|
23
|
+
const objectKeys = new WeakMap<object, string>();
|
|
24
|
+
const cellKey = (v: unknown): string => {
|
|
25
|
+
if (typeof v === 'string') return v;
|
|
26
|
+
if (typeof v !== 'object' || v === null) return JSON.stringify(v);
|
|
27
|
+
let k = objectKeys.get(v);
|
|
28
|
+
if (k === undefined) objectKeys.set(v, (k = JSON.stringify(v)));
|
|
29
|
+
return k;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The order `localeCompare` gives, made once. */
|
|
33
|
+
const text = new Intl.Collator();
|
|
24
34
|
|
|
25
35
|
/** The order of two cells: numbers by value, then text; empty cells last. */
|
|
26
36
|
function compareCells(a: unknown, b: unknown): number {
|
|
27
37
|
const empty = (v: unknown) => v === null || v === undefined || v === '';
|
|
28
38
|
if (empty(a) || empty(b)) return empty(a) === empty(b) ? 0 : empty(a) ? 1 : -1;
|
|
29
39
|
if (typeof a === 'number' && typeof b === 'number') return a - b;
|
|
30
|
-
return cellKey(a)
|
|
40
|
+
return text.compare(cellKey(a), cellKey(b));
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
/** `v` as a URL a cell may lead to: http or https only; null for anything else. */
|
|
@@ -29,6 +29,9 @@ export const taskName = (c: { key: string; kind?: string; unit?: string }): stri
|
|
|
29
29
|
/** Problems first (the order of `CT_ORDER`), then by task. */
|
|
30
30
|
const rank = (s: ContractStatus | null | undefined) => (s ? CT_ORDER.indexOf(s) : CT_ORDER.length);
|
|
31
31
|
|
|
32
|
+
/** The tasks' keys in the order `localeCompare` puts them, made once. */
|
|
33
|
+
const byKey = new Intl.Collator();
|
|
34
|
+
|
|
32
35
|
/**
|
|
33
36
|
* The files each task must write (`OutputContract`) and read (`InputContract`),
|
|
34
37
|
* problems first, each with the chip of its status, then the passed ones under
|
|
@@ -77,6 +80,10 @@ export class CaContracts extends LitElement {
|
|
|
77
80
|
/** The run's plan: with the journal, what happened to the task that writes a missing input. Optional. */
|
|
78
81
|
@property({ attribute: false }) plan: Plan | null = null;
|
|
79
82
|
private outcomes: ReadonlyMap<string, TaskOutcome> = new Map();
|
|
83
|
+
/** The outputs' rows sorted, each output's writer, and each task's last journal row: for the objects they were made from. */
|
|
84
|
+
private sorted: { outputs: Outputs | null; outs: (readonly [ContractRecord, ContractSpec])[] } | null = null;
|
|
85
|
+
private writers: { outputs: Outputs | null; by: Map<string, readonly [ContractRecord, ContractSpec]> } | null = null;
|
|
86
|
+
private lastRows: { journal: JournalRow[]; by: Map<string, JournalRow> } | null = null;
|
|
80
87
|
|
|
81
88
|
/** Which contracts the reader opened, kept across Back and reload. */
|
|
82
89
|
private readonly open = new RememberedSet('contracts:open', this);
|
|
@@ -87,9 +94,13 @@ export class CaContracts extends LitElement {
|
|
|
87
94
|
if (!cs.length) {
|
|
88
95
|
return html`<div class="blank"><span class="ic" aria-hidden="true">▣</span><b>${facts(msg('No checked files'), msg('no task declares an OutputContract or an InputContract'))}</b></div>`;
|
|
89
96
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
if (this.sorted?.outputs !== this.outputs) {
|
|
98
|
+
const outs = cs
|
|
99
|
+
.flatMap((c) => (c.outputs ?? []).map((o) => [c, o] as const))
|
|
100
|
+
.sort(([a, x], [b, y]) => rank(x.status) - rank(y.status) || byKey.compare(a.key, b.key));
|
|
101
|
+
this.sorted = { outputs: this.outputs, outs };
|
|
102
|
+
}
|
|
103
|
+
const outs = this.sorted.outs;
|
|
93
104
|
const ins = cs.flatMap((c) => (c.inputs ?? []).map((i) => [c, i] as const));
|
|
94
105
|
const others = outs.filter(([, o]) => o.status !== 'valid');
|
|
95
106
|
const passed = outs.filter(([, o]) => o.status === 'valid');
|
|
@@ -139,20 +150,27 @@ export class CaContracts extends LitElement {
|
|
|
139
150
|
|
|
140
151
|
/** Who writes each output file: for naming the task a missing input should have come from. */
|
|
141
152
|
private producerOf = (path: string): Producer | undefined => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
153
|
+
if (this.writers?.outputs !== this.outputs) {
|
|
154
|
+
// The first task that declares the path, and its first output of it.
|
|
155
|
+
const by = new Map<string, readonly [ContractRecord, ContractSpec]>();
|
|
156
|
+
for (const c of this.outputs?.contracts ?? []) for (const o of c.outputs ?? []) if (!by.has(o.path)) by.set(o.path, [c, o]);
|
|
157
|
+
this.writers = { outputs: this.outputs, by };
|
|
145
158
|
}
|
|
146
|
-
|
|
159
|
+
const w = this.writers.by.get(path);
|
|
160
|
+
if (!w) return undefined;
|
|
161
|
+
const [c, o] = w;
|
|
162
|
+
return { name: taskName(c), written: !!o.file.exists, outcome: this.outcomes.get(c.key)?.outcome };
|
|
147
163
|
};
|
|
148
164
|
|
|
149
165
|
/** The reason of a task's last journal row (run-relative), when the journal was given. */
|
|
150
166
|
private reasonOf(key: string): string {
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
167
|
+
if (this.lastRows?.journal !== this.journal) {
|
|
168
|
+
const by = new Map<string, JournalRow>();
|
|
169
|
+
for (const r of this.journal) by.set(r.key, r);
|
|
170
|
+
this.lastRows = { journal: this.journal, by };
|
|
154
171
|
}
|
|
155
|
-
|
|
172
|
+
const r = this.lastRows.by.get(key);
|
|
173
|
+
return r ? runRelative(r.reason, this.run) : '';
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
private outRow(c: ContractRecord, o: ContractSpec) {
|
|
@@ -95,23 +95,37 @@ export class CaJournalTable extends LitElement {
|
|
|
95
95
|
/** The run is a replay that also launched (`mixedReplay`): each attempt played from its recording is marked, and counted in a filter. */
|
|
96
96
|
@property({ type: Boolean, attribute: 'replay-marks' }) replayMarks = false;
|
|
97
97
|
|
|
98
|
+
/** The counts of the rows, and the rows shown: for the rows (and filter) they were made from. */
|
|
99
|
+
private counts: { rows: JournalRow[]; replayMarks: boolean; fixups: number; retried: number; replayed: number; tally: ReturnType<typeof journalTally> } | null = null;
|
|
100
|
+
private shown: { rows: JournalRow[]; kind: string; stop: string; q: string; shown: JournalRow[] } | null = null;
|
|
101
|
+
|
|
98
102
|
private open(rows: readonly JournalRow[], id: string): void {
|
|
99
103
|
const e = rows.find((r) => rowId(r) === id);
|
|
100
104
|
if (e) emit(this, 'ca-open-task', isPython(e) ? { key: e.key, view: 'output' } : { key: e.key });
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
override render() {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
const s = this.shown;
|
|
109
|
+
const rows = s && s.rows === this.rows && s.kind === this.kind && s.stop === this.stop && s.q === this.q ? s.shown : filterJournal(this.rows, this.kind, this.stop, this.q).reverse();
|
|
110
|
+
this.shown = { rows: this.rows, kind: this.kind, stop: this.stop, q: this.q, shown: rows };
|
|
111
|
+
if (this.counts?.rows !== this.rows || this.counts.replayMarks !== this.replayMarks) {
|
|
112
|
+
this.counts = {
|
|
113
|
+
rows: this.rows,
|
|
114
|
+
replayMarks: this.replayMarks,
|
|
115
|
+
fixups: this.rows.filter(hadFixup).length,
|
|
116
|
+
retried: retriedKeys(this.rows).size,
|
|
117
|
+
replayed: this.replayMarks ? this.rows.filter((r) => r.replayed).length : 0,
|
|
118
|
+
tally: journalTally(this.rows),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const { fixups, retried, replayed, tally } = this.counts;
|
|
108
122
|
const chip = (k: string, label: string, n: number, tip: string, tone: Tone = 'neutral') =>
|
|
109
123
|
html`<button type="button" class="jchip ${this.stop === k ? 'on' : ''}" data-tone=${toneAttr(tone)} data-mark=${markOf(tone) ?? nothing}
|
|
110
124
|
aria-pressed=${this.stop === k} title=${tip}
|
|
111
125
|
@click=${() => emit(this, 'ca-filter', { stop: this.stop === k ? '' : k })}>${label} <b>${n}</b></button>`;
|
|
112
126
|
const columns = journalColumns({ run: this.run, outcomes: deriveRun(this.rows, this.plan).outcomes, journal: this.rows, rows, extras: this.extras, replayMarks: this.replayMarks });
|
|
113
127
|
return html`<div class="jtally">
|
|
114
|
-
${
|
|
128
|
+
${tally.map(([k, c]) => chip(k, shortStop(k), c, k, stopTone(k)))}
|
|
115
129
|
${fixups ? chip('fixup', msg('attempts with corrections'), fixups, correctionTip()) : nothing}
|
|
116
130
|
${retried ? chip('retried', msg('tasks retried'), retried, retryTip()) : nothing}
|
|
117
131
|
${replayed ? chip('replayed', msg('attempts replayed'), replayed, replayedTip()) : nothing}
|
|
@@ -85,6 +85,8 @@ export class CaPanelBody extends LitElement {
|
|
|
85
85
|
|
|
86
86
|
/** The order of each table's rows, by the table's place in the body. */
|
|
87
87
|
@state() private sorts = new Map<string, SortState>();
|
|
88
|
+
/** Each table's rows as last sorted, for the body and the order they were sorted for. */
|
|
89
|
+
private sorted = new Map<string, { b: TableBody; sort: SortState; rows: BodyRow[] }>();
|
|
88
90
|
/** The node the reader selected in each tree, by the tree's place in the body. */
|
|
89
91
|
@state() private picks = new Map<string, string>();
|
|
90
92
|
|
|
@@ -191,12 +193,20 @@ export class CaPanelBody extends LitElement {
|
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
195
|
|
|
196
|
+
private sortedRows(at: string, b: TableBody, sort: SortState, make: () => BodyRow[]): BodyRow[] {
|
|
197
|
+
const had = this.sorted.get(at);
|
|
198
|
+
if (had && had.b === b && had.sort === sort) return had.rows;
|
|
199
|
+
const rows = make();
|
|
200
|
+
this.sorted.set(at, { b, sort, rows });
|
|
201
|
+
return rows;
|
|
202
|
+
}
|
|
203
|
+
|
|
194
204
|
private table(b: TableBody, at: string): TemplateResult {
|
|
195
205
|
const columns = bodyColumns(b);
|
|
196
206
|
const rows = b.rows as BodyRow[];
|
|
197
207
|
const keys = new Map(rows.map((r, i) => [r, String(i)]));
|
|
198
208
|
const sort = this.sorts.get(at) ?? null;
|
|
199
|
-
const shown = sort ? sortRows(rows, columns, sort) : rows;
|
|
209
|
+
const shown = sort ? this.sortedRows(at, b, sort, () => sortRows(rows, columns, sort)) : rows;
|
|
200
210
|
return html`<cae-data-table
|
|
201
211
|
.columns=${columns}
|
|
202
212
|
.rows=${shown}
|
|
@@ -60,6 +60,9 @@ export class CaTaskTable extends LitElement {
|
|
|
60
60
|
/** The run id: paths in a cause are shown relative to it. */
|
|
61
61
|
@property() run = '';
|
|
62
62
|
|
|
63
|
+
/** The rows shown, for the rows, kind and text they were filtered and sorted from. */
|
|
64
|
+
private shown: { all: readonly TaskRow[]; kind: string; q: string; rows: TaskRow[] } | null = null;
|
|
65
|
+
|
|
63
66
|
private open(rows: readonly TaskRow[], key: string): void {
|
|
64
67
|
const t = rows.find((r) => r.key === key);
|
|
65
68
|
// A Python task has no transcript, but its detail shows what it was given and what it left.
|
|
@@ -69,7 +72,9 @@ export class CaTaskTable extends LitElement {
|
|
|
69
72
|
override render() {
|
|
70
73
|
const d = this.plan ? deriveRun(this.journal, this.plan) : null;
|
|
71
74
|
const all = d?.rows ?? [];
|
|
72
|
-
const
|
|
75
|
+
const had = this.shown;
|
|
76
|
+
const rows = had && had.all === all && had.kind === this.kind && had.q === this.q ? had.rows : sortTasks(filterTasks(all, this.kind, this.q));
|
|
77
|
+
this.shown = { all, kind: this.kind, q: this.q, rows };
|
|
73
78
|
if (!rows.length)
|
|
74
79
|
return html`<div class="blank"><span class="ic" aria-hidden="true">≡</span>${all.length ? msg('No tasks match the filter') : msg('No tasks')}</div>`;
|
|
75
80
|
const columns = taskColumns({ run: this.run, live: this.live, outcomes: d?.outcomes ?? new Map(), all, rows, journal: this.journal });
|
|
@@ -22,8 +22,10 @@ import {
|
|
|
22
22
|
import { localized, msg, str } from '@lit/localize';
|
|
23
23
|
import { LitElement, css, html, nothing } from 'lit';
|
|
24
24
|
import { customElement, property } from 'lit/decorators.js';
|
|
25
|
+
import { guard } from 'lit/directives/guard.js';
|
|
25
26
|
import { repeat } from 'lit/directives/repeat.js';
|
|
26
27
|
import { emit } from '../../core/events.js';
|
|
28
|
+
import { getLocale } from '../../core/i18n.js';
|
|
27
29
|
import { facts, kindUnit, localTime, sentence } from '../../words/format.js';
|
|
28
30
|
import { notStartedCause, outcomeLabel, stateCount, stopLabel } from '../../words/index.js';
|
|
29
31
|
import { base } from '@contraagent/elements/styles';
|
|
@@ -263,7 +265,7 @@ export class CaOverview extends LitElement {
|
|
|
263
265
|
${s.cursor
|
|
264
266
|
? html`<section class="panel enter">
|
|
265
267
|
<h3 title=${msg('Stage and step per unit (CursorStore)')}>${msg('Resume cursor')}</h3>
|
|
266
|
-
${this.cursor()}
|
|
268
|
+
${guard([s.cursor, getLocale()], () => this.cursor())}
|
|
267
269
|
</section>`
|
|
268
270
|
: nothing}
|
|
269
271
|
</div>
|
|
@@ -273,7 +273,9 @@ export class CaPipelineGraph extends LitElement {
|
|
|
273
273
|
this.prevOrder = this.orders.get(path.join('/'));
|
|
274
274
|
}
|
|
275
275
|
const locale = getLocale();
|
|
276
|
-
|
|
276
|
+
// The journal says only why a task did not start: with none, the words stay as they are.
|
|
277
|
+
const why = changed.has('journal') && this.plan.nodes.some((n) => n.state === 'blocked');
|
|
278
|
+
const words = structure || why || changed.has('live') || changed.has('forecast') || locale !== this.locale;
|
|
277
279
|
if (words || changed.has('sort')) this.build(words, locale);
|
|
278
280
|
}
|
|
279
281
|
|
|
@@ -470,19 +472,61 @@ export class CaPipelineGraph extends LitElement {
|
|
|
470
472
|
return this.controls && this.tree.length && w >= OUTLINE_WIDE ? w - OUTLINE_COLUMN : w;
|
|
471
473
|
}
|
|
472
474
|
|
|
473
|
-
/** The place keys on the way to the layer shown (the functions of its path). */
|
|
475
|
+
/** The place keys on the way to the layer shown (the functions of its path), for the layer and the places they were found for. */
|
|
476
|
+
private current: { segments: string[]; stats: Map<string, PlaceStats>; keys: Set<string> } | null = null;
|
|
474
477
|
private currentKeys(): Set<string> {
|
|
478
|
+
const c = this.current;
|
|
479
|
+
if (c && c.segments === this.segments && c.stats === this.flowStats) return c.keys;
|
|
475
480
|
const { path } = splitLayer(this.segments);
|
|
476
481
|
const out = new Set<string>();
|
|
477
482
|
for (const s of this.flowStats.values()) {
|
|
478
483
|
const d = s.layer.length;
|
|
479
484
|
if (d < path.length && startsWith(path, s.layer) && path[d] === s.name) out.add(s.key);
|
|
480
485
|
}
|
|
486
|
+
this.current = { segments: this.segments, stats: this.flowStats, keys: out };
|
|
481
487
|
return out;
|
|
482
488
|
}
|
|
483
489
|
|
|
490
|
+
/** The places the outline unfolds on its own (`expanded`), and what they were found from. */
|
|
491
|
+
private unfolded: { current: Set<string>; open: Set<string>; selected: string; want: Set<string>; holds: Map<OutlineItem, boolean> } | null = null;
|
|
492
|
+
private unfoldedKeys(): Set<string> {
|
|
493
|
+
const current = this.currentKeys();
|
|
494
|
+
const selected = this.selectedItem();
|
|
495
|
+
const u = this.unfolded;
|
|
496
|
+
if (u && u.current === current && u.open === this.shownOpen && u.selected === selected) return u.want;
|
|
497
|
+
const want = new Set([...current, ...this.shownOpen, selected]);
|
|
498
|
+
this.unfolded = { current, open: this.shownOpen, selected, want, holds: new Map() };
|
|
499
|
+
return want;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** The outline's items by id, the first in the tree's order, for the tree they are of. */
|
|
503
|
+
private items: { tree: OutlineItem[]; byId: Map<string, OutlineItem> } | null = null;
|
|
504
|
+
private itemById(id: string): OutlineItem | undefined {
|
|
505
|
+
if (this.items?.tree !== this.tree) {
|
|
506
|
+
const byId = new Map<string, OutlineItem>();
|
|
507
|
+
const walk = (xs: readonly OutlineItem[]): void => {
|
|
508
|
+
for (const it of xs) {
|
|
509
|
+
if (!byId.has(it.id)) byId.set(it.id, it);
|
|
510
|
+
walk(it.kids);
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
walk(this.tree);
|
|
514
|
+
this.items = { tree: this.tree, byId };
|
|
515
|
+
}
|
|
516
|
+
return this.items.byId.get(id);
|
|
517
|
+
}
|
|
518
|
+
|
|
484
519
|
/** The place (or loop item) the selection is, or is in: '' for none. */
|
|
520
|
+
private selectedOf: { sel: string; graph: LayerGraph; item: string } | null = null;
|
|
485
521
|
private selectedItem(): string {
|
|
522
|
+
const had = this.selectedOf;
|
|
523
|
+
if (had && had.sel === this.sel && had.graph === this.graph) return had.item;
|
|
524
|
+
const item = this.findSelectedItem();
|
|
525
|
+
this.selectedOf = { sel: this.sel, graph: this.graph, item };
|
|
526
|
+
return item;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private findSelectedItem(): string {
|
|
486
530
|
const sel = this.sel;
|
|
487
531
|
if (!sel) return '';
|
|
488
532
|
if (sel.startsWith(FLOW_BOX)) return sel.slice(FLOW_BOX.length);
|
|
@@ -527,9 +571,14 @@ export class CaPipelineGraph extends LitElement {
|
|
|
527
571
|
const own = el.folds.get(id);
|
|
528
572
|
if (own !== undefined) return own;
|
|
529
573
|
// Unfolded by default: the top items, and those on the way to the layer shown, to what is open and to the selection.
|
|
530
|
-
const want =
|
|
531
|
-
const
|
|
532
|
-
const
|
|
574
|
+
const want = el.unfoldedKeys();
|
|
575
|
+
const holds = el.unfolded?.holds ?? new Map<OutlineItem, boolean>();
|
|
576
|
+
const has = (it: OutlineItem): boolean => {
|
|
577
|
+
let h = holds.get(it);
|
|
578
|
+
if (h === undefined) holds.set(it, (h = want.has(it.id) || it.kids.some(has)));
|
|
579
|
+
return h;
|
|
580
|
+
};
|
|
581
|
+
const it = el.itemById(id);
|
|
533
582
|
return !!it && (el.tree.includes(it) || has(it));
|
|
534
583
|
},
|
|
535
584
|
expand(id, on) {
|
|
@@ -621,16 +670,6 @@ function lightsOf(plan: Plan, links: TaskLinks, g: LayerGraph): (key: string) =>
|
|
|
621
670
|
};
|
|
622
671
|
}
|
|
623
672
|
|
|
624
|
-
/** The outline item `id`, wherever it is in the tree. */
|
|
625
|
-
function findItem(items: readonly OutlineItem[], id: string): OutlineItem | undefined {
|
|
626
|
-
for (const it of items) {
|
|
627
|
-
if (it.id === id) return it;
|
|
628
|
-
const k = findItem(it.kids, id);
|
|
629
|
-
if (k) return k;
|
|
630
|
-
}
|
|
631
|
-
return undefined;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
673
|
declare global {
|
|
635
674
|
interface HTMLElementTagNameMap {
|
|
636
675
|
'ca-pipeline-graph': CaPipelineGraph;
|
|
@@ -87,7 +87,8 @@ function inspect(h: PipelineHost, s: Selected) {
|
|
|
87
87
|
</dl>
|
|
88
88
|
<div class="acts">${link(msg(str`Inside ${shortName(m.flow)}`), () => primary(h, s), true)}${clear}</div>`;
|
|
89
89
|
}
|
|
90
|
-
const
|
|
90
|
+
const keys = new Set(m.keys);
|
|
91
|
+
const rows = h.journal.filter((r) => keys.has(r.key));
|
|
91
92
|
const ran = rows.filter(isLaunched);
|
|
92
93
|
const costs = ran.map((r) => r.cost_usd).filter((x): x is number => typeof x === 'number');
|
|
93
94
|
const wall = ran.reduce((a, r) => a + (r.wall_s ?? 0), 0);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { LayeredGraphStrings } from '@contraagent/elements/graph';
|
|
3
3
|
import type { MarkMatrixStrings } from '@contraagent/elements/matrix';
|
|
4
4
|
import { msg, str } from '@lit/localize';
|
|
5
|
+
import { getLocale } from '../../../core/i18n.js';
|
|
5
6
|
import { listText } from '../../../words/format.js';
|
|
6
7
|
import { lineageWord } from '../../../words/lineage.js';
|
|
7
8
|
|
|
@@ -11,6 +12,17 @@ import { lineageWord } from '../../../words/lineage.js';
|
|
|
11
12
|
* node always has its `progress`, so the words of a node's default count are not needed.
|
|
12
13
|
*/
|
|
13
14
|
export function graphStrings(up: boolean, functions: boolean): Omit<LayeredGraphStrings, 'progress'> {
|
|
15
|
+
// The same words are the same object: the graph does not draw its nodes again for them.
|
|
16
|
+
const id = `${getLocale()}|${up}|${functions}`;
|
|
17
|
+
let words = made.get(id);
|
|
18
|
+
if (!words) made.set(id, (words = makeGraphStrings(up, functions)));
|
|
19
|
+
return words;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const made = new Map<string, Omit<LayeredGraphStrings, 'progress'>>();
|
|
23
|
+
let madeMatrix: { locale: string; words: MarkMatrixStrings } | null = null;
|
|
24
|
+
|
|
25
|
+
function makeGraphStrings(up: boolean, functions: boolean): Omit<LayeredGraphStrings, 'progress'> {
|
|
14
26
|
const enter = msg('Enter: open');
|
|
15
27
|
return {
|
|
16
28
|
graph: msg('Graph', { id: 'view-graph', desc: 'Button: show the pipeline as a graph' }),
|
|
@@ -52,5 +64,7 @@ export function graphStrings(up: boolean, functions: boolean): Omit<LayeredGraph
|
|
|
52
64
|
|
|
53
65
|
/** The words of a loop's passes. */
|
|
54
66
|
export function matrixStrings(): MarkMatrixStrings {
|
|
55
|
-
|
|
67
|
+
const locale = getLocale();
|
|
68
|
+
if (madeMatrix?.locale !== locale) madeMatrix = { locale, words: { sortBy: msg('Sort units'), rowsHeader: msg('Unit') } };
|
|
69
|
+
return madeMatrix.words;
|
|
56
70
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The one time format of rows and chips (`localTime`), made once per language.
|
|
2
|
+
|
|
3
|
+
const formats = new Map<string, Intl.DateTimeFormat>();
|
|
4
|
+
|
|
5
|
+
/** The time of day `10:38:40`, or with the date `Oct 12, 10:38:40`, as `locale` writes it. */
|
|
6
|
+
export function clockFormat(locale: string, withDate: boolean): Intl.DateTimeFormat {
|
|
7
|
+
const id = `${locale}|${withDate}`;
|
|
8
|
+
let f = formats.get(id);
|
|
9
|
+
if (!f) {
|
|
10
|
+
f = new Intl.DateTimeFormat(locale, {
|
|
11
|
+
...(withDate ? { month: 'short', day: 'numeric' } : {}),
|
|
12
|
+
hour: '2-digit',
|
|
13
|
+
minute: '2-digit',
|
|
14
|
+
second: '2-digit',
|
|
15
|
+
hourCycle: 'h23',
|
|
16
|
+
});
|
|
17
|
+
formats.set(id, f);
|
|
18
|
+
}
|
|
19
|
+
return f;
|
|
20
|
+
}
|
package/src/words/format.ts
CHANGED
|
@@ -1,27 +1,11 @@
|
|
|
1
1
|
// Durations, times and small text helpers shared by the views.
|
|
2
2
|
import { msg, str } from '@lit/localize';
|
|
3
3
|
import { getLocale } from '../core/i18n.js';
|
|
4
|
+
import { clockFormat } from './clock-format.js';
|
|
4
5
|
|
|
5
6
|
// The formats that read the same in every language are the generic package's.
|
|
6
7
|
export { esc, fmtDur } from '@contraagent/elements';
|
|
7
8
|
|
|
8
|
-
const formats = new Map<string, Intl.DateTimeFormat>();
|
|
9
|
-
function formatter(locale: string, withDate: boolean): Intl.DateTimeFormat {
|
|
10
|
-
const id = `${locale}|${withDate}`;
|
|
11
|
-
let f = formats.get(id);
|
|
12
|
-
if (!f) {
|
|
13
|
-
f = new Intl.DateTimeFormat(locale, {
|
|
14
|
-
...(withDate ? { month: 'short', day: 'numeric' } : {}),
|
|
15
|
-
hour: '2-digit',
|
|
16
|
-
minute: '2-digit',
|
|
17
|
-
second: '2-digit',
|
|
18
|
-
hourCycle: 'h23',
|
|
19
|
-
});
|
|
20
|
-
formats.set(id, f);
|
|
21
|
-
}
|
|
22
|
-
return f;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
9
|
/**
|
|
26
10
|
* An ISO time in the reader's time zone, as the UI locale writes it: `10:38:40`,
|
|
27
11
|
* or with the date `Oct 12, 10:38:40` (the month as a word, so no reader takes
|
|
@@ -31,7 +15,7 @@ export function localTime(ts: string | null | undefined, withDate = false, local
|
|
|
31
15
|
if (!ts) return '';
|
|
32
16
|
const d = new Date(ts);
|
|
33
17
|
if (Number.isNaN(d.getTime())) return ts;
|
|
34
|
-
return
|
|
18
|
+
return clockFormat(locale, withDate).format(d);
|
|
35
19
|
}
|
|
36
20
|
|
|
37
21
|
/** An ISO time as `just now`, `5m ago`, `3h ago`, `2d ago` (the run picker only; rows and chips show `localTime`). */
|