@dxo/studio 0.0.0 → 0.0.10
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/README.md +23 -1
- package/designs/styles/studio.css +208 -0
- package/dist/api-server.d.ts +15 -0
- package/dist/api-server.d.ts.map +1 -0
- package/dist/api-server.js +23 -0
- package/dist/api-server.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/start.d.ts +25 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +65 -0
- package/dist/start.js.map +1 -0
- package/package.json +54 -8
- package/public/dxo-studio-api.js +2 -0
- package/src/Application.vmz +8 -0
- package/src/lib/api.ts +32 -0
- package/src/pages/compare.vmz +117 -0
- package/src/pages/index.vmz +60 -0
- package/src/pages/playground.vmz +91 -0
- package/src/pages/runs/[id].vmz +295 -0
- package/vmz.config.ts +12 -0
package/README.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
1
|
# @dxo/studio
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DXO training workbench (developer preview). **WebUI is VMZ** — not a separate Vue app.
|
|
4
|
+
|
|
5
|
+
## Two entry points
|
|
6
|
+
|
|
7
|
+
| Entry | What it does |
|
|
8
|
+
|----------------------|------------------------------------------------------------|
|
|
9
|
+
| **`dxo studio`** | VMZ **watch serve** + Rust inspect API → **browser** |
|
|
10
|
+
| **`dxo-studio.exe`** | Tauri desktop GUI (embedded VMZ static build + native API) |
|
|
11
|
+
|
|
12
|
+
| Layer | Path |
|
|
13
|
+
|-------------|--------------------------------------------------------------|
|
|
14
|
+
| Rust API | `projects/compilers/dxo-studio` |
|
|
15
|
+
| Rust GUI | `projects/compilers/dxo-studio-tauri` → **`dxo-studio.exe`** |
|
|
16
|
+
| TS launcher | `launcher/` → `dist/` |
|
|
17
|
+
| VMZ WebUI | package root (`src/*.vmz`, `vmz.config.ts`) |
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm exec dxo studio
|
|
21
|
+
pnpm --filter @dxo/studio dev # VMZ only
|
|
22
|
+
pnpm --filter @dxo/studio build:ui # static profile for Tauri embed
|
|
23
|
+
|
|
24
|
+
cargo build -p dxo-studio-tauri --release # → target/release/dxo-studio.exe
|
|
25
|
+
```
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
.studio {
|
|
2
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
3
|
+
max-width: 56rem;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
padding: 1.5rem;
|
|
6
|
+
color: #122;
|
|
7
|
+
background: #f7f8f6;
|
|
8
|
+
min-height: 100vh;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.studio__header {
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: space-between;
|
|
14
|
+
align-items: baseline;
|
|
15
|
+
margin-bottom: 1.5rem;
|
|
16
|
+
border-bottom: 1px solid #ccd;
|
|
17
|
+
padding-bottom: 0.75rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.studio__brand {
|
|
21
|
+
color: inherit;
|
|
22
|
+
text-decoration: none;
|
|
23
|
+
font-size: 1.25rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.studio__nav {
|
|
27
|
+
display: flex;
|
|
28
|
+
gap: 1rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.studio__nav a {
|
|
32
|
+
color: #345;
|
|
33
|
+
text-decoration: none;
|
|
34
|
+
font-size: 0.95rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.studio__nav a:hover {
|
|
38
|
+
text-decoration: underline;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.studio__btn {
|
|
42
|
+
margin-top: 0.75rem;
|
|
43
|
+
margin-right: 0.5rem;
|
|
44
|
+
padding: 0.4rem 0.9rem;
|
|
45
|
+
border: 1px solid #9ab;
|
|
46
|
+
border-radius: 0.35rem;
|
|
47
|
+
background: #dfe8e0;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
font-size: 0.9rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.studio__btn:disabled {
|
|
53
|
+
opacity: 0.5;
|
|
54
|
+
cursor: not-allowed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.studio__btn--ghost {
|
|
58
|
+
background: transparent;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.studio__pick-list label {
|
|
62
|
+
display: flex;
|
|
63
|
+
gap: 0.5rem;
|
|
64
|
+
align-items: center;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.studio__matrix td,
|
|
69
|
+
.studio__matrix th {
|
|
70
|
+
text-align: center;
|
|
71
|
+
min-width: 2.5rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.studio__cell--warm {
|
|
75
|
+
background: #e8f0dc;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.studio__cell--hot {
|
|
79
|
+
background: #c5ddb0;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.studio__samples {
|
|
84
|
+
list-style: none;
|
|
85
|
+
padding: 0;
|
|
86
|
+
display: grid;
|
|
87
|
+
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
|
88
|
+
gap: 0.75rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.studio__samples li {
|
|
92
|
+
border: 1px solid #dde;
|
|
93
|
+
padding: 0.35rem;
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
gap: 0.35rem;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.studio__samples img {
|
|
100
|
+
width: 100%;
|
|
101
|
+
height: auto;
|
|
102
|
+
display: block;
|
|
103
|
+
background: #eef1ee;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.studio__upload .studio__file {
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-direction: column;
|
|
109
|
+
gap: 0.35rem;
|
|
110
|
+
margin: 1rem 0;
|
|
111
|
+
font-size: 0.9rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.studio__preview img {
|
|
115
|
+
max-width: 16rem;
|
|
116
|
+
margin-top: 0.75rem;
|
|
117
|
+
border: 1px solid #ccd;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.studio__compare {
|
|
121
|
+
margin-top: 1.5rem;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.studio__panel h1 {
|
|
125
|
+
font-size: 1.5rem;
|
|
126
|
+
margin: 0 0 0.5rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.studio__panel h2 {
|
|
130
|
+
font-size: 1.1rem;
|
|
131
|
+
margin: 1.5rem 0 0.5rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.studio__hint,
|
|
135
|
+
.studio__status,
|
|
136
|
+
.studio__meta {
|
|
137
|
+
color: #456;
|
|
138
|
+
font-size: 0.9rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.studio__list {
|
|
142
|
+
list-style: none;
|
|
143
|
+
padding: 0;
|
|
144
|
+
margin: 1rem 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.studio__list li {
|
|
148
|
+
padding: 0.5rem 0;
|
|
149
|
+
border-bottom: 1px solid #dde;
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-wrap: wrap;
|
|
152
|
+
gap: 0.75rem;
|
|
153
|
+
align-items: baseline;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.studio__list--compact li {
|
|
157
|
+
font-size: 0.85rem;
|
|
158
|
+
font-family: ui-monospace, monospace;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.studio__badge {
|
|
162
|
+
font-size: 0.75rem;
|
|
163
|
+
padding: 0.1rem 0.4rem;
|
|
164
|
+
border-radius: 0.25rem;
|
|
165
|
+
background: #dde8e0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.studio__table {
|
|
169
|
+
width: 100%;
|
|
170
|
+
border-collapse: collapse;
|
|
171
|
+
font-size: 0.9rem;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.studio__table th,
|
|
175
|
+
.studio__table td {
|
|
176
|
+
text-align: left;
|
|
177
|
+
padding: 0.35rem 0.5rem;
|
|
178
|
+
border-bottom: 1px solid #dde;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.studio__pre {
|
|
182
|
+
background: #eef1ee;
|
|
183
|
+
padding: 0.75rem;
|
|
184
|
+
overflow: auto;
|
|
185
|
+
font-size: 0.8rem;
|
|
186
|
+
max-height: 24rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.studio__tabs {
|
|
190
|
+
display: flex;
|
|
191
|
+
gap: 0.35rem;
|
|
192
|
+
margin: 0.5rem 0 0.75rem;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.studio__tab {
|
|
196
|
+
border: 1px solid #ccd;
|
|
197
|
+
background: #f7f8f7;
|
|
198
|
+
color: #334;
|
|
199
|
+
padding: 0.35rem 0.7rem;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
font: inherit;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.studio__tab--active {
|
|
205
|
+
background: #2a4;
|
|
206
|
+
color: #fff;
|
|
207
|
+
border-color: #2a4;
|
|
208
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type InspectApiServerOptions = {
|
|
2
|
+
host?: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
runsRoot?: string;
|
|
5
|
+
};
|
|
6
|
+
export type InspectApiServer = {
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
url: string;
|
|
10
|
+
runsRoot: string;
|
|
11
|
+
close: () => Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
/** Loopback HTTP API over the append-only inspect run store (Rust `dxo-studio`). */
|
|
14
|
+
export declare function createInspectApiServer(options?: InspectApiServerOptions): Promise<InspectApiServer>;
|
|
15
|
+
//# sourceMappingURL=api-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-server.d.ts","sourceRoot":"","sources":["../launcher/api-server.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,uBAAuB,GAAG;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAcF,oFAAoF;AACpF,wBAAsB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmB7G"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { loadNative } from '@dxo/core/native';
|
|
2
|
+
/** Loopback HTTP API over the append-only inspect run store (Rust `dxo-studio`). */
|
|
3
|
+
export async function createInspectApiServer(options = {}) {
|
|
4
|
+
const native = loadNative();
|
|
5
|
+
if (typeof native.createInspectApiServer !== 'function') {
|
|
6
|
+
throw new Error('createInspectApiServer requires `pnpm build:native`');
|
|
7
|
+
}
|
|
8
|
+
const handle = native.createInspectApiServer({
|
|
9
|
+
host: options.host,
|
|
10
|
+
port: options.port,
|
|
11
|
+
runsRoot: options.runsRoot,
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
host: handle.host,
|
|
15
|
+
port: handle.port,
|
|
16
|
+
url: handle.url,
|
|
17
|
+
runsRoot: handle.runsRoot,
|
|
18
|
+
close: async () => {
|
|
19
|
+
handle.close();
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=api-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-server.js","sourceRoot":"","sources":["../launcher/api-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AA4B9C,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,UAAmC,EAAE;IAC9E,MAAM,MAAM,GAAG,UAAU,EAAuB,CAAC;IACjD,IAAI,OAAO,MAAM,CAAC,sBAAsB,KAAK,UAAU,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IAC3E,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,sBAAsB,CAAC;QACzC,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ;KAC7B,CAAC,CAAC;IACH,OAAO;QACH,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,KAAK,EAAE,KAAK,IAAI,EAAE;YACd,MAAM,CAAC,KAAK,EAAE,CAAC;QACnB,CAAC;KACJ,CAAC;AACN,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../launcher/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC/B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../launcher/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,sBAAsB,GAGzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,YAAY,EAA+C,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/start.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type InspectApiServer, type InspectApiServerOptions } from './api-server.js';
|
|
3
|
+
export type StartStudioOptions = InspectApiServerOptions & {
|
|
4
|
+
/** Start VMZ watch serve for browser WebUI. Default true. */
|
|
5
|
+
webui?: boolean;
|
|
6
|
+
/** VMZ dev port; omit to auto-pick from 5173. */
|
|
7
|
+
webuiPort?: number;
|
|
8
|
+
};
|
|
9
|
+
export type StudioProcess = InspectApiServer & {
|
|
10
|
+
webui?: ChildProcess;
|
|
11
|
+
webuiUrl?: string;
|
|
12
|
+
closeAll: () => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* `dxo studio`: Rust inspect API + VMZ watch serve → open in browser.
|
|
16
|
+
* Desktop GUI is `dxo-studio.exe` (Tauri), not this path.
|
|
17
|
+
*/
|
|
18
|
+
export declare function startStudio(options?: StartStudioOptions): Promise<StudioProcess>;
|
|
19
|
+
/** @deprecated Use {@link startStudio}. */
|
|
20
|
+
export declare function createStudio(options?: StartStudioOptions): {
|
|
21
|
+
kind: 'launcher';
|
|
22
|
+
options: StartStudioOptions;
|
|
23
|
+
};
|
|
24
|
+
export declare function studioVersion(): string;
|
|
25
|
+
//# sourceMappingURL=start.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../launcher/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAS,MAAM,oBAAoB,CAAC;AAI9D,OAAO,EAA0B,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAE9G,MAAM,MAAM,kBAAkB,GAAG,uBAAuB,GAAG;IACvD,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG;IAC3C,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC,CAAC;AA6BF;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,aAAa,CAAC,CA2B1F;AAED,2CAA2C;AAC3C,wBAAgB,YAAY,CAAC,OAAO,GAAE,kBAAuB,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,kBAAkB,CAAA;CAAE,CAEhH;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
package/dist/start.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createInspectApiServer } from './api-server.js';
|
|
6
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
function spawnVmzDev(apiUrl, port) {
|
|
8
|
+
const env = {
|
|
9
|
+
...process.env,
|
|
10
|
+
DXO_STUDIO_API: apiUrl,
|
|
11
|
+
...(port ? { PORT: String(port), VMZ_PORT: String(port) } : {}),
|
|
12
|
+
};
|
|
13
|
+
const isWin = process.platform === 'win32';
|
|
14
|
+
const cmd = isWin ? 'pnpm.cmd' : 'pnpm';
|
|
15
|
+
const args = ['exec', 'vmz', 'dev', '.'];
|
|
16
|
+
if (port) {
|
|
17
|
+
args.push('--port', String(port));
|
|
18
|
+
}
|
|
19
|
+
return spawn(cmd, args, {
|
|
20
|
+
cwd: packageRoot,
|
|
21
|
+
env,
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
shell: isWin,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function writeStudioApiBootstrap(apiUrl) {
|
|
27
|
+
const out = path.join(packageRoot, 'public', 'dxo-studio-api.js');
|
|
28
|
+
await writeFile(out, `window.__DXO_STUDIO_API__=${JSON.stringify(apiUrl)};\n`, 'utf8');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `dxo studio`: Rust inspect API + VMZ watch serve → open in browser.
|
|
32
|
+
* Desktop GUI is `dxo-studio.exe` (Tauri), not this path.
|
|
33
|
+
*/
|
|
34
|
+
export async function startStudio(options = {}) {
|
|
35
|
+
const api = await createInspectApiServer({
|
|
36
|
+
host: options.host ?? '127.0.0.1',
|
|
37
|
+
port: options.port ?? 4310,
|
|
38
|
+
runsRoot: options.runsRoot,
|
|
39
|
+
});
|
|
40
|
+
const webuiEnabled = options.webui !== false;
|
|
41
|
+
let webui;
|
|
42
|
+
let webuiUrl;
|
|
43
|
+
if (webuiEnabled) {
|
|
44
|
+
await writeStudioApiBootstrap(api.url);
|
|
45
|
+
webui = spawnVmzDev(api.url, options.webuiPort);
|
|
46
|
+
webuiUrl = options.webuiPort
|
|
47
|
+
? `http://127.0.0.1:${options.webuiPort}`
|
|
48
|
+
: 'http://127.0.0.1:5173 (VMZ default; check terminal if port differs)';
|
|
49
|
+
}
|
|
50
|
+
const closeAll = async () => {
|
|
51
|
+
if (webui && !webui.killed) {
|
|
52
|
+
webui.kill();
|
|
53
|
+
}
|
|
54
|
+
await api.close();
|
|
55
|
+
};
|
|
56
|
+
return { ...api, webui, webuiUrl, closeAll };
|
|
57
|
+
}
|
|
58
|
+
/** @deprecated Use {@link startStudio}. */
|
|
59
|
+
export function createStudio(options = {}) {
|
|
60
|
+
return { kind: 'launcher', options };
|
|
61
|
+
}
|
|
62
|
+
export function studioVersion() {
|
|
63
|
+
return 'dxo-studio@0.1.0-preview';
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=start.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start.js","sourceRoot":"","sources":["../launcher/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,sBAAsB,EAAuD,MAAM,iBAAiB,CAAC;AAe9G,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AAErF,SAAS,WAAW,CAAC,MAAc,EAAE,IAAa;IAC9C,MAAM,GAAG,GAAG;QACR,GAAG,OAAO,CAAC,GAAG;QACd,cAAc,EAAE,MAAM;QACtB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClE,CAAC;IACF,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;IAC3C,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;IACxC,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;IACzC,IAAI,IAAI,EAAE,CAAC;QACP,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE;QACpB,GAAG,EAAE,WAAW;QAChB,GAAG;QACH,KAAK,EAAE,SAAS;QAChB,KAAK,EAAE,KAAK;KACf,CAAC,CAAC;AACP,CAAC;AAED,KAAK,UAAU,uBAAuB,CAAC,MAAc;IACjD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,mBAAmB,CAAC,CAAC;IAClE,MAAM,SAAS,CAAC,GAAG,EAAE,6BAA6B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAC3F,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,UAA8B,EAAE;IAC9D,MAAM,GAAG,GAAG,MAAM,sBAAsB,CAAC;QACrC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,WAAW;QACjC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI;QAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;KAC7B,CAAC,CAAC;IAEH,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,KAAK,KAAK,CAAC;IAC7C,IAAI,KAA+B,CAAC;IACpC,IAAI,QAA4B,CAAC;IAEjC,IAAI,YAAY,EAAE,CAAC;QACf,MAAM,uBAAuB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvC,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAChD,QAAQ,GAAG,OAAO,CAAC,SAAS;YACxB,CAAC,CAAC,oBAAoB,OAAO,CAAC,SAAS,EAAE;YACzC,CAAC,CAAC,qEAAqE,CAAC;IAChF,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,IAAmB,EAAE;QACvC,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YACzB,KAAK,CAAC,IAAI,EAAE,CAAC;QACjB,CAAC;QACD,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC,CAAC;IAEF,OAAO,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AACjD,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,YAAY,CAAC,UAA8B,EAAE;IACzD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,aAAa;IACzB,OAAO,0BAA0B,CAAC;AACtC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
2
|
+
"name": "@dxo/studio",
|
|
3
|
+
"version": "0.0.10",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "DXO training workbench — VMZ WebUI + launcher; desktop GUI is dxo-studio.exe",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"public",
|
|
19
|
+
"designs",
|
|
20
|
+
"vmz.config.ts",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"build:ui": "vmz build --release --profile static .",
|
|
26
|
+
"dev": "vmz dev .",
|
|
27
|
+
"check": "vmz check ."
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@dxo/core": "0.0.10",
|
|
31
|
+
"@dxo/graph": "0.0.10",
|
|
32
|
+
"@dxo/inspect": "0.0.10",
|
|
33
|
+
"@vmz/core": "^0.1.12"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"dxo",
|
|
37
|
+
"studio",
|
|
38
|
+
"tensorboard",
|
|
39
|
+
"vmz",
|
|
40
|
+
"tauri",
|
|
41
|
+
"typescript",
|
|
42
|
+
"deep-learning"
|
|
43
|
+
],
|
|
44
|
+
"homepage": "https://github.com/ai4waifu/dxo-framework#readme",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/ai4waifu/dxo-framework.git",
|
|
48
|
+
"directory": "projects/runtimes/dxo-studio"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/ai4waifu/dxo-framework/issues"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
10
56
|
}
|
package/src/lib/api.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Default Studio API base (loopback). Override with window.__DXO_STUDIO_API__. */
|
|
2
|
+
export function studioApiBase(): string {
|
|
3
|
+
const w = globalThis as typeof globalThis & { __DXO_STUDIO_API__?: string };
|
|
4
|
+
if (typeof w.__DXO_STUDIO_API__ === 'string' && w.__DXO_STUDIO_API__.length > 0) {
|
|
5
|
+
return w.__DXO_STUDIO_API__.replace(/\/$/, '');
|
|
6
|
+
}
|
|
7
|
+
return 'http://127.0.0.1:4310';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function apiGet<T>(path: string): Promise<T> {
|
|
11
|
+
const url = `${studioApiBase()}${path.startsWith('/') ? path : `/${path}`}`;
|
|
12
|
+
const res = await fetch(url);
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
throw new Error(`Studio API ${res.status} for ${path}`);
|
|
15
|
+
}
|
|
16
|
+
return (await res.json()) as T;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Loopback URL for run artifact bytes (images, etc.). */
|
|
20
|
+
export function fileUrl(runId: string, rel: string): string {
|
|
21
|
+
const encoded = rel.split('/').map(encodeURIComponent).join('/');
|
|
22
|
+
return `${studioApiBase()}/api/runs/${encodeURIComponent(runId)}/files/${encoded}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Default `@dxo/ui` model-app serve base for Playground. */
|
|
26
|
+
export function modelAppBase(): string {
|
|
27
|
+
const w = globalThis as typeof globalThis & { __DXO_MODEL_APP__?: string };
|
|
28
|
+
if (typeof w.__DXO_MODEL_APP__ === 'string' && w.__DXO_MODEL_APP__.length > 0) {
|
|
29
|
+
return w.__DXO_MODEL_APP__.replace(/\/$/, '');
|
|
30
|
+
}
|
|
31
|
+
return 'http://127.0.0.1:7860';
|
|
32
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<main class="studio">
|
|
3
|
+
<header class="studio__header">
|
|
4
|
+
<a class="studio__brand" href="/"><strong>DXO Studio</strong></a>
|
|
5
|
+
<nav class="studio__nav">
|
|
6
|
+
<a href="/">Runs</a>
|
|
7
|
+
<a href="/compare">Compare</a>
|
|
8
|
+
<a href="/playground">Playground</a>
|
|
9
|
+
</nav>
|
|
10
|
+
</header>
|
|
11
|
+
|
|
12
|
+
<section class="studio__panel">
|
|
13
|
+
<h1>Compare runs</h1>
|
|
14
|
+
<p class="studio__hint">Select two or more runs to overlay scalar metrics.</p>
|
|
15
|
+
<p class="studio__status">{status}</p>
|
|
16
|
+
|
|
17
|
+
<ul class="studio__list studio__pick-list">
|
|
18
|
+
<li each={runs} as="run" key={run.runId}>
|
|
19
|
+
<label>
|
|
20
|
+
<input type="checkbox" checked={isSelected(run.runId)} onChange={() => toggleRun(run.runId)} />
|
|
21
|
+
{runLabel(run)}
|
|
22
|
+
<span class="studio__badge">{run.meta.status}</span>
|
|
23
|
+
</label>
|
|
24
|
+
</li>
|
|
25
|
+
</ul>
|
|
26
|
+
|
|
27
|
+
<button class="studio__btn" onClick={loadCompare} disabled={selected.length < 2}>Compare</button>
|
|
28
|
+
|
|
29
|
+
<div class="studio__compare" if={compareRows.length > 0}>
|
|
30
|
+
<h2>Metric overlay</h2>
|
|
31
|
+
<table class="studio__table">
|
|
32
|
+
<thead>
|
|
33
|
+
<tr>
|
|
34
|
+
<th>run</th>
|
|
35
|
+
<th>metric</th>
|
|
36
|
+
<th>step</th>
|
|
37
|
+
<th>value</th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody>
|
|
41
|
+
<tr each={compareRows} as="row" key={row.key}>
|
|
42
|
+
<td>{row.runLabel}</td>
|
|
43
|
+
<td>{row.name}</td>
|
|
44
|
+
<td>{row.step}</td>
|
|
45
|
+
<td>{row.value}</td>
|
|
46
|
+
</tr>
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
</main>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script client>
|
|
55
|
+
import { apiGet } from '../lib/api.ts';
|
|
56
|
+
|
|
57
|
+
export default class ComparePage {
|
|
58
|
+
status = 'loading…';
|
|
59
|
+
runs = [];
|
|
60
|
+
selected = [];
|
|
61
|
+
compareRows = [];
|
|
62
|
+
|
|
63
|
+
runLabel(run) {
|
|
64
|
+
return run.meta?.label ?? run.runId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
isSelected(runId) {
|
|
68
|
+
return this.selected.includes(runId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
toggleRun(runId) {
|
|
72
|
+
if (this.selected.includes(runId)) {
|
|
73
|
+
this.selected = this.selected.filter((id) => id !== runId);
|
|
74
|
+
} else {
|
|
75
|
+
this.selected = [...this.selected, runId];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async onMount() {
|
|
80
|
+
try {
|
|
81
|
+
const data = await apiGet('/api/runs');
|
|
82
|
+
this.runs = data.runs ?? [];
|
|
83
|
+
this.selected = this.runs.slice(0, Math.min(2, this.runs.length)).map((r) => r.runId);
|
|
84
|
+
this.status = `${this.runs.length} run(s) available`;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
this.status = err instanceof Error ? err.message : String(err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async loadCompare() {
|
|
91
|
+
if (this.selected.length < 2) return;
|
|
92
|
+
this.status = 'loading compare…';
|
|
93
|
+
try {
|
|
94
|
+
const q = encodeURIComponent(this.selected.join(','));
|
|
95
|
+
const data = await apiGet(`/api/compare?runs=${q}`);
|
|
96
|
+
const rows = [];
|
|
97
|
+
for (const s of data.series ?? []) {
|
|
98
|
+
const label = s.meta?.label ?? s.runId;
|
|
99
|
+
for (const m of s.metrics ?? []) {
|
|
100
|
+
rows.push({
|
|
101
|
+
key: `${s.runId}:${m.name}:${m.step}`,
|
|
102
|
+
runLabel: label,
|
|
103
|
+
name: m.name,
|
|
104
|
+
step: m.step ?? '—',
|
|
105
|
+
value: Number(m.value).toFixed(6),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.compareRows = rows;
|
|
110
|
+
this.status = rows.length ? `${rows.length} metric point(s)` : 'no metrics in selected runs';
|
|
111
|
+
} catch (err) {
|
|
112
|
+
this.status = err instanceof Error ? err.message : String(err);
|
|
113
|
+
this.compareRows = [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<main class="studio">
|
|
3
|
+
<header class="studio__header">
|
|
4
|
+
<a class="studio__brand" href="/"><strong>DXO Studio</strong></a>
|
|
5
|
+
<nav class="studio__nav">
|
|
6
|
+
<a href="/">Runs</a>
|
|
7
|
+
<a href="/compare">Compare</a>
|
|
8
|
+
<a href="/playground">Playground</a>
|
|
9
|
+
</nav>
|
|
10
|
+
</header>
|
|
11
|
+
|
|
12
|
+
<section class="studio__panel">
|
|
13
|
+
<h1>Runs</h1>
|
|
14
|
+
<p class="studio__hint">Append-only inspect store — refresh reloads history after the trainer exits.</p>
|
|
15
|
+
<p class="studio__status">{status}</p>
|
|
16
|
+
<ul class="studio__list">
|
|
17
|
+
<li each={runs} as="run" key={run.runId}>
|
|
18
|
+
<a href={`/runs/${run.runId}`}>{runLabel(run)}</a>
|
|
19
|
+
<span class="studio__badge">{run.meta.status}</span>
|
|
20
|
+
<span class="studio__meta">{formatTime(run.meta.startedAtMs)}</span>
|
|
21
|
+
</li>
|
|
22
|
+
</ul>
|
|
23
|
+
</section>
|
|
24
|
+
</main>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script client>
|
|
28
|
+
import { apiGet } from '../lib/api.ts';
|
|
29
|
+
|
|
30
|
+
export default class RunsPage {
|
|
31
|
+
status = 'loading…';
|
|
32
|
+
runs = [];
|
|
33
|
+
|
|
34
|
+
runLabel(run) {
|
|
35
|
+
return run.meta?.label ?? run.runId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
formatTime(ms) {
|
|
39
|
+
try {
|
|
40
|
+
return new Date(ms).toLocaleString();
|
|
41
|
+
} catch {
|
|
42
|
+
return String(ms);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async onMount() {
|
|
47
|
+
try {
|
|
48
|
+
const data = await apiGet('/api/runs');
|
|
49
|
+
this.runs = data.runs ?? [];
|
|
50
|
+
this.status =
|
|
51
|
+
this.runs.length === 0
|
|
52
|
+
? 'No runs yet. Train with inspect RunRecorder, then refresh.'
|
|
53
|
+
: `${this.runs.length} run(s)`;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
this.status = err instanceof Error ? err.message : String(err);
|
|
56
|
+
this.runs = [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<main class="studio">
|
|
3
|
+
<header class="studio__header">
|
|
4
|
+
<a class="studio__brand" href="/"><strong>DXO Studio</strong></a>
|
|
5
|
+
<nav class="studio__nav">
|
|
6
|
+
<a href="/">Runs</a>
|
|
7
|
+
<a href="/compare">Compare</a>
|
|
8
|
+
<a href="/playground">Playground</a>
|
|
9
|
+
</nav>
|
|
10
|
+
</header>
|
|
11
|
+
|
|
12
|
+
<section class="studio__panel studio__upload">
|
|
13
|
+
<h1>Image playground</h1>
|
|
14
|
+
<p class="studio__hint">
|
|
15
|
+
Upload an image to a loopback `@dxo/ui` model app (`defineModelApp`). Start serve separately on {appBase}.
|
|
16
|
+
</p>
|
|
17
|
+
<p class="studio__status">{status}</p>
|
|
18
|
+
|
|
19
|
+
<label class="studio__file">
|
|
20
|
+
<span>Image file</span>
|
|
21
|
+
<input type="file" accept="image/*" onChange={onFile} />
|
|
22
|
+
</label>
|
|
23
|
+
|
|
24
|
+
<button class="studio__btn" onClick={predict} disabled={!file}>Predict</button>
|
|
25
|
+
<button class="studio__btn studio__btn--ghost" onClick={cancel} disabled={!busy}>Cancel</button>
|
|
26
|
+
|
|
27
|
+
<div if={previewUrl} class="studio__preview">
|
|
28
|
+
<img src={previewUrl} alt="upload preview" />
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<pre class="studio__pre" if={resultText}>{resultText}</pre>
|
|
32
|
+
</section>
|
|
33
|
+
</main>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script client>
|
|
37
|
+
import { modelAppBase } from '../lib/api.ts';
|
|
38
|
+
|
|
39
|
+
export default class PlaygroundPage {
|
|
40
|
+
status = 'ready';
|
|
41
|
+
appBase = modelAppBase();
|
|
42
|
+
file = null;
|
|
43
|
+
previewUrl = '';
|
|
44
|
+
resultText = '';
|
|
45
|
+
busy = false;
|
|
46
|
+
#controller = null;
|
|
47
|
+
|
|
48
|
+
onFile(e) {
|
|
49
|
+
const input = e.target;
|
|
50
|
+
const f = input?.files?.[0];
|
|
51
|
+
this.file = f ?? null;
|
|
52
|
+
if (this.previewUrl) URL.revokeObjectURL(this.previewUrl);
|
|
53
|
+
this.previewUrl = f ? URL.createObjectURL(f) : '';
|
|
54
|
+
this.resultText = '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async predict() {
|
|
58
|
+
if (!this.file) return;
|
|
59
|
+
this.#controller?.abort();
|
|
60
|
+
this.#controller = new AbortController();
|
|
61
|
+
this.busy = true;
|
|
62
|
+
this.status = 'predicting…';
|
|
63
|
+
this.resultText = '';
|
|
64
|
+
try {
|
|
65
|
+
const form = new FormData();
|
|
66
|
+
form.append('image', this.file);
|
|
67
|
+
const res = await fetch(`${this.appBase}/run`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: form,
|
|
70
|
+
signal: this.#controller.signal,
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) throw new Error(`model app ${res.status}`);
|
|
73
|
+
const body = await res.json();
|
|
74
|
+
this.resultText = JSON.stringify(body, null, 2);
|
|
75
|
+
this.status = 'done';
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
78
|
+
this.status = 'cancelled';
|
|
79
|
+
} else {
|
|
80
|
+
this.status = err instanceof Error ? err.message : String(err);
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
this.busy = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
cancel() {
|
|
88
|
+
this.#controller?.abort();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
</script>
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<main class="studio">
|
|
3
|
+
<header class="studio__header">
|
|
4
|
+
<a class="studio__brand" href="/"><strong>DXO Studio</strong></a>
|
|
5
|
+
<nav class="studio__nav">
|
|
6
|
+
<a href="/">Runs</a>
|
|
7
|
+
<a href="/compare">Compare</a>
|
|
8
|
+
<a href="/playground">Playground</a>
|
|
9
|
+
</nav>
|
|
10
|
+
</header>
|
|
11
|
+
|
|
12
|
+
<section class="studio__panel">
|
|
13
|
+
<h1>Run {runId}</h1>
|
|
14
|
+
<p class="studio__status">{status}</p>
|
|
15
|
+
|
|
16
|
+
<h2>Metrics</h2>
|
|
17
|
+
<table class="studio__table">
|
|
18
|
+
<thead>
|
|
19
|
+
<tr><th>name</th><th>step</th><th>value</th></tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
<tr each={metrics} as="m" key={metricKey(m)}>
|
|
23
|
+
<td>{m.name}</td>
|
|
24
|
+
<td>{m.step ?? '—'}</td>
|
|
25
|
+
<td>{formatValue(m.value)}</td>
|
|
26
|
+
</tr>
|
|
27
|
+
</tbody>
|
|
28
|
+
</table>
|
|
29
|
+
|
|
30
|
+
<h2>Confusion matrix</h2>
|
|
31
|
+
<p class="studio__hint" if={!hasMatrix}>No confusion-matrix artifact.</p>
|
|
32
|
+
<table class="studio__table studio__matrix" if={hasMatrix}>
|
|
33
|
+
<thead>
|
|
34
|
+
<tr>
|
|
35
|
+
<th></th>
|
|
36
|
+
<th each={matrixLabels} as="lbl" key={lbl}>{lbl}</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody>
|
|
40
|
+
<tr each={matrixRows} as="row" key={row.label}>
|
|
41
|
+
<th>{row.label}</th>
|
|
42
|
+
<td each={row.cells} as="cell" key={cell.key} class={cellClass(cell.value)}>{cell.value}</td>
|
|
43
|
+
</tr>
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
|
|
47
|
+
<h2>Image samples</h2>
|
|
48
|
+
<p class="studio__hint" if={samples.length === 0}>No image-samples artifact.</p>
|
|
49
|
+
<ul class="studio__samples" if={samples.length > 0}>
|
|
50
|
+
<li each={samples} as="s" key={s.uri}>
|
|
51
|
+
<img src={s.src} alt={s.caption} />
|
|
52
|
+
<span class="studio__meta">{s.caption}</span>
|
|
53
|
+
</li>
|
|
54
|
+
</ul>
|
|
55
|
+
|
|
56
|
+
<h2>Training events</h2>
|
|
57
|
+
<ul class="studio__list studio__list--compact">
|
|
58
|
+
<li each={trainEvents} as="e" key={eventKey(e)}><code>{e.type}</code> {summarize(e)}</li>
|
|
59
|
+
</ul>
|
|
60
|
+
|
|
61
|
+
<h2>Checkpoints</h2>
|
|
62
|
+
<ul class="studio__list">
|
|
63
|
+
<li each={checkpoints} as="c" key={c.uri}><code>{c.name}</code> · {c.uri}</li>
|
|
64
|
+
</ul>
|
|
65
|
+
|
|
66
|
+
<h2>Models</h2>
|
|
67
|
+
<p class="studio__hint">Module · Execution · Kernel are separate views — never one merged architecture diagram.</p>
|
|
68
|
+
<div class="studio__tabs">
|
|
69
|
+
<button class={tabClass('module')} onClick={() => selectGraphTab('module')}>Module</button>
|
|
70
|
+
<button class={tabClass('execution')} onClick={() => selectGraphTab('execution')}>Execution</button>
|
|
71
|
+
<button class={tabClass('kernel')} onClick={() => selectGraphTab('kernel')}>Kernel</button>
|
|
72
|
+
</div>
|
|
73
|
+
<p class="studio__meta" if={graphAvailability}>{graphAvailability}</p>
|
|
74
|
+
<pre class="studio__pre">{graphText}</pre>
|
|
75
|
+
|
|
76
|
+
<h2>Profiler</h2>
|
|
77
|
+
<p class="studio__hint" if={!hasProfile}>{profileHint}</p>
|
|
78
|
+
<ul class="studio__list studio__list--compact" if={hasProfile}>
|
|
79
|
+
<li each={profileSpans} as="s" key={spanKey(s)}>
|
|
80
|
+
<code>{s.category}</code> {s.name} · {s.startMs}–{s.endMs} ms
|
|
81
|
+
</li>
|
|
82
|
+
</ul>
|
|
83
|
+
</section>
|
|
84
|
+
</main>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<script client>
|
|
88
|
+
import { apiGet, fileUrl } from '../../lib/api.ts';
|
|
89
|
+
|
|
90
|
+
export default class RunDetailPage {
|
|
91
|
+
runId = '';
|
|
92
|
+
status = 'loading…';
|
|
93
|
+
metrics = [];
|
|
94
|
+
trainEvents = [];
|
|
95
|
+
checkpoints = [];
|
|
96
|
+
graphTab = 'module';
|
|
97
|
+
graphBundle = null;
|
|
98
|
+
graphText = '(none)';
|
|
99
|
+
graphAvailability = '';
|
|
100
|
+
hasMatrix = false;
|
|
101
|
+
matrixLabels = [];
|
|
102
|
+
matrixRows = [];
|
|
103
|
+
samples = [];
|
|
104
|
+
hasProfile = false;
|
|
105
|
+
profileHint = 'No profile-trace artifact (Titan spans required; timeline is never fabricated).';
|
|
106
|
+
profileSpans = [];
|
|
107
|
+
|
|
108
|
+
metricKey(m) {
|
|
109
|
+
return `${m.name}:${m.step ?? ''}:${m.wallTimeMs ?? ''}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
eventKey(e) {
|
|
113
|
+
return `${e.type}:${e.wallTimeMs ?? ''}:${e.step ?? ''}:${e.epoch ?? ''}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
spanKey(s) {
|
|
117
|
+
return `${s.category}:${s.name}:${s.startMs}:${s.endMs}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
tabClass(tab) {
|
|
121
|
+
return this.graphTab === tab ? 'studio__tab studio__tab--active' : 'studio__tab';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
selectGraphTab(tab) {
|
|
125
|
+
this.graphTab = tab;
|
|
126
|
+
this.renderGraphTab();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
renderGraphTab() {
|
|
130
|
+
const bundle = this.graphBundle;
|
|
131
|
+
if (!bundle) {
|
|
132
|
+
this.graphText = '(no model-graph artifact)';
|
|
133
|
+
this.graphAvailability = '';
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const g = bundle[this.graphTab] ?? bundle.module;
|
|
137
|
+
const avail = g.availability === 'unavailable'
|
|
138
|
+
? `unavailable: ${g.unavailableReason ?? 'n/a'}`
|
|
139
|
+
: `view=${g.view} · nodes=${(g.nodes ?? []).length}`;
|
|
140
|
+
this.graphAvailability = avail;
|
|
141
|
+
this.graphText = JSON.stringify(g, null, 2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
formatValue(v) {
|
|
145
|
+
return Number(v).toFixed(6);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
cellClass(v) {
|
|
149
|
+
const n = Number(v);
|
|
150
|
+
if (n >= 5) return 'studio__cell--hot';
|
|
151
|
+
if (n >= 2) return 'studio__cell--warm';
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
summarize(e) {
|
|
156
|
+
if (e.type === 'train/batch') return `epoch=${e.epoch} step=${e.step} loss=${e.loss}`;
|
|
157
|
+
if (e.type === 'train/epoch_end') return `epoch=${e.epoch} meanLoss=${e.meanLoss}`;
|
|
158
|
+
if (e.type === 'run/end') return `status=${e.status}`;
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
buildMatrix(data) {
|
|
163
|
+
const labels = data.labels ?? [];
|
|
164
|
+
const matrix = data.matrix ?? [];
|
|
165
|
+
this.matrixLabels = labels;
|
|
166
|
+
this.matrixRows = labels.map((label, i) => ({
|
|
167
|
+
label,
|
|
168
|
+
cells: (matrix[i] ?? []).map((value, j) => ({
|
|
169
|
+
key: `${i}:${j}`,
|
|
170
|
+
value,
|
|
171
|
+
})),
|
|
172
|
+
}));
|
|
173
|
+
this.hasMatrix = labels.length > 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
buildSamples(data) {
|
|
177
|
+
const list = data.samples ?? [];
|
|
178
|
+
this.samples = list.map((s) => {
|
|
179
|
+
const parts = [];
|
|
180
|
+
if (s.label !== undefined) parts.push(`label=${s.label}`);
|
|
181
|
+
if (s.pred !== undefined) parts.push(`pred=${s.pred}`);
|
|
182
|
+
return {
|
|
183
|
+
uri: s.uri,
|
|
184
|
+
src: fileUrl(this.runId, s.uri),
|
|
185
|
+
caption: parts.join(' · ') || s.uri,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
normalizeGraphArtifact(raw) {
|
|
191
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
192
|
+
if (raw.format === 'dxo-model-graph-bundle') return raw;
|
|
193
|
+
if (raw.format === 'dxo-model-graph') {
|
|
194
|
+
const emptyExec = {
|
|
195
|
+
format: 'dxo-model-graph',
|
|
196
|
+
version: 0,
|
|
197
|
+
view: 'execution',
|
|
198
|
+
availability: 'unavailable',
|
|
199
|
+
unavailableReason: 'legacy module-only artifact',
|
|
200
|
+
nodes: [],
|
|
201
|
+
edges: [],
|
|
202
|
+
};
|
|
203
|
+
const emptyKernel = {
|
|
204
|
+
format: 'dxo-model-graph',
|
|
205
|
+
version: 0,
|
|
206
|
+
view: 'kernel',
|
|
207
|
+
availability: 'unavailable',
|
|
208
|
+
unavailableReason: 'legacy module-only artifact',
|
|
209
|
+
nodes: [],
|
|
210
|
+
edges: [],
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
format: 'dxo-model-graph-bundle',
|
|
214
|
+
version: 0,
|
|
215
|
+
module: raw.view === 'module' ? raw : {
|
|
216
|
+
format: 'dxo-model-graph',
|
|
217
|
+
version: 0,
|
|
218
|
+
view: 'module',
|
|
219
|
+
availability: 'unavailable',
|
|
220
|
+
unavailableReason: 'legacy artifact was not module view',
|
|
221
|
+
nodes: [],
|
|
222
|
+
edges: [],
|
|
223
|
+
},
|
|
224
|
+
execution: raw.view === 'execution' ? raw : emptyExec,
|
|
225
|
+
kernel: raw.view === 'kernel' ? raw : emptyKernel,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async onMount() {
|
|
232
|
+
const parts = location.pathname.split('/').filter(Boolean);
|
|
233
|
+
this.runId = parts[1] ?? '';
|
|
234
|
+
if (!this.runId) {
|
|
235
|
+
this.status = 'missing run id';
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const [metaRes, metricsRes, eventsRes, ckRes] = await Promise.all([
|
|
240
|
+
apiGet(`/api/runs/${encodeURIComponent(this.runId)}/meta`),
|
|
241
|
+
apiGet(`/api/runs/${encodeURIComponent(this.runId)}/metrics`),
|
|
242
|
+
apiGet(`/api/runs/${encodeURIComponent(this.runId)}/events`),
|
|
243
|
+
apiGet(`/api/runs/${encodeURIComponent(this.runId)}/checkpoints`),
|
|
244
|
+
]);
|
|
245
|
+
this.status = metaRes.meta?.status ?? 'unknown';
|
|
246
|
+
this.metrics = metricsRes.metrics ?? [];
|
|
247
|
+
this.trainEvents = (eventsRes.events ?? []).filter(
|
|
248
|
+
(e) => String(e.type).startsWith('train/') || e.type === 'run/start' || e.type === 'run/end',
|
|
249
|
+
);
|
|
250
|
+
this.checkpoints = ckRes.checkpoints ?? [];
|
|
251
|
+
try {
|
|
252
|
+
const g = await apiGet(`/api/runs/${encodeURIComponent(this.runId)}/model-graph`);
|
|
253
|
+
this.graphBundle = this.normalizeGraphArtifact(g.graph ?? g);
|
|
254
|
+
this.renderGraphTab();
|
|
255
|
+
} catch {
|
|
256
|
+
this.graphBundle = null;
|
|
257
|
+
this.renderGraphTab();
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const cm = await apiGet(`/api/runs/${encodeURIComponent(this.runId)}/confusion-matrix`);
|
|
261
|
+
this.buildMatrix(cm.confusionMatrix ?? cm);
|
|
262
|
+
} catch {
|
|
263
|
+
this.hasMatrix = false;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const imgs = await apiGet(`/api/runs/${encodeURIComponent(this.runId)}/image-samples`);
|
|
267
|
+
this.buildSamples(imgs.imageSamples ?? imgs);
|
|
268
|
+
} catch {
|
|
269
|
+
this.samples = [];
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const pr = await apiGet(`/api/runs/${encodeURIComponent(this.runId)}/profile-trace`);
|
|
273
|
+
const trace = pr.profileTrace ?? pr.trace ?? pr;
|
|
274
|
+
const spans = (trace.spans ?? []).filter(
|
|
275
|
+
(s) => s && (s.category === 'kernel' || s.category === 'transfer' || s.category === 'readback' || s.category === 'op'),
|
|
276
|
+
);
|
|
277
|
+
if (trace.availability === 'unavailable' || spans.length === 0) {
|
|
278
|
+
this.hasProfile = false;
|
|
279
|
+
this.profileHint = trace.unavailableReason
|
|
280
|
+
?? 'ProfileTrace unavailable (no Titan spans; timeline not fabricated).';
|
|
281
|
+
this.profileSpans = [];
|
|
282
|
+
} else {
|
|
283
|
+
this.hasProfile = true;
|
|
284
|
+
this.profileSpans = spans;
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
this.hasProfile = false;
|
|
288
|
+
this.profileHint = 'No profile-trace artifact (Titan spans required; timeline is never fabricated).';
|
|
289
|
+
}
|
|
290
|
+
} catch (err) {
|
|
291
|
+
this.status = err instanceof Error ? err.message : String(err);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
</script>
|
package/vmz.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from '@vmz/vmz';
|
|
2
|
+
|
|
3
|
+
/** Studio WebUI — VMZ dev watch (`dxo studio`) + static build for Tauri embed. */
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
delivery: {
|
|
6
|
+
default: 'node',
|
|
7
|
+
profiles: {
|
|
8
|
+
node: { host: 'node', assembly: 'node-ssr' },
|
|
9
|
+
static: { host: 'browser', assembly: 'static-cdn' },
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|