lastriko 0.1.2 → 0.1.7
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 +1 -1
- package/dist/__tests__/integration/ws-flow.integration.test.js +38 -140
- package/dist/__tests__/integration/ws-flow.integration.test.js.map +1 -1
- package/dist/client/events.js +169 -49
- package/dist/client/events.js.map +1 -1
- package/dist/client/swap.d.ts +14 -1
- package/dist/client/swap.js +185 -7
- package/dist/client/swap.js.map +1 -1
- package/dist/client/ws.d.ts +1 -0
- package/dist/client/ws.js +214 -35
- package/dist/client/ws.js.map +1 -1
- package/dist/client/ws.test.js +6 -146
- package/dist/client/ws.test.js.map +1 -1
- package/dist/components/context.d.ts +14 -2
- package/dist/components/context.js +330 -5
- package/dist/components/context.js.map +1 -1
- package/dist/components/context.test.js +74 -56
- package/dist/components/context.test.js.map +1 -1
- package/dist/components/registry.d.ts +2 -2
- package/dist/components/registry.js +52 -4
- package/dist/components/registry.js.map +1 -1
- package/dist/components/types.d.ts +180 -4
- package/dist/engine/code-highlight.d.ts +1 -0
- package/dist/engine/code-highlight.js +64 -0
- package/dist/engine/code-highlight.js.map +1 -0
- package/dist/engine/code-highlight.test.d.ts +1 -0
- package/dist/engine/code-highlight.test.js +15 -0
- package/dist/engine/code-highlight.test.js.map +1 -0
- package/dist/engine/executor.js +16 -0
- package/dist/engine/executor.js.map +1 -1
- package/dist/engine/executor.test.js +33 -4
- package/dist/engine/executor.test.js.map +1 -1
- package/dist/engine/messages.d.ts +9 -1
- package/dist/engine/messages.js.map +1 -1
- package/dist/engine/messages.test.d.ts +1 -0
- package/dist/engine/messages.test.js +39 -0
- package/dist/engine/messages.test.js.map +1 -0
- package/dist/engine/renderer.js +241 -14
- package/dist/engine/renderer.js.map +1 -1
- package/dist/engine/renderer.test.js +230 -202
- package/dist/engine/renderer.test.js.map +1 -1
- package/dist/engine/server.d.ts +2 -0
- package/dist/engine/server.js +57 -6
- package/dist/engine/server.js.map +1 -1
- package/dist/engine/server.test.js +105 -4
- package/dist/engine/server.test.js.map +1 -1
- package/dist/engine/watcher.d.ts +17 -1
- package/dist/engine/watcher.js +42 -5
- package/dist/engine/watcher.js.map +1 -1
- package/dist/engine/watcher.test.d.ts +1 -0
- package/dist/engine/watcher.test.js +62 -0
- package/dist/engine/watcher.test.js.map +1 -0
- package/dist/engine/websocket.d.ts +2 -0
- package/dist/engine/websocket.hub.test.js +24 -0
- package/dist/engine/websocket.hub.test.js.map +1 -1
- package/dist/engine/websocket.js +6 -1
- package/dist/engine/websocket.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/style.css +150 -0
- package/dist/theme/lastriko.css +150 -0
- package/package.json +5 -1
package/dist/client/ws.test.js
CHANGED
|
@@ -1,152 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { applyBatch } from './swap';
|
|
2
3
|
import { createWSManager } from './ws';
|
|
3
|
-
class FakeClassList {
|
|
4
|
-
values = new Set();
|
|
5
|
-
constructor(initial = []) {
|
|
6
|
-
for (const value of initial) {
|
|
7
|
-
this.values.add(value);
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
add(value) {
|
|
11
|
-
this.values.add(value);
|
|
12
|
-
}
|
|
13
|
-
contains(value) {
|
|
14
|
-
return this.values.has(value);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
class FakeElement {
|
|
18
|
-
children = [];
|
|
19
|
-
classList = new FakeClassList();
|
|
20
|
-
textContent = '';
|
|
21
|
-
id = '';
|
|
22
|
-
parent = null;
|
|
23
|
-
currentClassName = '';
|
|
24
|
-
set className(value) {
|
|
25
|
-
this.currentClassName = value;
|
|
26
|
-
for (const cls of value.split(/\s+/).filter(Boolean)) {
|
|
27
|
-
this.classList.add(cls);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
get className() {
|
|
31
|
-
return this.currentClassName;
|
|
32
|
-
}
|
|
33
|
-
get childElementCount() {
|
|
34
|
-
return this.children.length;
|
|
35
|
-
}
|
|
36
|
-
get firstElementChild() {
|
|
37
|
-
return this.children[0] ?? null;
|
|
38
|
-
}
|
|
39
|
-
appendChild(child) {
|
|
40
|
-
child.parent = this;
|
|
41
|
-
this.children.push(child);
|
|
42
|
-
}
|
|
43
|
-
remove() {
|
|
44
|
-
if (!this.parent) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
this.parent.children.splice(this.parent.children.indexOf(this), 1);
|
|
48
|
-
this.parent = null;
|
|
49
|
-
}
|
|
50
|
-
querySelector(selector) {
|
|
51
|
-
const classes = selector
|
|
52
|
-
.split('.')
|
|
53
|
-
.filter(Boolean);
|
|
54
|
-
if (classes.length === 0) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
const stack = [...this.children];
|
|
58
|
-
while (stack.length > 0) {
|
|
59
|
-
const current = stack.shift();
|
|
60
|
-
const allMatch = classes.every((cls) => current.classList.contains(cls));
|
|
61
|
-
if (allMatch) {
|
|
62
|
-
return current;
|
|
63
|
-
}
|
|
64
|
-
stack.push(...current.children);
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function setupDom() {
|
|
70
|
-
const previousWindow = globalThis.window;
|
|
71
|
-
const previousDocument = globalThis.document;
|
|
72
|
-
const root = new FakeElement();
|
|
73
|
-
root.id = 'lk-root';
|
|
74
|
-
const toastRoot = new FakeElement();
|
|
75
|
-
toastRoot.id = 'lk-toast-root';
|
|
76
|
-
const byId = new Map([
|
|
77
|
-
['lk-root', root],
|
|
78
|
-
['lk-toast-root', toastRoot],
|
|
79
|
-
]);
|
|
80
|
-
const fakeDocument = {
|
|
81
|
-
documentElement: { setAttribute: () => { } },
|
|
82
|
-
addEventListener: () => { },
|
|
83
|
-
getElementById: (id) => byId.get(id) ?? null,
|
|
84
|
-
createElement: () => new FakeElement(),
|
|
85
|
-
};
|
|
86
|
-
const storage = new Map();
|
|
87
|
-
const fakeWindow = {
|
|
88
|
-
location: { protocol: 'http:', host: '127.0.0.1:3500' },
|
|
89
|
-
localStorage: {
|
|
90
|
-
getItem: (key) => storage.get(key) ?? null,
|
|
91
|
-
setItem: (key, value) => {
|
|
92
|
-
storage.set(key, value);
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
setTimeout,
|
|
96
|
-
};
|
|
97
|
-
Object.assign(globalThis, {
|
|
98
|
-
window: fakeWindow,
|
|
99
|
-
document: fakeDocument,
|
|
100
|
-
});
|
|
101
|
-
return {
|
|
102
|
-
document: fakeDocument,
|
|
103
|
-
restore: () => {
|
|
104
|
-
Object.assign(globalThis, {
|
|
105
|
-
window: previousWindow,
|
|
106
|
-
document: previousDocument,
|
|
107
|
-
});
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
4
|
describe('client ws manager', () => {
|
|
112
|
-
it('
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
wsFactory: (() => {
|
|
118
|
-
const socket = {
|
|
119
|
-
onopen: null,
|
|
120
|
-
onmessage: null,
|
|
121
|
-
onclose: null,
|
|
122
|
-
send: () => { },
|
|
123
|
-
close: () => { },
|
|
124
|
-
};
|
|
125
|
-
sockets.push(socket);
|
|
126
|
-
return socket;
|
|
127
|
-
}),
|
|
128
|
-
});
|
|
129
|
-
manager.connect();
|
|
130
|
-
const socketRef = sockets[0];
|
|
131
|
-
expect(socketRef).toBeDefined();
|
|
132
|
-
if (!socketRef) {
|
|
133
|
-
throw new Error('mock socket was not created');
|
|
134
|
-
}
|
|
135
|
-
socketRef.onmessage?.({
|
|
136
|
-
data: JSON.stringify({
|
|
137
|
-
type: 'TOAST',
|
|
138
|
-
payload: { message: 'hello toast', type: 'success', duration: 5000 },
|
|
139
|
-
}),
|
|
140
|
-
});
|
|
141
|
-
const container = document.getElementById('lk-toast-root');
|
|
142
|
-
expect(container).not.toBeNull();
|
|
143
|
-
expect(container?.classList.contains('lk-toast-container')).toBe(true);
|
|
144
|
-
const toast = container?.querySelector('.lk-toast.lk-toast--success');
|
|
145
|
-
expect(toast?.textContent).toBe('hello toast');
|
|
146
|
-
}
|
|
147
|
-
finally {
|
|
148
|
-
restore();
|
|
149
|
-
}
|
|
5
|
+
it('exports createWSManager', () => {
|
|
6
|
+
expect(typeof createWSManager).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
it('exposes batch apply helper', () => {
|
|
9
|
+
expect(typeof applyBatch).toBe('function');
|
|
150
10
|
});
|
|
151
11
|
});
|
|
152
12
|
//# sourceMappingURL=ws.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws.test.js","sourceRoot":"","sources":["../../src/client/ws.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"ws.test.js","sourceRoot":"","sources":["../../src/client/ws.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAEvC,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AlertOpts, ButtonCallbackHandle, ButtonHandle, ButtonOpts, ChatHandle, ChatUIOptions, CodeProps,
|
|
1
|
+
import type { AccordionOpts, AccordionSection, AlertOpts, AudioProps, BeforeAfterOpts, ButtonCallbackHandle, ButtonHandle, ButtonOpts, ChatHandle, ChatUIOptions, CodeProps, ColorPickerHandle, ColorPickerOpts, ConnectionScope, DateInputHandle, DateInputOpts, DiffProps, FileUploadHandle, FileUploadOpts, FilmStripItem, FilmStripOpts, FullscreenHandle, FullscreenOpts, GridOpts, ImageGridProps, ImageProps, LoadingOpts, MetricHandle, MetricOpts, ModelCompareHandle, ModelCompareOpts, ModelSpec, MultiSelectHandle, MultiSelectOpts, NumberInputHandle, NumberInputOpts, ParameterPanelHandle, ParameterPanelOpts, ParameterSchema, ProgressHandle, ProgressOpts, PromptEditorHandle, PromptEditorOpts, SelectHandle, SelectOption, SelectOpts, ShellOpts, ShellRegions, SliderHandle, SliderOpts, StreamHandle, StreamTextOpts, TabDef, TableHandle, TableProps, TableRow, TabsHandle, TabsOpts, TextHandle, TextInputHandle, TextInputOpts, ToastOpts, ToggleHandle, ToggleOpts, UIContext as IUIContext, VideoProps } from './types';
|
|
2
2
|
export declare class UIContext implements IUIContext {
|
|
3
3
|
readonly scope: ConnectionScope;
|
|
4
4
|
constructor(scope: ConnectionScope);
|
|
@@ -17,6 +17,9 @@ export declare class UIContext implements IUIContext {
|
|
|
17
17
|
slider(label: string, opts: SliderOpts): SliderHandle;
|
|
18
18
|
toggle(label: string, opts?: ToggleOpts): ToggleHandle;
|
|
19
19
|
select(label: string, options: SelectOption[], opts?: SelectOpts): SelectHandle;
|
|
20
|
+
multiSelect(label: string, options: SelectOption[], opts?: MultiSelectOpts): MultiSelectHandle;
|
|
21
|
+
colorPicker(label: string, opts?: ColorPickerOpts): ColorPickerHandle;
|
|
22
|
+
dateInput(label: string, opts?: DateInputOpts): DateInputHandle;
|
|
20
23
|
fileUpload(label: string, opts?: FileUploadOpts): FileUploadHandle;
|
|
21
24
|
markdown(content: string): void;
|
|
22
25
|
image(src: string | null | undefined, opts?: Partial<ImageProps>): void;
|
|
@@ -25,18 +28,27 @@ export declare class UIContext implements IUIContext {
|
|
|
25
28
|
alt?: string;
|
|
26
29
|
caption?: string;
|
|
27
30
|
}>, opts?: Partial<ImageGridProps>): void;
|
|
31
|
+
video(src: string, opts?: VideoProps): void;
|
|
32
|
+
audio(src: string, opts?: AudioProps): void;
|
|
28
33
|
code(content: string, opts?: Partial<CodeProps>): void;
|
|
29
34
|
json(data: unknown, opts?: {
|
|
30
35
|
label?: string;
|
|
31
36
|
}): void;
|
|
37
|
+
diff(before: string, after: string, opts?: DiffProps): void;
|
|
32
38
|
table(data: TableRow[], opts?: Partial<TableProps>): TableHandle;
|
|
33
39
|
private createRowHandle;
|
|
34
40
|
metric(label: string, value: string, opts?: MetricOpts): MetricHandle;
|
|
35
41
|
progress(value: number | null, opts?: ProgressOpts): ProgressHandle;
|
|
36
42
|
shell(regions: ShellRegions, opts?: ShellOpts): void;
|
|
37
43
|
grid(areas: Array<(ctx: IUIContext) => void>, opts?: GridOpts): void;
|
|
38
|
-
tabs(tabs: TabDef[], opts?: TabsOpts):
|
|
44
|
+
tabs(tabs: TabDef[], opts?: TabsOpts): TabsHandle;
|
|
39
45
|
card(titleOrContent: string | ((ctx: IUIContext) => void), contentOrNothing?: (ctx: IUIContext) => void): void;
|
|
46
|
+
accordion(sections: AccordionSection[], opts?: AccordionOpts): void;
|
|
47
|
+
fullscreen(content: (ctx: IUIContext) => void, opts?: FullscreenOpts): FullscreenHandle;
|
|
48
|
+
modelCompare(models: ModelSpec[], opts: ModelCompareOpts): ModelCompareHandle;
|
|
49
|
+
parameterPanel(schema: ParameterSchema, opts?: ParameterPanelOpts): ParameterPanelHandle;
|
|
50
|
+
filmStrip(images: Array<string | FilmStripItem>, opts?: FilmStripOpts): void;
|
|
51
|
+
beforeAfter(before: string, after: string, opts?: BeforeAfterOpts): void;
|
|
40
52
|
divider(opts?: {
|
|
41
53
|
label?: string;
|
|
42
54
|
}): void;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createComponentId } from './id';
|
|
2
|
+
function normalizeSelectOptions(options) {
|
|
3
|
+
return options.map((option) => (typeof option === 'string' ? { label: option, value: option } : option));
|
|
4
|
+
}
|
|
2
5
|
export class UIContext {
|
|
3
6
|
scope;
|
|
4
7
|
constructor(scope) {
|
|
@@ -172,7 +175,7 @@ export class UIContext {
|
|
|
172
175
|
return this.register(handle);
|
|
173
176
|
}
|
|
174
177
|
select(label, options, opts = {}) {
|
|
175
|
-
const normalized = options
|
|
178
|
+
const normalized = normalizeSelectOptions(options);
|
|
176
179
|
const initial = opts.default ?? normalized[0]?.value ?? '';
|
|
177
180
|
const id = createComponentId(this.scope, 'select');
|
|
178
181
|
const valueAtom = this.scope.getAtom(`${id}/value`, initial);
|
|
@@ -195,6 +198,80 @@ export class UIContext {
|
|
|
195
198
|
};
|
|
196
199
|
return this.register(handle);
|
|
197
200
|
}
|
|
201
|
+
multiSelect(label, options, opts = {}) {
|
|
202
|
+
const normalized = normalizeSelectOptions(options);
|
|
203
|
+
const initialDefaults = Array.isArray(opts.defaults) ? opts.defaults : [];
|
|
204
|
+
const initial = initialDefaults.filter((value) => normalized.some((option) => option.value === value));
|
|
205
|
+
const id = createComponentId(this.scope, 'multiSelect');
|
|
206
|
+
const valueAtom = this.scope.getAtom(`${id}/value`, initial);
|
|
207
|
+
const handle = {
|
|
208
|
+
id,
|
|
209
|
+
type: 'multiSelect',
|
|
210
|
+
props: { label, options: normalized, value: initial, ...opts },
|
|
211
|
+
get value() {
|
|
212
|
+
return valueAtom.get();
|
|
213
|
+
},
|
|
214
|
+
update: (patch) => {
|
|
215
|
+
if (patch.value !== undefined) {
|
|
216
|
+
let next = Array.isArray(patch.value) ? patch.value.map((value) => String(value)) : [];
|
|
217
|
+
if (typeof handle.props.maxSelections === 'number' && handle.props.maxSelections > 0) {
|
|
218
|
+
next = next.slice(0, handle.props.maxSelections);
|
|
219
|
+
}
|
|
220
|
+
valueAtom.set(next);
|
|
221
|
+
handle.props.value = next;
|
|
222
|
+
}
|
|
223
|
+
Object.assign(handle.props, patch);
|
|
224
|
+
this.scope.pushFragment(handle);
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
return this.register(handle);
|
|
228
|
+
}
|
|
229
|
+
colorPicker(label, opts = {}) {
|
|
230
|
+
const id = createComponentId(this.scope, 'colorPicker');
|
|
231
|
+
const initial = typeof opts.default === 'string' && opts.default.trim() ? opts.default : '#e94560';
|
|
232
|
+
const valueAtom = this.scope.getAtom(`${id}/value`, initial);
|
|
233
|
+
const handle = {
|
|
234
|
+
id,
|
|
235
|
+
type: 'colorPicker',
|
|
236
|
+
props: { label, value: initial, format: opts.format ?? 'hex', ...opts },
|
|
237
|
+
get value() {
|
|
238
|
+
return valueAtom.get();
|
|
239
|
+
},
|
|
240
|
+
update: (patch) => {
|
|
241
|
+
if (patch.value !== undefined) {
|
|
242
|
+
const next = String(patch.value || '#000000');
|
|
243
|
+
valueAtom.set(next);
|
|
244
|
+
handle.props.value = next;
|
|
245
|
+
}
|
|
246
|
+
Object.assign(handle.props, patch);
|
|
247
|
+
this.scope.pushFragment(handle);
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
return this.register(handle);
|
|
251
|
+
}
|
|
252
|
+
dateInput(label, opts = {}) {
|
|
253
|
+
const id = createComponentId(this.scope, 'dateInput');
|
|
254
|
+
const initial = opts.default ?? '';
|
|
255
|
+
const valueAtom = this.scope.getAtom(`${id}/value`, initial);
|
|
256
|
+
const handle = {
|
|
257
|
+
id,
|
|
258
|
+
type: 'dateInput',
|
|
259
|
+
props: { label, value: initial, type: opts.type ?? 'date', ...opts },
|
|
260
|
+
get value() {
|
|
261
|
+
return valueAtom.get();
|
|
262
|
+
},
|
|
263
|
+
update: (patch) => {
|
|
264
|
+
if (patch.value !== undefined) {
|
|
265
|
+
const next = String(patch.value);
|
|
266
|
+
valueAtom.set(next);
|
|
267
|
+
handle.props.value = next;
|
|
268
|
+
}
|
|
269
|
+
Object.assign(handle.props, patch);
|
|
270
|
+
this.scope.pushFragment(handle);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
return this.register(handle);
|
|
274
|
+
}
|
|
198
275
|
fileUpload(label, opts = {}) {
|
|
199
276
|
const id = createComponentId(this.scope, 'fileUpload');
|
|
200
277
|
const initial = null;
|
|
@@ -243,12 +320,46 @@ export class UIContext {
|
|
|
243
320
|
minWidth: opts.minWidth,
|
|
244
321
|
});
|
|
245
322
|
}
|
|
323
|
+
video(src, opts = { src: '' }) {
|
|
324
|
+
const autoplay = Boolean(opts.autoplay);
|
|
325
|
+
const muted = autoplay ? true : Boolean(opts.muted);
|
|
326
|
+
this.createPassiveHandle('video', {
|
|
327
|
+
src,
|
|
328
|
+
controls: opts.controls ?? true,
|
|
329
|
+
autoplay,
|
|
330
|
+
muted,
|
|
331
|
+
loop: opts.loop ?? false,
|
|
332
|
+
poster: opts.poster,
|
|
333
|
+
width: opts.width ?? '100%',
|
|
334
|
+
caption: opts.caption,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
audio(src, opts = { src: '' }) {
|
|
338
|
+
this.createPassiveHandle('audio', {
|
|
339
|
+
src,
|
|
340
|
+
controls: opts.controls ?? true,
|
|
341
|
+
autoplay: opts.autoplay ?? false,
|
|
342
|
+
loop: opts.loop ?? false,
|
|
343
|
+
label: opts.label,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
246
346
|
code(content, opts = {}) {
|
|
247
347
|
this.createPassiveHandle('code', { content, lang: opts.lang });
|
|
248
348
|
}
|
|
249
349
|
json(data, opts) {
|
|
250
350
|
this.createPassiveHandle('json', { data, label: opts?.label });
|
|
251
351
|
}
|
|
352
|
+
diff(before, after, opts = { before: '', after: '' }) {
|
|
353
|
+
this.createPassiveHandle('diff', {
|
|
354
|
+
before,
|
|
355
|
+
after,
|
|
356
|
+
mode: opts.mode ?? 'split',
|
|
357
|
+
beforeLabel: opts.beforeLabel ?? 'Before',
|
|
358
|
+
afterLabel: opts.afterLabel ?? 'After',
|
|
359
|
+
lang: opts.lang,
|
|
360
|
+
context: opts.context,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
252
363
|
table(data, opts = {}) {
|
|
253
364
|
const id = createComponentId(this.scope, 'table');
|
|
254
365
|
const columns = opts.columns ?? Object.keys(data[0] ?? {});
|
|
@@ -380,8 +491,7 @@ export class UIContext {
|
|
|
380
491
|
}
|
|
381
492
|
tabs(tabs, opts = {}) {
|
|
382
493
|
const id = createComponentId(this.scope, 'tabs');
|
|
383
|
-
const
|
|
384
|
-
const valueAtom = this.scope.getAtom(`${id}/value`, initial);
|
|
494
|
+
const requestedDefault = opts.defaultTab ?? tabs[0]?.label ?? '';
|
|
385
495
|
const renderedTabs = tabs.map((tab) => {
|
|
386
496
|
const before = this.scope.listHandles().length;
|
|
387
497
|
tab.content(this);
|
|
@@ -391,6 +501,12 @@ export class UIContext {
|
|
|
391
501
|
ids: this.scope.listHandles().slice(before).map((h) => h.id),
|
|
392
502
|
};
|
|
393
503
|
});
|
|
504
|
+
const firstEnabled = renderedTabs.find((tab) => !tab.disabled)?.label ?? '';
|
|
505
|
+
const initial = renderedTabs.some((tab) => tab.label === requestedDefault && !tab.disabled)
|
|
506
|
+
? requestedDefault
|
|
507
|
+
: firstEnabled;
|
|
508
|
+
const valueAtom = this.scope.getAtom(`${id}/value`, initial);
|
|
509
|
+
const canActivate = (label) => renderedTabs.some((tab) => tab.label === label && !tab.disabled);
|
|
394
510
|
const handle = {
|
|
395
511
|
id,
|
|
396
512
|
type: 'tabs',
|
|
@@ -402,11 +518,20 @@ export class UIContext {
|
|
|
402
518
|
Object.assign(handle.props, patch);
|
|
403
519
|
if (patch.value !== undefined) {
|
|
404
520
|
const next = String(patch.value);
|
|
405
|
-
|
|
406
|
-
|
|
521
|
+
if (canActivate(next)) {
|
|
522
|
+
valueAtom.set(next);
|
|
523
|
+
handle.props.active = next;
|
|
524
|
+
}
|
|
407
525
|
}
|
|
408
526
|
this.scope.pushFragment(handle);
|
|
409
527
|
},
|
|
528
|
+
setActive: (label) => {
|
|
529
|
+
if (!canActivate(label))
|
|
530
|
+
return;
|
|
531
|
+
valueAtom.set(label);
|
|
532
|
+
handle.props.active = label;
|
|
533
|
+
this.scope.pushFragment(handle);
|
|
534
|
+
},
|
|
410
535
|
};
|
|
411
536
|
this.register(handle);
|
|
412
537
|
return handle;
|
|
@@ -419,6 +544,206 @@ export class UIContext {
|
|
|
419
544
|
const ids = this.scope.listHandles().slice(before).map((h) => h.id);
|
|
420
545
|
this.createPassiveHandle('card', { title, ids });
|
|
421
546
|
}
|
|
547
|
+
accordion(sections, opts = {}) {
|
|
548
|
+
const rendered = sections.map((section) => {
|
|
549
|
+
const before = this.scope.listHandles().length;
|
|
550
|
+
section.content(this);
|
|
551
|
+
return {
|
|
552
|
+
label: section.label,
|
|
553
|
+
defaultOpen: Boolean(section.defaultOpen),
|
|
554
|
+
ids: this.scope.listHandles().slice(before).map((h) => h.id),
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
this.createPassiveHandle('accordion', {
|
|
558
|
+
sections: rendered,
|
|
559
|
+
opts: { allowMultiple: opts.allowMultiple ?? false },
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
fullscreen(content, opts = {}) {
|
|
563
|
+
const id = createComponentId(this.scope, 'fullscreen');
|
|
564
|
+
const before = this.scope.listHandles().length;
|
|
565
|
+
content(this);
|
|
566
|
+
const ids = this.scope.listHandles().slice(before).map((h) => h.id);
|
|
567
|
+
const valueAtom = this.scope.getAtom(`${id}/value`, Boolean(opts.defaultOpen));
|
|
568
|
+
const handle = {
|
|
569
|
+
id,
|
|
570
|
+
type: 'fullscreen',
|
|
571
|
+
props: {
|
|
572
|
+
ids,
|
|
573
|
+
trigger: opts.trigger ?? 'button',
|
|
574
|
+
label: opts.label ?? 'Open fullscreen',
|
|
575
|
+
open: Boolean(opts.defaultOpen),
|
|
576
|
+
},
|
|
577
|
+
get value() {
|
|
578
|
+
return valueAtom.get();
|
|
579
|
+
},
|
|
580
|
+
update: (patch) => {
|
|
581
|
+
Object.assign(handle.props, patch);
|
|
582
|
+
if (patch.value !== undefined) {
|
|
583
|
+
const next = Boolean(patch.value);
|
|
584
|
+
valueAtom.set(next);
|
|
585
|
+
handle.props.open = next;
|
|
586
|
+
}
|
|
587
|
+
this.scope.pushFragment(handle);
|
|
588
|
+
},
|
|
589
|
+
open: () => {
|
|
590
|
+
valueAtom.set(true);
|
|
591
|
+
handle.props.open = true;
|
|
592
|
+
this.scope.pushFragment(handle);
|
|
593
|
+
},
|
|
594
|
+
close: () => {
|
|
595
|
+
valueAtom.set(false);
|
|
596
|
+
handle.props.open = false;
|
|
597
|
+
this.scope.pushFragment(handle);
|
|
598
|
+
},
|
|
599
|
+
};
|
|
600
|
+
return this.register(handle);
|
|
601
|
+
}
|
|
602
|
+
modelCompare(models, opts) {
|
|
603
|
+
const id = createComponentId(this.scope, 'modelCompare');
|
|
604
|
+
const initial = {
|
|
605
|
+
results: Object.fromEntries(models.map((model) => [model.label, ''])),
|
|
606
|
+
isStreaming: Object.fromEntries(models.map((model) => [model.label, false])),
|
|
607
|
+
errors: Object.fromEntries(models.map((model) => [model.label, null])),
|
|
608
|
+
latencies: Object.fromEntries(models.map((model) => [model.label, 0])),
|
|
609
|
+
};
|
|
610
|
+
const valueAtom = this.scope.getAtom(`${id}/value`, initial);
|
|
611
|
+
const handle = {
|
|
612
|
+
id,
|
|
613
|
+
type: 'modelCompare',
|
|
614
|
+
props: { models, ...opts, value: initial },
|
|
615
|
+
get value() {
|
|
616
|
+
return valueAtom.get();
|
|
617
|
+
},
|
|
618
|
+
get results() {
|
|
619
|
+
return valueAtom.get().results;
|
|
620
|
+
},
|
|
621
|
+
get isStreaming() {
|
|
622
|
+
return valueAtom.get().isStreaming;
|
|
623
|
+
},
|
|
624
|
+
get errors() {
|
|
625
|
+
return valueAtom.get().errors;
|
|
626
|
+
},
|
|
627
|
+
get latencies() {
|
|
628
|
+
return valueAtom.get().latencies;
|
|
629
|
+
},
|
|
630
|
+
update: (patch) => {
|
|
631
|
+
Object.assign(handle.props, patch);
|
|
632
|
+
if (patch.value !== undefined) {
|
|
633
|
+
valueAtom.set(patch.value);
|
|
634
|
+
handle.props.value = patch.value;
|
|
635
|
+
}
|
|
636
|
+
this.scope.pushFragment(handle);
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
return this.register(handle);
|
|
640
|
+
}
|
|
641
|
+
parameterPanel(schema, opts = {}) {
|
|
642
|
+
const id = createComponentId(this.scope, 'parameterPanel');
|
|
643
|
+
const before = this.scope.listHandles().length;
|
|
644
|
+
const byKey = {};
|
|
645
|
+
for (const [key, def] of Object.entries(schema)) {
|
|
646
|
+
const label = def.label ?? key;
|
|
647
|
+
if (def.type === 'number') {
|
|
648
|
+
if (typeof def.min === 'number' && typeof def.max === 'number') {
|
|
649
|
+
byKey[key] = this.slider(label, {
|
|
650
|
+
min: def.min,
|
|
651
|
+
max: def.max,
|
|
652
|
+
step: def.step,
|
|
653
|
+
default: typeof def.default === 'number' ? def.default : def.min,
|
|
654
|
+
helperText: def.description,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
byKey[key] = this.numberInput(label, {
|
|
659
|
+
min: def.min,
|
|
660
|
+
max: def.max,
|
|
661
|
+
step: def.step,
|
|
662
|
+
default: typeof def.default === 'number' ? def.default : 0,
|
|
663
|
+
helperText: def.description,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (def.type === 'boolean') {
|
|
669
|
+
byKey[key] = this.toggle(label, {
|
|
670
|
+
default: Boolean(def.default ?? false),
|
|
671
|
+
helperText: def.description,
|
|
672
|
+
});
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (def.type === 'select') {
|
|
676
|
+
byKey[key] = this.select(label, def.options ?? [], {
|
|
677
|
+
default: typeof def.default === 'string' ? def.default : (def.options?.[0] ?? ''),
|
|
678
|
+
helperText: def.description,
|
|
679
|
+
});
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
byKey[key] = this.textInput(label, {
|
|
683
|
+
default: typeof def.default === 'string' ? def.default : '',
|
|
684
|
+
helperText: def.description,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
const collectValue = () => {
|
|
688
|
+
const value = {};
|
|
689
|
+
for (const [key, child] of Object.entries(byKey)) {
|
|
690
|
+
value[key] = child.value;
|
|
691
|
+
}
|
|
692
|
+
return value;
|
|
693
|
+
};
|
|
694
|
+
const ids = this.scope.listHandles().slice(before).map((h) => h.id);
|
|
695
|
+
const handle = {
|
|
696
|
+
id,
|
|
697
|
+
type: 'parameterPanel',
|
|
698
|
+
props: {
|
|
699
|
+
schema,
|
|
700
|
+
ids,
|
|
701
|
+
title: opts.title,
|
|
702
|
+
collapsible: opts.collapsible ?? false,
|
|
703
|
+
value: collectValue(),
|
|
704
|
+
},
|
|
705
|
+
get value() {
|
|
706
|
+
return collectValue();
|
|
707
|
+
},
|
|
708
|
+
update: (patch) => {
|
|
709
|
+
if (patch.value && typeof patch.value === 'object') {
|
|
710
|
+
for (const [key, next] of Object.entries(patch.value)) {
|
|
711
|
+
const child = byKey[key];
|
|
712
|
+
if (!child) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
child.update({ value: next });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
Object.assign(handle.props, patch);
|
|
719
|
+
handle.props.value = collectValue();
|
|
720
|
+
this.scope.pushFragment(handle);
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
return this.register(handle);
|
|
724
|
+
}
|
|
725
|
+
filmStrip(images, opts = {}) {
|
|
726
|
+
const normalized = images.map((item) => (typeof item === 'string'
|
|
727
|
+
? { src: item, alt: 'Image' }
|
|
728
|
+
: item));
|
|
729
|
+
this.createPassiveHandle('filmStrip', {
|
|
730
|
+
images: normalized,
|
|
731
|
+
height: opts.height ?? 120,
|
|
732
|
+
zoom: opts.zoom ?? true,
|
|
733
|
+
showCaptions: opts.showCaptions ?? true,
|
|
734
|
+
selectedIndex: opts.selectedIndex ?? 0,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
beforeAfter(before, after, opts = {}) {
|
|
738
|
+
this.createPassiveHandle('beforeAfter', {
|
|
739
|
+
before,
|
|
740
|
+
after,
|
|
741
|
+
beforeLabel: opts.beforeLabel ?? 'Before',
|
|
742
|
+
afterLabel: opts.afterLabel ?? 'After',
|
|
743
|
+
initialPosition: opts.initialPosition ?? 50,
|
|
744
|
+
orientation: opts.orientation ?? 'horizontal',
|
|
745
|
+
});
|
|
746
|
+
}
|
|
422
747
|
divider(opts) {
|
|
423
748
|
this.createPassiveHandle('divider', { label: opts?.label });
|
|
424
749
|
}
|