portable-agent-layer 0.72.0 → 0.74.0

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.
Files changed (69) hide show
  1. package/package.json +10 -7
  2. package/src/cli/index.ts +21 -2
  3. package/src/cli/server.ts +33 -0
  4. package/src/hooks/handlers/agenda.ts +195 -7
  5. package/src/hooks/handlers/update-check.ts +1 -1
  6. package/src/hooks/lib/agenda-store.ts +2 -0
  7. package/src/hooks/lib/goal-links.ts +83 -0
  8. package/src/hooks/lib/models.ts +1 -1
  9. package/src/hooks/lib/projects.ts +5 -0
  10. package/src/hooks/lib/settings.ts +2 -0
  11. package/src/hooks/lib/telos-goals.ts +8 -2
  12. package/src/hooks/lib/token-usage.ts +2 -0
  13. package/src/tools/agent/project.ts +22 -20
  14. package/src/tools/control-room/attention.ts +146 -0
  15. package/src/tools/control-room/data.ts +76 -5
  16. package/src/tools/control-room/detail.ts +158 -0
  17. package/src/tools/control-room/matrix.ts +91 -19
  18. package/src/tools/control-room/prefs.ts +96 -0
  19. package/src/tools/control-room/server-config.ts +8 -0
  20. package/src/tools/control-room/server.ts +196 -11
  21. package/src/tools/control-room/snooze.ts +65 -0
  22. package/src/tools/control-room/static.ts +68 -0
  23. package/src/tools/control-room/ui/app.tsx +196 -50
  24. package/src/tools/control-room/ui/attention-bell.tsx +118 -0
  25. package/src/tools/control-room/ui/components/README.md +18 -0
  26. package/src/tools/control-room/ui/components/badge.tsx +38 -0
  27. package/src/tools/control-room/ui/components/button.tsx +43 -0
  28. package/src/tools/control-room/ui/components/card.tsx +45 -0
  29. package/src/tools/control-room/ui/components/input.tsx +24 -0
  30. package/src/tools/control-room/ui/components/label.tsx +18 -0
  31. package/src/tools/control-room/ui/components/popover.tsx +37 -0
  32. package/src/tools/control-room/ui/components/separator.tsx +25 -0
  33. package/src/tools/control-room/ui/components/skeleton.tsx +14 -0
  34. package/src/tools/control-room/ui/components/switch.tsx +27 -0
  35. package/src/tools/control-room/ui/components/table.tsx +60 -0
  36. package/src/tools/control-room/ui/components/toggle-group.tsx +38 -0
  37. package/src/tools/control-room/ui/dist/assets/index-BoQjFpzV.js +49 -0
  38. package/src/tools/control-room/ui/dist/assets/index-Dncp2bYg.css +2 -0
  39. package/src/tools/control-room/ui/dist/index.html +19 -0
  40. package/src/tools/control-room/ui/format.ts +0 -12
  41. package/src/tools/control-room/ui/frame.tsx +125 -0
  42. package/src/tools/control-room/ui/index.html +2 -2
  43. package/src/tools/control-room/ui/lib/api.ts +27 -0
  44. package/src/tools/control-room/ui/lib/cn.ts +6 -0
  45. package/src/tools/control-room/ui/lib/write.ts +43 -0
  46. package/src/tools/control-room/ui/package.json +28 -0
  47. package/src/tools/control-room/ui/parts.tsx +235 -0
  48. package/src/tools/control-room/ui/screens/log.tsx +274 -0
  49. package/src/tools/control-room/ui/screens/project-detail.tsx +302 -0
  50. package/src/tools/control-room/ui/screens/projects.tsx +128 -0
  51. package/src/tools/control-room/ui/screens/settings.tsx +205 -0
  52. package/src/tools/control-room/ui/screens/today.tsx +391 -0
  53. package/src/tools/control-room/ui/theme.css +127 -0
  54. package/src/tools/control-room/ui/vite.config.ts +36 -0
  55. package/src/tools/control-room/writes.ts +106 -0
  56. package/src/tools/ledger/outcomes.ts +9 -0
  57. package/src/tools/ledger/query.ts +2 -0
  58. package/src/tools/ledger/view.ts +31 -10
  59. package/src/tools/lib/handoff-note.ts +14 -0
  60. package/src/tools/lib/project-isc.ts +61 -0
  61. package/src/tools/control-room/ui/agenda.tsx +0 -43
  62. package/src/tools/control-room/ui/agents.tsx +0 -67
  63. package/src/tools/control-room/ui/app.css +0 -857
  64. package/src/tools/control-room/ui/board.tsx +0 -82
  65. package/src/tools/control-room/ui/handoffs.tsx +0 -37
  66. package/src/tools/control-room/ui/ledger.tsx +0 -137
  67. package/src/tools/control-room/ui/matrix.tsx +0 -117
  68. package/src/tools/control-room/ui/panel.tsx +0 -60
  69. package/src/tools/control-room/ui/signal.tsx +0 -161
@@ -1,82 +0,0 @@
1
- import type { ProjectCard } from "../data";
2
- import { age, plural } from "./format";
3
- import { Panel, Pending, useLoaded } from "./panel";
4
-
5
- function reasonTone(reason: string): string {
6
- if (reason.startsWith("handoff")) return "amber";
7
- if (reason.endsWith("blocker") || reason.endsWith("blockers")) return "bad";
8
- return "ghost";
9
- }
10
-
11
- function projectTone(p: ProjectCard): string {
12
- if (p.asking.length > 0) return "asking";
13
- if (p.stale) return "quiet";
14
- return "";
15
- }
16
-
17
- function Project({ p }: { p: ProjectCard }) {
18
- return (
19
- <div className={`project ${projectTone(p)}`}>
20
- <div className="name" title={p.path ?? "not checked out here"}>
21
- {p.slug}
22
- <small>
23
- {p.status} · {age(p.ageDays)}
24
- {p.path ? "" : " · elsewhere"}
25
- </small>
26
- </div>
27
- <div className="reasons">
28
- {p.asking.map((r) => (
29
- <span key={r} className={`tag ${reasonTone(r)}`}>
30
- {r}
31
- </span>
32
- ))}
33
- {p.blockers.map((b) => (
34
- <span key={b} className="tag bad" title={b}>
35
- ⛔ {b.length > 48 ? `${b.slice(0, 48)}…` : b}
36
- </span>
37
- ))}
38
- </div>
39
- <div className="counts">
40
- <b>{p.openIscs}</b> open · <b>{p.sessions30d}</b> sessions/30d
41
- {p.lastSession && (
42
- <>
43
- {" "}
44
- · last <b>{p.lastSession.date}</b>
45
- </>
46
- )}
47
- </div>
48
- {p.next[0] && <div className="next">{p.next[0]}</div>}
49
- </div>
50
- );
51
- }
52
-
53
- export function Board() {
54
- const cards = useLoaded<ProjectCard[]>("/api/board");
55
- const asking =
56
- cards.state === "ready" ? cards.data.filter((c) => c.asking.length > 0) : [];
57
- return (
58
- <Panel
59
- index="01 · projects"
60
- title="Asking for you"
61
- span={8}
62
- order={0}
63
- aside={
64
- cards.state === "ready"
65
- ? `${plural(asking.length, "project")} of ${cards.data.length}`
66
- : ""
67
- }
68
- >
69
- <Pending value={cards} />
70
- {cards.state === "ready" && cards.data.length === 0 && (
71
- <div className="empty">No project is registered yet.</div>
72
- )}
73
- {cards.state === "ready" && (
74
- <div className="board">
75
- {cards.data.map((p) => (
76
- <Project key={p.slug} p={p} />
77
- ))}
78
- </div>
79
- )}
80
- </Panel>
81
- );
82
- }
@@ -1,37 +0,0 @@
1
- import type { HandoffCard } from "../data";
2
- import { age } from "./format";
3
- import { Panel, Pending, useLoaded } from "./panel";
4
-
5
- function titleRepeatsSentence(h: HandoffCard): boolean {
6
- return h.title.startsWith(h.sentence.slice(0, 24));
7
- }
8
-
9
- function Handoff({ h }: { h: HandoffCard }) {
10
- return (
11
- <div className="handoff">
12
- <div className="where">
13
- <span>
14
- <b title={h.cwd}>{h.label}</b> · {h.source}
15
- </span>
16
- <span>{age(h.ageDays)}</span>
17
- </div>
18
- <div className="sentence" title={h.handoff}>
19
- {h.sentence}
20
- </div>
21
- {!titleRepeatsSentence(h) && <div className="title">{h.title}</div>}
22
- </div>
23
- );
24
- }
25
-
26
- export function Handoffs() {
27
- const cards = useLoaded<HandoffCard[]>("/api/handoffs");
28
- return (
29
- <Panel index="03 · unfinished" title="Where you left off" span={5} order={2}>
30
- <Pending value={cards} />
31
- {cards.state === "ready" && cards.data.length === 0 && (
32
- <div className="empty">Nothing in progress. Every handoff is closed.</div>
33
- )}
34
- {cards.state === "ready" && cards.data.map((h) => <Handoff key={h.cwd} h={h} />)}
35
- </Panel>
36
- );
37
- }
@@ -1,137 +0,0 @@
1
- import { useState } from "react";
2
- import type { LedgerView, LedgerViewRow, PageOutcome } from "../../ledger/view";
3
- import { PAGE_OUTCOMES } from "../../ledger/view";
4
- import { clock, isoDay } from "./format";
5
- import { Panel, Pending, useLoaded } from "./panel";
6
-
7
- const DEFAULT_WINDOW_DAYS = 14;
8
- const MAX_ROWS = 200;
9
-
10
- function ledgerQuery(project: string, since: string, until: string): string {
11
- const params = new URLSearchParams();
12
- if (project) params.set("project", project);
13
- if (since) params.set("since", since);
14
- if (until) params.set("until", until);
15
- const query = params.toString();
16
- return query ? `/api/ledger?${query}` : "/api/ledger";
17
- }
18
-
19
- function Row({ r }: { r: LedgerViewRow }) {
20
- const [slug, ...rest] = r.target.split(" ");
21
- return (
22
- <tr>
23
- <td className="when">{clock(r.ts)}</td>
24
- <td>
25
- {r.actor}
26
- <span className="reason">
27
- {r.authority} · {r.runtime}
28
- </span>
29
- </td>
30
- <td>{r.tool}</td>
31
- <td className="target" title={r.command ?? r.target}>
32
- <b>{slug}</b> {rest.join(" ")}
33
- {r.command && <span className="reason">{r.command}</span>}
34
- </td>
35
- <td>{r.change}</td>
36
- <td>
37
- <span className={`outcome ${r.outcome}`}>{r.outcome}</span>
38
- {r.reason && <span className="reason">{r.reason}</span>}
39
- </td>
40
- </tr>
41
- );
42
- }
43
-
44
- function Strip({ view }: { view: LedgerView }) {
45
- return (
46
- <div className="strip">
47
- <span>
48
- <b>{view.stats.total}</b>actions
49
- </span>
50
- <span className="refusals">
51
- <b>{view.stats.refusals}</b>refusals
52
- </span>
53
- {PAGE_OUTCOMES.map((o: PageOutcome) => (
54
- <span key={o}>
55
- <b>{view.stats.outcomes[o].total}</b>
56
- {o}
57
- </span>
58
- ))}
59
- </div>
60
- );
61
- }
62
-
63
- export function Ledger() {
64
- const [project, setProject] = useState("");
65
- const [since, setSince] = useState(
66
- isoDay(new Date(Date.now() - DEFAULT_WINDOW_DAYS * 86_400_000))
67
- );
68
- const [until, setUntil] = useState("");
69
- const projects = useLoaded<{ slug: string }[]>("/api/projects");
70
- const view = useLoaded<LedgerView>(ledgerQuery(project, since, until));
71
- return (
72
- <Panel
73
- index="05 · ledger"
74
- title="What the agents did"
75
- span={12}
76
- order={4}
77
- aside={
78
- <span className="filters">
79
- <label>
80
- project
81
- <select value={project} onChange={(e) => setProject(e.target.value)}>
82
- <option value="">all</option>
83
- {projects.state === "ready" &&
84
- projects.data.map((p) => (
85
- <option key={p.slug} value={p.slug}>
86
- {p.slug}
87
- </option>
88
- ))}
89
- </select>
90
- </label>
91
- <label>
92
- from
93
- <input type="date" value={since} onChange={(e) => setSince(e.target.value)} />
94
- </label>
95
- <label>
96
- to
97
- <input type="date" value={until} onChange={(e) => setUntil(e.target.value)} />
98
- </label>
99
- </span>
100
- }
101
- >
102
- <Pending value={view} />
103
- {view.state === "ready" && (
104
- <>
105
- <Strip view={view.data} />
106
- {view.data.rows.length === 0 ? (
107
- <div className="empty">No actions in this window.</div>
108
- ) : (
109
- <table className="ledger">
110
- <thead>
111
- <tr>
112
- <th>when</th>
113
- <th>actor</th>
114
- <th>action</th>
115
- <th>target</th>
116
- <th>change</th>
117
- <th>outcome</th>
118
- </tr>
119
- </thead>
120
- <tbody>
121
- {view.data.rows.slice(0, MAX_ROWS).map((r) => (
122
- <Row key={r.id} r={r} />
123
- ))}
124
- </tbody>
125
- </table>
126
- )}
127
- {view.data.rows.length > MAX_ROWS && (
128
- <div className="empty">
129
- Newest {MAX_ROWS} of {view.data.rows.length}. Narrow the window for the
130
- rest.
131
- </div>
132
- )}
133
- </>
134
- )}
135
- </Panel>
136
- );
137
- }
@@ -1,117 +0,0 @@
1
- import { useState } from "react";
2
- import type { Matrix, MatrixItem } from "../matrix";
3
- import { Pending, useLoaded } from "./panel";
4
-
5
- const QUADRANTS: { key: keyof Matrix; title: string; note: string }[] = [
6
- { key: "now", title: "Do now", note: "matters and cannot wait" },
7
- { key: "plan", title: "Give it a slot", note: "matters, nothing forcing it" },
8
- { key: "noise", title: "Loud, not load-bearing", note: "pressing but serves nothing" },
9
- { key: "later", title: "Let it sit", note: "neither" },
10
- ];
11
-
12
- const KINDS = ["goal", "revenue", "fun"] as const;
13
-
14
- async function saveServes(project: string, serves: string): Promise<void> {
15
- const res = await fetch("/api/serves", {
16
- method: "POST",
17
- headers: { "content-type": "application/json" },
18
- body: JSON.stringify({ project, serves }),
19
- });
20
- if (!res.ok) throw new Error(`override answered ${res.status}`);
21
- }
22
-
23
- function Serves({ item, onSaved }: { item: MatrixItem; onSaved: () => void }) {
24
- if (item.kind !== "project") return null;
25
- return (
26
- <select
27
- className={item.servesBy === "user" ? "serves mine" : "serves"}
28
- value={item.serves ?? ""}
29
- title={
30
- item.servesBy === "user" ? "your answer" : "PAL's guess — change it to correct it"
31
- }
32
- onChange={(e) => {
33
- saveServes(item.id, e.target.value)
34
- .then(onSaved)
35
- .catch(() => onSaved());
36
- }}
37
- >
38
- <option value="" disabled>
39
- unranked
40
- </option>
41
- {KINDS.map((kind) => (
42
- <option key={kind} value={kind}>
43
- {kind}
44
- </option>
45
- ))}
46
- </select>
47
- );
48
- }
49
-
50
- function Item({ item, onSaved }: { item: MatrixItem; onSaved: () => void }) {
51
- return (
52
- <li className={`m-item ${item.kind}`}>
53
- <div className="m-head">
54
- <span className="m-label">{item.label}</span>
55
- <Serves item={item} onSaved={onSaved} />
56
- </div>
57
- {item.waitingOn && <div className="waiting">waiting on you: {item.waitingOn}</div>}
58
- {item.detail && <div className="m-detail">{item.detail}</div>}
59
- <div className="m-reasons">
60
- <span className="why-important">{item.importantBecause}</span>
61
- {item.urgentBecause.map((reason) => (
62
- <span className="why-urgent" key={reason}>
63
- {reason}
64
- </span>
65
- ))}
66
- </div>
67
- </li>
68
- );
69
- }
70
-
71
- export function MatrixGrid() {
72
- const [nonce, setNonce] = useState(0);
73
- const loaded = useLoaded<Matrix>(`/api/matrix?v=${nonce}`);
74
- const reload = () => setNonce((n) => n + 1);
75
-
76
- return (
77
- <section className="matrix">
78
- <header>
79
- <h2>What is worth your morning</h2>
80
- {loaded.state === "ready" && loaded.data.unranked > 0 && (
81
- <span className="agenda-age stale">
82
- {loaded.data.unranked} project(s) with no purpose on record
83
- </span>
84
- )}
85
- </header>
86
- <Pending value={loaded} />
87
- {loaded.state === "ready" && (
88
- <div className="quadrants">
89
- {QUADRANTS.map(({ key, title, note }) => {
90
- const items = loaded.data[key] as MatrixItem[];
91
- return (
92
- <div className={`quadrant q-${key}`} key={key}>
93
- <div className="q-head">
94
- <h3>{title}</h3>
95
- <span>{note}</span>
96
- </div>
97
- {items.length === 0 ? (
98
- <div className="empty">nothing here</div>
99
- ) : (
100
- <ul>
101
- {items.map((item) => (
102
- <Item
103
- key={`${item.kind}:${item.id}`}
104
- item={item}
105
- onSaved={reload}
106
- />
107
- ))}
108
- </ul>
109
- )}
110
- </div>
111
- );
112
- })}
113
- </div>
114
- )}
115
- </section>
116
- );
117
- }
@@ -1,60 +0,0 @@
1
- import { type ReactNode, useEffect, useState } from "react";
2
-
3
- interface PanelProps {
4
- index: string;
5
- title: string;
6
- span: number;
7
- order: number;
8
- aside?: ReactNode;
9
- children: ReactNode;
10
- }
11
-
12
- export function Panel({ index, title, span, order, aside, children }: PanelProps) {
13
- return (
14
- <section
15
- className="panel"
16
- style={{ "--span": span, "--i": order } as React.CSSProperties}
17
- >
18
- <header>
19
- <div>
20
- <span className="index">{index}</span>
21
- <h2>{title}</h2>
22
- </div>
23
- {aside && <span className="aside">{aside}</span>}
24
- </header>
25
- {children}
26
- </section>
27
- );
28
- }
29
-
30
- export type Loaded<T> =
31
- | { state: "loading" }
32
- | { state: "error"; message: string }
33
- | { state: "ready"; data: T };
34
-
35
- export async function getJson<T>(path: string): Promise<T> {
36
- const res = await fetch(path);
37
- if (!res.ok) throw new Error(`${path} answered ${res.status}`);
38
- return (await res.json()) as T;
39
- }
40
-
41
- export function useLoaded<T>(url: string): Loaded<T> {
42
- const [value, setValue] = useState<Loaded<T>>({ state: "loading" });
43
- useEffect(() => {
44
- let live = true;
45
- setValue({ state: "loading" });
46
- getJson<T>(url)
47
- .then((data) => live && setValue({ state: "ready", data }))
48
- .catch((e: Error) => live && setValue({ state: "error", message: e.message }));
49
- return () => {
50
- live = false;
51
- };
52
- }, [url]);
53
- return value;
54
- }
55
-
56
- export function Pending({ value }: { value: Loaded<unknown> }) {
57
- if (value.state === "loading") return <div className="empty">reading…</div>;
58
- if (value.state === "error") return <div className="error">{value.message}</div>;
59
- return null;
60
- }
@@ -1,161 +0,0 @@
1
- import type { DueBadge, RatingPoint, SignalView } from "../data";
2
- import { percent, tenths } from "./format";
3
- import { Panel, Pending, useLoaded } from "./panel";
4
-
5
- const LOW_RATING = 3;
6
- const W = 320;
7
- const H = 72;
8
- const PAD = 4;
9
-
10
- function sparkPath(points: RatingPoint[]): {
11
- line: string;
12
- area: string;
13
- xy: [number, number][];
14
- } {
15
- if (points.length === 0) return { line: "", area: "", xy: [] };
16
- const step = points.length > 1 ? (W - PAD * 2) / (points.length - 1) : 0;
17
- const xy = points.map<[number, number]>((p, i) => [
18
- PAD + i * step,
19
- H - PAD - ((p.rating - 1) / 9) * (H - PAD * 2),
20
- ]);
21
- const line = xy
22
- .map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)} ${y.toFixed(1)}`)
23
- .join(" ");
24
- const area = `${line} L${xy.at(-1)?.[0].toFixed(1)} ${H} L${xy[0][0].toFixed(1)} ${H} Z`;
25
- return { line, area, xy };
26
- }
27
-
28
- function Sparkline({ points }: { points: RatingPoint[] }) {
29
- const { line, area, xy } = sparkPath(points);
30
- const last = xy.at(-1);
31
- const midY = H - PAD - (4 / 9) * (H - PAD * 2);
32
- return (
33
- <svg
34
- className="spark"
35
- viewBox={`0 0 ${W} ${H}`}
36
- preserveAspectRatio="none"
37
- role="img"
38
- aria-label="ratings"
39
- >
40
- <title>ratings, oldest to newest</title>
41
- <defs>
42
- <linearGradient id="sparkfill" x1="0" x2="0" y1="0" y2="1">
43
- <stop offset="0" stopColor="#f0b14a" stopOpacity="0.28" />
44
- <stop offset="1" stopColor="#f0b14a" stopOpacity="0" />
45
- </linearGradient>
46
- </defs>
47
- <line className="rule" x1={PAD} x2={W - PAD} y1={midY} y2={midY} />
48
- <path className="area" d={area} />
49
- <path className="line" d={line} />
50
- {points.map((p, i) =>
51
- p.rating <= LOW_RATING ? (
52
- <circle key={p.ts} className="low" cx={xy[i][0]} cy={xy[i][1]} r="2" />
53
- ) : null
54
- )}
55
- {last && <circle className="last" cx={last[0]} cy={last[1]} r="3" />}
56
- </svg>
57
- );
58
- }
59
-
60
- function Figure({ value, label, tone }: { value: string; label: string; tone?: string }) {
61
- return (
62
- <div className="figure">
63
- <div className={`value ${tone ?? ""}`}>{value}</div>
64
- <div className="label">{label}</div>
65
- </div>
66
- );
67
- }
68
-
69
- const BADGE_LOOK: Record<DueBadge["state"], { row: string; tag: string }> = {
70
- due: { row: "due", tag: "amber" },
71
- clear: { row: "clear", tag: "good" },
72
- "n/a": { row: "na", tag: "ghost" },
73
- };
74
-
75
- function Due({ name, badge }: { name: string; badge: DueBadge }) {
76
- const look = BADGE_LOOK[badge.state];
77
- return (
78
- <div className={`due-row ${look.row}`}>
79
- <span className={`tag ${look.tag}`}>{badge.state}</span>
80
- <span className="detail">
81
- {name}
82
- {badge.detail ? ` — ${badge.detail}` : ""}
83
- </span>
84
- </div>
85
- );
86
- }
87
-
88
- function ratingTone(avg: number): string {
89
- if (avg < 5) return "bad";
90
- if (avg >= 7) return "good";
91
- return "";
92
- }
93
-
94
- export function Signal() {
95
- const view = useLoaded<SignalView>("/api/signal");
96
- return (
97
- <Panel
98
- index="02 · feedback"
99
- title="Signal"
100
- span={4}
101
- order={1}
102
- aside={
103
- view.state === "ready" && view.data.synthesizedAt
104
- ? `synthesised ${view.data.synthesizedAt.slice(0, 10)}`
105
- : ""
106
- }
107
- >
108
- <Pending value={view} />
109
- {view.state === "ready" && (
110
- <>
111
- <div className="figures">
112
- <Figure
113
- value={view.data.ratings ? tenths(view.data.ratings.recentAvg) : "–"}
114
- label="last 10"
115
- tone={view.data.ratings ? ratingTone(view.data.ratings.recentAvg) : ""}
116
- />
117
- <Figure
118
- value={view.data.ratings ? tenths(view.data.ratings.avg) : "–"}
119
- label={`avg of ${view.data.ratings?.count ?? 0}`}
120
- />
121
- <Figure
122
- value={view.data.ratings ? String(view.data.ratings.lowCount) : "–"}
123
- label="low (≤3)"
124
- tone={view.data.ratings && view.data.ratings.lowCount > 5 ? "bad" : ""}
125
- />
126
- </div>
127
- {view.data.series.length > 0 ? (
128
- <Sparkline points={view.data.series} />
129
- ) : (
130
- <div className="empty">No ratings yet.</div>
131
- )}
132
- <div className="spark-caption">
133
- <span>last {view.data.series.length} ratings</span>
134
- <span>{view.data.ratings?.trend ?? ""}</span>
135
- </div>
136
- <div className="figures" style={{ marginTop: 16 }}>
137
- <Figure
138
- value={
139
- view.data.algorithm ? String(view.data.algorithm.reflectionCount) : "–"
140
- }
141
- label="reflections"
142
- />
143
- <Figure
144
- value={view.data.algorithm ? percent(view.data.algorithm.passRate) : "–"}
145
- label="criteria pass"
146
- />
147
- <Figure
148
- value={view.data.algorithm ? tenths(view.data.algorithm.avgSentiment) : "–"}
149
- label="sentiment"
150
- />
151
- </div>
152
- <div className="due">
153
- <Due name="learning analysis" badge={view.data.due.analysis} />
154
- <Due name="algorithm review" badge={view.data.due.algorithmReview} />
155
- <Due name="relationship reflect" badge={view.data.due.relationshipReflect} />
156
- </div>
157
- </>
158
- )}
159
- </Panel>
160
- );
161
- }