kalfa 3.0.0.dev0__py3-none-any.whl

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 (148) hide show
  1. kalfa/__init__.py +19 -0
  2. kalfa/api.py +284 -0
  3. kalfa/board/__init__.py +1 -0
  4. kalfa/board/answers.py +419 -0
  5. kalfa/board/server.py +536 -0
  6. kalfa/board/static/api.js +176 -0
  7. kalfa/board/static/app.js +122 -0
  8. kalfa/board/static/board.css +228 -0
  9. kalfa/board/static/charts.css +46 -0
  10. kalfa/board/static/components/badge.js +14 -0
  11. kalfa/board/static/components/chart.js +204 -0
  12. kalfa/board/static/components/diagram.js +857 -0
  13. kalfa/board/static/components/error.js +17 -0
  14. kalfa/board/static/components/loading.js +7 -0
  15. kalfa/board/static/components/progress.js +25 -0
  16. kalfa/board/static/components/settings.js +418 -0
  17. kalfa/board/static/components/spark.js +43 -0
  18. kalfa/board/static/components/table.js +156 -0
  19. kalfa/board/static/diagram.css +76 -0
  20. kalfa/board/static/format.js +138 -0
  21. kalfa/board/static/index.html +34 -0
  22. kalfa/board/static/plot.js +594 -0
  23. kalfa/board/static/predictions.css +20 -0
  24. kalfa/board/static/store.js +215 -0
  25. kalfa/board/static/views/compare.js +185 -0
  26. kalfa/board/static/views/home.js +90 -0
  27. kalfa/board/static/views/record/config.js +81 -0
  28. kalfa/board/static/views/record/curves.js +137 -0
  29. kalfa/board/static/views/record/data.js +378 -0
  30. kalfa/board/static/views/record/files.js +128 -0
  31. kalfa/board/static/views/record/log.js +164 -0
  32. kalfa/board/static/views/record/model.js +286 -0
  33. kalfa/board/static/views/record/overview.js +216 -0
  34. kalfa/board/static/views/record/plots.js +103 -0
  35. kalfa/board/static/views/record/predictions.js +834 -0
  36. kalfa/board/static/views/record.js +179 -0
  37. kalfa/board/static/views/runs.js +124 -0
  38. kalfa/board/static/views/sweep.js +282 -0
  39. kalfa/board/watch.py +87 -0
  40. kalfa/cli.py +371 -0
  41. kalfa/commands/__init__.py +0 -0
  42. kalfa/commands/common.py +8 -0
  43. kalfa/commands/export.py +117 -0
  44. kalfa/commands/generate.py +31 -0
  45. kalfa/commands/guide/__init__.py +101 -0
  46. kalfa/commands/guide/about.py +93 -0
  47. kalfa/commands/guide/calls.py +102 -0
  48. kalfa/commands/guide/sections.py +186 -0
  49. kalfa/commands/guide/seen.py +307 -0
  50. kalfa/commands/guide/show.py +106 -0
  51. kalfa/commands/guide/training.py +333 -0
  52. kalfa/commands/guide/words.py +97 -0
  53. kalfa/commands/legos.py +508 -0
  54. kalfa/commands/plots.py +27 -0
  55. kalfa/commands/report/__init__.py +64 -0
  56. kalfa/commands/report/page.py +112 -0
  57. kalfa/commands/report/predictions.py +216 -0
  58. kalfa/commands/report/sections.py +356 -0
  59. kalfa/commands/stop.py +60 -0
  60. kalfa/commands/sweep/__init__.py +202 -0
  61. kalfa/commands/sweep/checks.py +98 -0
  62. kalfa/commands/sweep/plan.py +183 -0
  63. kalfa/commands/sweep/points.py +123 -0
  64. kalfa/commands/sweep/runner.py +100 -0
  65. kalfa/commands/sweep/space.py +67 -0
  66. kalfa/commands/sweep/strategies.py +228 -0
  67. kalfa/core/__init__.py +0 -0
  68. kalfa/core/aliases.py +229 -0
  69. kalfa/core/build.py +727 -0
  70. kalfa/core/check.py +550 -0
  71. kalfa/core/coerce.py +36 -0
  72. kalfa/core/context.py +149 -0
  73. kalfa/core/device.py +63 -0
  74. kalfa/core/errors.py +45 -0
  75. kalfa/core/graph.py +304 -0
  76. kalfa/core/history.py +70 -0
  77. kalfa/core/loop.py +710 -0
  78. kalfa/core/make.py +121 -0
  79. kalfa/core/plan.py +156 -0
  80. kalfa/core/predict.py +290 -0
  81. kalfa/core/protocols.py +122 -0
  82. kalfa/core/record.py +347 -0
  83. kalfa/core/refs.py +88 -0
  84. kalfa/core/rng.py +100 -0
  85. kalfa/core/rules.py +513 -0
  86. kalfa/core/run.py +568 -0
  87. kalfa/core/spec.py +413 -0
  88. kalfa/core/state.py +100 -0
  89. kalfa/core/wrap.py +671 -0
  90. kalfa/records/__init__.py +24 -0
  91. kalfa/records/compare.py +86 -0
  92. kalfa/records/reader.py +561 -0
  93. kalfa/records/stats/__init__.py +36 -0
  94. kalfa/records/stats/choices.py +282 -0
  95. kalfa/records/stats/classes.py +236 -0
  96. kalfa/records/stats/measures.py +144 -0
  97. kalfa/records/stats/pairs.py +225 -0
  98. kalfa/records/stats/tables.py +153 -0
  99. kalfa/records/sweep.py +244 -0
  100. kalfa/std/__init__.py +17 -0
  101. kalfa/std/actions.py +35 -0
  102. kalfa/std/common.py +56 -0
  103. kalfa/std/data/__init__.py +1 -0
  104. kalfa/std/data/base.py +45 -0
  105. kalfa/std/data/columns.py +190 -0
  106. kalfa/std/data/flow.py +317 -0
  107. kalfa/std/data/images.py +443 -0
  108. kalfa/std/data/options.py +147 -0
  109. kalfa/std/data/pre.py +84 -0
  110. kalfa/std/data/splits.py +140 -0
  111. kalfa/std/data/stream.py +294 -0
  112. kalfa/std/data/table.py +642 -0
  113. kalfa/std/data/text.py +436 -0
  114. kalfa/std/data/transforms.py +327 -0
  115. kalfa/std/data/weights.py +113 -0
  116. kalfa/std/data/window.py +135 -0
  117. kalfa/std/figure.py +37 -0
  118. kalfa/std/init.py +72 -0
  119. kalfa/std/loggers.py +105 -0
  120. kalfa/std/losses.py +161 -0
  121. kalfa/std/metrics.py +463 -0
  122. kalfa/std/models.py +122 -0
  123. kalfa/std/notes.py +1553 -0
  124. kalfa/std/objectives.py +360 -0
  125. kalfa/std/plots/__init__.py +75 -0
  126. kalfa/std/plots/classification.py +207 -0
  127. kalfa/std/plots/common.py +44 -0
  128. kalfa/std/plots/curves.py +56 -0
  129. kalfa/std/plots/images.py +188 -0
  130. kalfa/std/plots/pairs.py +66 -0
  131. kalfa/std/plots/predictions.py +154 -0
  132. kalfa/std/samplers.py +261 -0
  133. kalfa/std/samples.py +72 -0
  134. kalfa/std/schedules.py +74 -0
  135. kalfa/std/steps.py +133 -0
  136. kalfa/std/triggers.py +135 -0
  137. kalfa/summary.py +296 -0
  138. kalfa/tui/__init__.py +4 -0
  139. kalfa/tui/app.py +107 -0
  140. kalfa/tui/config.py +61 -0
  141. kalfa/tui/guide.py +100 -0
  142. kalfa/tui/jobs.py +97 -0
  143. kalfa/tui/legos.py +62 -0
  144. kalfa/tui/records.py +439 -0
  145. kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
  146. kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
  147. kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
  148. kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,122 @@
1
+ import { computed, createApp, onMounted, ref, watch }
2
+ from "https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.esm-browser.prod.js";
3
+
4
+ import {
5
+ EVERY,
6
+ TABS,
7
+ applyTheme,
8
+ compareLink,
9
+ cycleTheme,
10
+ isActive,
11
+ latestSweep,
12
+ refresh,
13
+ setEvery,
14
+ startLive,
15
+ store,
16
+ sweepLink,
17
+ watchScheme,
18
+ } from "./store.js";
19
+ import { Compare } from "./views/compare.js";
20
+ import { Home } from "./views/home.js";
21
+ import { Record } from "./views/record.js";
22
+ import { Runs } from "./views/runs.js";
23
+ import { Sweep } from "./views/sweep.js";
24
+
25
+
26
+ const PAGES = ["home", "runs", "compare", "sweep", "record"];
27
+
28
+ function decodePart(part) {
29
+ try {
30
+ return decodeURIComponent(part);
31
+ } catch (error) {
32
+ return part;
33
+ }
34
+ }
35
+
36
+ export function parseHash(hash) {
37
+ const raw = String(hash || "").replace(/^#/, "").replace(/^\//, "");
38
+ const mark = raw.indexOf("?");
39
+ const route = mark >= 0 ? raw.slice(0, mark) : raw;
40
+ const query = Object.fromEntries(new URLSearchParams(mark >= 0 ? raw.slice(mark + 1) : ""));
41
+ const parts = route.split("/").filter(Boolean).map(decodePart);
42
+ const page = parts.length ? parts[0] : "home";
43
+ const rest = parts.slice(1);
44
+ if (page === "record") {
45
+ const named = rest.length > 1 && TABS.includes(rest[rest.length - 1]);
46
+ const tab = named ? rest[rest.length - 1] : "overview";
47
+ const path = (named ? rest.slice(0, -1) : rest).join("/");
48
+ return { page, path, tab, query, raw };
49
+ }
50
+ if (page === "sweep") return { page, path: rest.join("/"), tab: "", query, raw };
51
+ return { page: PAGES.includes(page) ? page : "missing", path: "", tab: "", query, raw };
52
+ }
53
+
54
+ const Board = {
55
+ name: "KalfaBoard",
56
+ components: { HomeView: Home, RunsView: Runs, CompareView: Compare, SweepView: Sweep, RecordView: Record },
57
+ setup() {
58
+ const route = ref(parseHash(window.location.hash));
59
+ const onHash = () => {
60
+ store.fault = "";
61
+ route.value = parseHash(window.location.hash);
62
+ };
63
+ const running = computed(() => store.records.filter(isActive).length);
64
+ const sweepHref = computed(() => {
65
+ const sweep = latestSweep();
66
+ return sweep ? sweepLink(sweep.path) : "";
67
+ });
68
+ const compareHref = computed(() => compareLink(store.selected));
69
+ const liveTitle = computed(() => (store.connected ? "live: the server reports changes every " + store.every + "s"
70
+ : "not connected to the live stream, retrying"));
71
+ const changeEvery = (event) => setEvery(event.target.value);
72
+
73
+ watch(running, (count) => {
74
+ document.title = count ? "kalfa board (" + count + " running)" : "kalfa board";
75
+ }, { immediate: true });
76
+ onMounted(() => {
77
+ window.addEventListener("hashchange", onHash);
78
+ watchScheme();
79
+ refresh();
80
+ startLive();
81
+ });
82
+ return { store, route, running, sweepHref, compareHref, liveTitle, changeEvery, cycleTheme, EVERY };
83
+ },
84
+ template: `
85
+ <header class="topbar">
86
+ <a class="brand" href="#/">kalfa</a>
87
+ <nav class="nav">
88
+ <a :class="{ active: route.page === 'home' }" href="#/">home</a>
89
+ <a :class="{ active: route.page === 'runs' }" href="#/runs">runs</a>
90
+ <a :class="{ active: route.page === 'compare' }" :href="compareHref">compare<span v-if="store.selected.length" class="count">{{ store.selected.length }}</span></a>
91
+ <a v-if="sweepHref" :class="{ active: route.page === 'sweep' }" :href="sweepHref">sweep</a>
92
+ <span v-else class="nav-off" title="no sweep under this root">sweep</span>
93
+ </nav>
94
+ <span class="root mono" :title="store.root">{{ store.root }}</span>
95
+ <span class="spacer"></span>
96
+ <span class="live" :class="{ on: store.connected }" :title="liveTitle">
97
+ <span class="dot"></span>{{ store.connected ? "live" : "offline" }}<span v-if="running" class="count">{{ running }}</span>
98
+ </span>
99
+ <select class="every" :value="store.every" @change="changeEvery" title="how often the server looks for changes">
100
+ <option v-for="value in EVERY" :key="value" :value="value">{{ value }}s</option>
101
+ </select>
102
+ <button type="button" class="tool" @click="cycleTheme" title="theme: auto follows the system">{{ store.theme }}</button>
103
+ </header>
104
+ <div v-if="store.fault" class="error-box page-fault">{{ store.fault }}</div>
105
+ <main class="main">
106
+ <home-view v-if="route.page === 'home'"></home-view>
107
+ <runs-view v-else-if="route.page === 'runs'"></runs-view>
108
+ <compare-view v-else-if="route.page === 'compare'" :key="'compare:' + route.raw" :query="route.query"></compare-view>
109
+ <sweep-view v-else-if="route.page === 'sweep'" :key="'sweep:' + route.path" :path="route.path"></sweep-view>
110
+ <record-view v-else-if="route.page === 'record'" :key="'record:' + route.path" :path="route.path" :tab="route.tab"></record-view>
111
+ <div v-else class="empty">there is no page at #/{{ route.raw }}; go <a class="link" href="#/">home</a></div>
112
+ </main>`,
113
+ };
114
+
115
+ const app = createApp(Board);
116
+ app.config.errorHandler = (error) => {
117
+ console.error(error);
118
+ store.fault = "the page hit an error: " + (error && error.message ? error.message : String(error));
119
+ };
120
+ applyTheme();
121
+ app.mount("#app");
122
+ window.kalfaMounted = true;
@@ -0,0 +1,228 @@
1
+ :root {
2
+ color-scheme: light;
3
+ --bg: #f4f4f2;
4
+ --surface: #fcfcfb;
5
+ --surface-2: #efeeeb;
6
+ --ink: #0b0b0b;
7
+ --ink-2: #52514e;
8
+ --muted: #74736e;
9
+ --line: #e2e1dc;
10
+ --line-2: #cbc9c2;
11
+ --accent: #2a78d6;
12
+ --accent-soft: #e3edfa;
13
+ --running: #2a78d6;
14
+ --stale: #b97a00;
15
+ --done: #1b8a5a;
16
+ --stopped: #74736e;
17
+ --failed: #d03b3a;
18
+ --series-1: #2a78d6;
19
+ --series-2: #eb6834;
20
+ --series-3: #1baf7a;
21
+ --series-4: #eda100;
22
+ --series-5: #e87ba4;
23
+ --series-6: #008300;
24
+ --series-7: #6250d6;
25
+ --series-8: #e34948;
26
+ --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
27
+ --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
28
+ --radius: 8px;
29
+ --shadow: 0 1px 2px rgba(20, 20, 18, 0.06);
30
+ }
31
+
32
+ @media (prefers-color-scheme: dark) {
33
+ :root:not([data-theme="light"]) {
34
+ color-scheme: dark;
35
+ --bg: #121211;
36
+ --surface: #1a1a19;
37
+ --surface-2: #232321;
38
+ --ink: #f0efec;
39
+ --ink-2: #c3c2b7;
40
+ --muted: #8f8e86;
41
+ --line: #2c2c2a;
42
+ --line-2: #3d3d3a;
43
+ --accent: #5b9cec;
44
+ --accent-soft: #1d2d44;
45
+ --running: #5b9cec;
46
+ --stale: #d9a227;
47
+ --done: #3dbb83;
48
+ --stopped: #8f8e86;
49
+ --failed: #ec6b6a;
50
+ --series-1: #3987e5;
51
+ --series-2: #d95926;
52
+ --series-3: #199e70;
53
+ --series-4: #c98500;
54
+ --series-5: #d55181;
55
+ --series-6: #008300;
56
+ --series-7: #9085e9;
57
+ --series-8: #e66767;
58
+ --shadow: none;
59
+ }
60
+ }
61
+
62
+ :root[data-theme="dark"] {
63
+ color-scheme: dark;
64
+ --bg: #121211;
65
+ --surface: #1a1a19;
66
+ --surface-2: #232321;
67
+ --ink: #f0efec;
68
+ --ink-2: #c3c2b7;
69
+ --muted: #8f8e86;
70
+ --line: #2c2c2a;
71
+ --line-2: #3d3d3a;
72
+ --accent: #5b9cec;
73
+ --accent-soft: #1d2d44;
74
+ --running: #5b9cec;
75
+ --stale: #d9a227;
76
+ --done: #3dbb83;
77
+ --stopped: #8f8e86;
78
+ --failed: #ec6b6a;
79
+ --series-1: #3987e5;
80
+ --series-2: #d95926;
81
+ --series-3: #199e70;
82
+ --series-4: #c98500;
83
+ --series-5: #d55181;
84
+ --series-6: #008300;
85
+ --series-7: #9085e9;
86
+ --series-8: #e66767;
87
+ --shadow: none;
88
+ }
89
+
90
+ * { box-sizing: border-box; }
91
+ html, body { margin: 0; padding: 0; }
92
+ body { font: 13px/1.5 var(--font); color: var(--ink); background: var(--bg); min-height: 100vh; }
93
+ h1, h2, h3 { margin: 0; font-weight: 600; }
94
+ h2 { font-size: 17px; }
95
+ h3 { font-size: 14px; }
96
+ a { color: inherit; }
97
+ button, input, select { font: inherit; color: inherit; }
98
+ pre { margin: 0; }
99
+ .mono { font-family: var(--mono); font-size: 12px; }
100
+ .muted { color: var(--muted); }
101
+ .small { font-size: 12px; }
102
+ .num { text-align: right; font-variant-numeric: tabular-nums; }
103
+ .spacer { flex: 1; }
104
+ .link { color: var(--accent); text-decoration: none; }
105
+ .link:hover { text-decoration: underline; }
106
+ .empty { color: var(--muted); margin: 8px 0; }
107
+ .loading { color: var(--muted); margin: 16px 0; }
108
+ .loading::before { content: ""; display: inline-block; width: 10px; height: 10px; margin-right: 8px; border: 2px solid var(--line-2); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; vertical-align: -1px; }
109
+ @keyframes spin { to { transform: rotate(360deg); } }
110
+ .fail-text { color: var(--failed); }
111
+
112
+ .topbar { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; gap: 14px; padding: 8px 16px; background: var(--surface); border-bottom: 1px solid var(--line); flex-wrap: wrap; }
113
+ .brand { font-weight: 700; font-size: 15px; text-decoration: none; letter-spacing: 0.02em; }
114
+ .nav { display: flex; gap: 4px; flex-wrap: wrap; }
115
+ .nav a, .nav-off { padding: 4px 10px; border-radius: 6px; text-decoration: none; color: var(--ink-2); }
116
+ .nav a:hover { background: var(--surface-2); color: var(--ink); }
117
+ .nav a.active { background: var(--accent-soft); color: var(--accent); }
118
+ .nav-off { color: var(--muted); opacity: 0.6; cursor: default; }
119
+ .count { display: inline-block; margin-left: 6px; padding: 0 6px; min-width: 18px; border-radius: 9px; background: var(--accent); color: #fff; font-size: 11px; text-align: center; line-height: 18px; }
120
+ .root { color: var(--muted); max-width: 40vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; }
121
+ .live { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); }
122
+ .live .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--line-2); }
123
+ .live.on { color: var(--ink-2); }
124
+ .live.on .dot { background: var(--done); box-shadow: 0 0 0 3px color-mix(in srgb, var(--done) 25%, transparent); }
125
+ .every { padding: 3px 6px; border: 1px solid var(--line-2); border-radius: 6px; background: var(--surface); }
126
+
127
+ .main { max-width: 1440px; margin: 0 auto; padding: 16px; }
128
+ .page { display: flex; flex-direction: column; gap: 12px; }
129
+ .page-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
130
+ .section-title { margin-top: 8px; }
131
+ .page-fault { margin: 12px 16px 0; }
132
+
133
+ .card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 12px; min-width: 0; }
134
+ .card-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; min-width: 0; }
135
+ .card-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
136
+ .card-tools { display: flex; gap: 4px; flex: none; }
137
+ .card-note { margin-bottom: 4px; }
138
+ .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr)); gap: 12px; }
139
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(420px, 100%), 1fr)); gap: 12px; }
140
+ .stat-label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
141
+ .stat-value { font-size: 18px; font-weight: 600; margin: 2px 0; overflow-wrap: anywhere; }
142
+ .chip { display: inline-block; margin: 2px 4px 2px 0; padding: 1px 7px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--line); font-size: 12px; }
143
+ .notice { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 8px 12px; border-radius: var(--radius); background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); }
144
+ .error-box { padding: 8px 12px; border-radius: var(--radius); color: var(--failed); background: color-mix(in srgb, var(--failed) 10%, transparent); border: 1px solid color-mix(in srgb, var(--failed) 35%, transparent); white-space: pre-wrap; overflow-wrap: anywhere; }
145
+ .notice-box { padding: 8px 12px; border-radius: var(--radius); color: var(--ink); background: color-mix(in srgb, var(--stopped) 12%, transparent); border: 1px solid color-mix(in srgb, var(--stopped) 40%, transparent); white-space: pre-wrap; overflow-wrap: anywhere; }
146
+
147
+ .button, .tool { display: inline-flex; align-items: center; gap: 4px; border: 1px solid var(--line-2); background: var(--surface); border-radius: 6px; cursor: pointer; text-decoration: none; white-space: nowrap; }
148
+ .button { padding: 5px 12px; }
149
+ .tool { padding: 2px 8px; font-size: 12px; color: var(--ink-2); }
150
+ .button:hover, .tool:hover { border-color: var(--accent); color: var(--accent); }
151
+ .tool.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
152
+ .button.ghost { border-color: transparent; background: transparent; }
153
+ .button.danger { border-color: color-mix(in srgb, var(--failed) 50%, transparent); color: var(--failed); }
154
+ .button.danger:hover { background: color-mix(in srgb, var(--failed) 10%, transparent); color: var(--failed); border-color: var(--failed); }
155
+ .button.disabled, .button:disabled { opacity: 0.5; pointer-events: none; }
156
+ .filter { padding: 5px 9px; border: 1px solid var(--line-2); border-radius: 6px; background: var(--surface); min-width: 0; width: min(360px, 100%); }
157
+ .filter:focus, .every:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
158
+ .toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
159
+ .toolbar .filter { flex: 1; width: auto; max-width: 640px; }
160
+
161
+ .badge { display: inline-block; padding: 0 8px; border-radius: 10px; font-size: 11px; line-height: 18px; font-weight: 600; white-space: nowrap; --tone: var(--stopped); color: var(--tone); background: color-mix(in srgb, var(--tone) 14%, transparent); border: 1px solid color-mix(in srgb, var(--tone) 35%, transparent); }
162
+ .badge.is-running { --tone: var(--running); }
163
+ .badge.is-stale { --tone: var(--stale); }
164
+ .badge.is-done { --tone: var(--done); }
165
+ .badge.is-stopped { --tone: var(--stopped); }
166
+ .badge.is-failed { --tone: var(--failed); }
167
+ .badge.is-partial { --tone: var(--stale); }
168
+ .badge.is-unknown { --tone: var(--muted); }
169
+
170
+ .progress { display: inline-flex; align-items: center; gap: 8px; min-width: 120px; }
171
+ .progress-track { flex: 1; min-width: 60px; height: 6px; border-radius: 3px; background: var(--surface-2); border: 1px solid var(--line); overflow: hidden; }
172
+ .progress-fill { display: block; height: 100%; background: var(--accent); border-radius: 3px; transition: width 0.3s ease; }
173
+ .progress.unknown .progress-track { opacity: 0.5; }
174
+ .progress-text { color: var(--ink-2); font-variant-numeric: tabular-nums; white-space: nowrap; font-size: 12px; }
175
+
176
+ .spark { display: inline-block; vertical-align: middle; color: var(--accent); }
177
+
178
+ .table-box { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
179
+ .table-tools { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
180
+ .table-wrap { overflow-x: auto; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
181
+ table.data { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
182
+ table.data th, table.data td { padding: 6px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; white-space: nowrap; }
183
+ table.data th { position: sticky; top: 0; background: var(--surface-2); color: var(--ink-2); font-weight: 600; font-size: 12px; user-select: none; }
184
+ table.data th.sortable { cursor: pointer; }
185
+ table.data th.sortable:hover { color: var(--ink); }
186
+ table.data th.num, table.data td.num { text-align: right; }
187
+ table.data tbody tr:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
188
+ table.data tbody tr.selected { background: var(--accent-soft); }
189
+ table.data tbody tr:last-child td { border-bottom: 0; }
190
+ table.data .check { width: 28px; padding-right: 0; }
191
+ table.data .arrow { color: var(--accent); font-size: 9px; }
192
+ table.data td.empty { text-align: center; padding: 16px; }
193
+
194
+ .live-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); gap: 12px; }
195
+ .live-card { display: flex; flex-direction: column; gap: 6px; text-decoration: none; }
196
+ .live-card:hover { border-color: var(--accent); }
197
+ .live-head { display: flex; align-items: center; gap: 8px; }
198
+ .live-name { font-weight: 600; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
199
+
200
+ .plot { width: 100%; min-height: 120px; }
201
+ .modal { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(10, 10, 9, 0.55); }
202
+ .modal-box { display: flex; flex-direction: column; width: min(1280px, 100%); height: min(860px, 100%); padding: 12px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
203
+ .plot.large { flex: 1; min-height: 0; }
204
+
205
+ .diff { font-family: var(--mono); font-size: 12px; padding: 8px; overflow-x: auto; background: var(--surface-2); border-radius: 6px; }
206
+ .diff span { display: block; white-space: pre; min-height: 1.4em; }
207
+ .diff .head { color: var(--muted); }
208
+ .diff .hunk { color: var(--accent); }
209
+ .diff .add { color: var(--done); background: color-mix(in srgb, var(--done) 10%, transparent); }
210
+ .diff .del { color: var(--failed); background: color-mix(in srgb, var(--failed) 10%, transparent); }
211
+ .shared { line-height: 2; }
212
+
213
+ .record-head { display: flex; flex-direction: column; gap: 8px; }
214
+ .record-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
215
+ .record-title h2 { overflow-wrap: anywhere; }
216
+ .record-meta { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
217
+ .tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); overflow-x: auto; }
218
+ .tab { padding: 6px 12px; text-decoration: none; color: var(--ink-2); border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; }
219
+ .tab:hover { color: var(--ink); }
220
+ .tab.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
221
+ .tab-body { min-width: 0; }
222
+
223
+ @media (max-width: 720px) {
224
+ .main { padding: 12px 16px; }
225
+ .root { display: none; }
226
+ .grid { grid-template-columns: 1fr; }
227
+ .record-meta { gap: 8px 12px; }
228
+ }
@@ -0,0 +1,46 @@
1
+ .chart-card .tool.tuned::after { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
2
+
3
+ .chart-modal { padding: 16px; }
4
+ .chart-box { width: min(1480px, 100%); height: min(940px, 100%); gap: 6px; box-shadow: var(--shadow); font-family: var(--font); color: var(--ink); }
5
+ .chart-box > .card-head { margin-bottom: 0; }
6
+ .chart-body { flex: 1; min-height: 0; display: flex; gap: 14px; }
7
+ .chart-stage { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; align-items: flex-start; overflow: auto; }
8
+ .chart-stage .plot.large { min-height: 200px; }
9
+
10
+ .chart-panel { flex: none; width: 300px; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; padding: 0 4px 8px 14px; border-left: 1px solid var(--line); font-size: 12px; }
11
+ .chart-group { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; border: 0; }
12
+ .chart-group-title { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
13
+ details.chart-group > summary { cursor: pointer; user-select: none; }
14
+ details.chart-group > summary:hover { color: var(--ink); }
15
+ .chart-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
16
+ .chart-label { flex: none; width: 74px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
17
+ .chart-unit { flex: none; color: var(--muted); }
18
+ .chart-hint { color: var(--muted); font-size: 11px; line-height: 1.4; }
19
+
20
+ .chart-panel input[type="number"], .chart-panel input[type="text"], .chart-panel select, .chart-panel textarea { min-width: 0; padding: 3px 6px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); font-family: var(--font); font-size: 12px; }
21
+ .chart-row input[type="number"], .chart-row input[type="text"], .chart-row select { flex: 1 1 0; width: 0; }
22
+ .chart-panel input:focus, .chart-panel select:focus, .chart-panel textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
23
+ .chart-panel select:disabled { opacity: 0.5; }
24
+ .chart-panel input[type="range"] { flex: 1; min-width: 0; margin: 0; accent-color: var(--accent); }
25
+ .chart-panel input[type="checkbox"] { margin: 0; accent-color: var(--accent); }
26
+ .chart-panel textarea { width: 100%; min-height: 104px; resize: vertical; padding: 6px 8px; font-family: var(--mono); line-height: 1.45; }
27
+ .chart-check { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: var(--ink); }
28
+
29
+ .chart-auto { flex: none; min-width: 42px; padding: 1px 4px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--muted); font-family: var(--mono); font-size: 11px; text-align: right; cursor: pointer; }
30
+ .chart-auto:not(:disabled):hover { color: var(--accent); border-color: var(--line); }
31
+ .chart-auto:disabled { cursor: default; }
32
+
33
+ .chart-swatches { display: flex; flex-wrap: wrap; gap: 3px; padding-left: 80px; }
34
+ .chart-swatch { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--line); }
35
+
36
+ .chart-actions { flex-wrap: wrap; }
37
+ .chart-reset:hover { border-color: var(--failed); color: var(--failed); }
38
+ .chart-error { color: var(--failed); font-size: 11px; white-space: pre-wrap; overflow-wrap: anywhere; }
39
+
40
+ @media (max-width: 900px) {
41
+ .chart-modal { padding: 8px; }
42
+ .chart-box { height: 100%; overflow-y: auto; }
43
+ .chart-body { flex: none; flex-direction: column; }
44
+ .chart-stage { flex: none; height: 62vh; }
45
+ .chart-panel { width: auto; overflow: visible; padding: 12px 0 0; border-left: 0; border-top: 1px solid var(--line); }
46
+ }
@@ -0,0 +1,14 @@
1
+ const KNOWN = ["running", "stale", "done", "stopped", "failed", "partial", "unknown"];
2
+
3
+ export const Badge = {
4
+ name: "KBadge",
5
+ props: { status: { type: String, default: "unknown" } },
6
+ setup(props) {
7
+ const label = () => props.status || "unknown";
8
+ const tone = () => (KNOWN.includes(label()) ? label() : "unknown");
9
+ return { label, tone };
10
+ },
11
+ template: '<span class="badge" :class="\'is-\' + tone()">{{ label() }}</span>',
12
+ };
13
+
14
+ export default Badge;
@@ -0,0 +1,204 @@
1
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch }
2
+ from "https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.esm-browser.prod.js";
3
+
4
+ import { draw, exportImage, numberOf, purge } from "../plot.js";
5
+ import { store } from "../store.js";
6
+ import { ChartSettings, chartSettings, drawOptions, forgetSettings, isTuned, payloadOf, saveSettings }
7
+ from "./settings.js";
8
+
9
+
10
+ function labelsOf(kind, options) {
11
+ const fallback = { histogram: ["", "count"], heatmap: ["predicted", "truth"] }[kind] || ["", ""];
12
+ return { xlabel: options.xlabel || fallback[0], ylabel: options.ylabel || fallback[1] };
13
+ }
14
+
15
+ export const Chart = {
16
+ name: "KChart",
17
+ components: { KChartSettings: ChartSettings },
18
+ props: {
19
+ title: { type: String, default: "" },
20
+ kind: { type: String, default: "lines" },
21
+ data: { default: null },
22
+ options: { type: Object, default: () => ({}) },
23
+ height: { type: Number, default: 260 },
24
+ logToggle: { type: Boolean, default: true },
25
+ note: { type: String, default: "" },
26
+ settingsKey: { type: String, default: "" },
27
+ binsBox: { type: Boolean, default: false },
28
+ },
29
+ emits: ["settings"],
30
+ setup(props, { emit }) {
31
+ const host = ref(null);
32
+ const large = ref(null);
33
+ const open = ref(false);
34
+ const panel = ref(true);
35
+ const failure = ref("");
36
+ const measured = ref({ width: 0, height: 0 });
37
+ let pressed = false;
38
+ let announced = "";
39
+
40
+ const base = () => props.options || {};
41
+ const key = computed(() => props.settingsKey || props.kind + ":" + props.title);
42
+ const settings = computed(() => chartSettings(key.value));
43
+ const tuned = computed(() => isTuned(settings.value));
44
+ const logy = computed(() => {
45
+ const scale = settings.value.yscale;
46
+ return scale ? scale === "log" : Boolean(base().logy);
47
+ });
48
+ const auto = computed(() => ({
49
+ logx: Boolean(base().logx),
50
+ logy: Boolean(base().logy),
51
+ horizontal: base().horizontal !== false,
52
+ ...labelsOf(props.kind, base()),
53
+ }));
54
+ const box = computed(() => ({
55
+ width: numberOf(settings.value.boxWidth),
56
+ height: numberOf(settings.value.boxHeight),
57
+ }));
58
+ const largeStyle = computed(() => {
59
+ const style = {};
60
+ if (box.value.width) style.width = Math.max(200, box.value.width) + "px";
61
+ if (box.value.height) {
62
+ style.height = Math.max(200, box.value.height) + "px";
63
+ style.flex = "none";
64
+ }
65
+ return style;
66
+ });
67
+
68
+ const optionsFor = (height) => ({ ...base(), ...drawOptions(settings.value), revision: key.value, height });
69
+ const paint = (el, height) => {
70
+ if (!el || props.data === null || props.data === undefined) return;
71
+ try {
72
+ draw(props.kind, el, props.data, optionsFor(height));
73
+ failure.value = "";
74
+ } catch (error) {
75
+ failure.value = "cannot draw this chart: " + error.message;
76
+ }
77
+ };
78
+ const bigHeight = () => {
79
+ if (box.value.height) return Math.max(200, box.value.height);
80
+ return Math.max(240, (large.value && large.value.clientHeight) || window.innerHeight * 0.6);
81
+ };
82
+ const paintLarge = () => {
83
+ const el = large.value;
84
+ if (!open.value || !el) return;
85
+ paint(el, bigHeight());
86
+ const size = { width: el.clientWidth, height: el.clientHeight };
87
+ if (size.width !== measured.value.width || size.height !== measured.value.height) measured.value = size;
88
+ };
89
+ const redraw = () => {
90
+ paint(host.value, props.height);
91
+ if (open.value) nextTick(paintLarge);
92
+ };
93
+ const announce = (force) => {
94
+ const payload = payloadOf(key.value, settings.value, base());
95
+ const text = JSON.stringify(payload);
96
+ if (text === announced) return;
97
+ announced = text;
98
+ if (force) emit("settings", payload);
99
+ };
100
+
101
+ const onKey = (event) => {
102
+ if (event.key === "Escape") close();
103
+ };
104
+ const close = () => {
105
+ purge(large.value);
106
+ open.value = false;
107
+ window.removeEventListener("keydown", onKey);
108
+ };
109
+ const expand = () => {
110
+ open.value = true;
111
+ window.addEventListener("keydown", onKey);
112
+ nextTick(paintLarge);
113
+ };
114
+ const press = (event) => {
115
+ pressed = event.target === event.currentTarget;
116
+ };
117
+ const release = (event) => {
118
+ if (pressed && event.target === event.currentTarget) close();
119
+ pressed = false;
120
+ };
121
+ const togglePanel = () => {
122
+ panel.value = !panel.value;
123
+ };
124
+ const toggleLog = () => {
125
+ const next = !logy.value;
126
+ settings.value.yscale = next === Boolean(base().logy) ? "" : next ? "log" : "linear";
127
+ };
128
+ const reset = () => forgetSettings(key.value);
129
+ const download = (format) => {
130
+ const current = settings.value;
131
+ exportImage(large.value, {
132
+ format,
133
+ width: numberOf(current.imageWidth) || measured.value.width,
134
+ height: numberOf(current.imageHeight) || measured.value.height,
135
+ scale: numberOf(current.imageScale),
136
+ filename: props.title || key.value,
137
+ }).catch((error) => {
138
+ failure.value = "cannot download this chart: " + error.message;
139
+ });
140
+ };
141
+
142
+ watch([key, () => JSON.stringify(settings.value)], (now, before) => {
143
+ if (now[0] === before[0]) saveSettings(key.value, settings.value);
144
+ redraw();
145
+ announce(true);
146
+ });
147
+ watch(() => props.data, redraw);
148
+ watch(() => props.kind, redraw);
149
+ watch(() => JSON.stringify(props.options || {}), redraw);
150
+ watch(() => store.paint, redraw);
151
+ onMounted(() => {
152
+ redraw();
153
+ announce(tuned.value);
154
+ });
155
+ onBeforeUnmount(() => {
156
+ window.removeEventListener("keydown", onKey);
157
+ purge(host.value);
158
+ purge(large.value);
159
+ });
160
+ return {
161
+ host, large, open, panel, failure, measured, settings, tuned, logy, auto, largeStyle, expand, close, press,
162
+ release, togglePanel, toggleLog, reset, download,
163
+ };
164
+ },
165
+ template: `
166
+ <div class="card chart-card">
167
+ <div class="card-head">
168
+ <span class="card-title" :title="title">{{ title }}</span>
169
+ <span class="card-tools">
170
+ <button v-if="logToggle" type="button" class="tool" :class="{ on: logy }" @click="toggleLog">log y</button>
171
+ <button type="button" class="tool" :class="{ tuned: tuned }" @click="expand"
172
+ :title="tuned ? 'open this chart large; it has saved settings' : 'open this chart large with its settings'">expand</button>
173
+ </span>
174
+ </div>
175
+ <div v-if="note" class="card-note muted small">{{ note }}</div>
176
+ <div v-if="failure" class="error-box">{{ failure }}</div>
177
+ <div ref="host" class="plot" :style="{ height: height + 'px' }"></div>
178
+ <teleport to="body">
179
+ <div v-if="open" class="modal chart-modal" @mousedown="press" @click="release">
180
+ <div class="modal-box chart-box" role="dialog" aria-modal="true" :aria-label="title || 'chart'">
181
+ <div class="card-head">
182
+ <span class="card-title" :title="title">{{ title }}</span>
183
+ <span class="card-tools">
184
+ <button v-if="logToggle" type="button" class="tool" :class="{ on: logy }" @click="toggleLog">log y</button>
185
+ <button type="button" class="tool" :class="{ on: panel }" @click="togglePanel">settings</button>
186
+ <button type="button" class="tool" @click="close">close</button>
187
+ </span>
188
+ </div>
189
+ <div v-if="note" class="card-note muted small">{{ note }}</div>
190
+ <div v-if="failure" class="error-box">{{ failure }}</div>
191
+ <div class="chart-body">
192
+ <div class="chart-stage">
193
+ <div ref="large" class="plot large" :style="largeStyle"></div>
194
+ </div>
195
+ <k-chart-settings v-if="panel" :settings="settings" :kind="kind" :auto="auto" :size="measured"
196
+ :bins-box="binsBox" @reset="reset" @download="download"></k-chart-settings>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </teleport>
201
+ </div>`,
202
+ };
203
+
204
+ export default Chart;