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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/integration/ws-flow.integration.test.js +38 -140
  3. package/dist/__tests__/integration/ws-flow.integration.test.js.map +1 -1
  4. package/dist/client/events.js +169 -49
  5. package/dist/client/events.js.map +1 -1
  6. package/dist/client/swap.d.ts +14 -1
  7. package/dist/client/swap.js +185 -7
  8. package/dist/client/swap.js.map +1 -1
  9. package/dist/client/ws.d.ts +1 -0
  10. package/dist/client/ws.js +214 -35
  11. package/dist/client/ws.js.map +1 -1
  12. package/dist/client/ws.test.js +6 -146
  13. package/dist/client/ws.test.js.map +1 -1
  14. package/dist/components/context.d.ts +14 -2
  15. package/dist/components/context.js +330 -5
  16. package/dist/components/context.js.map +1 -1
  17. package/dist/components/context.test.js +74 -56
  18. package/dist/components/context.test.js.map +1 -1
  19. package/dist/components/registry.d.ts +2 -2
  20. package/dist/components/registry.js +52 -4
  21. package/dist/components/registry.js.map +1 -1
  22. package/dist/components/types.d.ts +180 -4
  23. package/dist/engine/code-highlight.d.ts +1 -0
  24. package/dist/engine/code-highlight.js +64 -0
  25. package/dist/engine/code-highlight.js.map +1 -0
  26. package/dist/engine/code-highlight.test.d.ts +1 -0
  27. package/dist/engine/code-highlight.test.js +15 -0
  28. package/dist/engine/code-highlight.test.js.map +1 -0
  29. package/dist/engine/executor.js +16 -0
  30. package/dist/engine/executor.js.map +1 -1
  31. package/dist/engine/executor.test.js +33 -4
  32. package/dist/engine/executor.test.js.map +1 -1
  33. package/dist/engine/messages.d.ts +9 -1
  34. package/dist/engine/messages.js.map +1 -1
  35. package/dist/engine/messages.test.d.ts +1 -0
  36. package/dist/engine/messages.test.js +39 -0
  37. package/dist/engine/messages.test.js.map +1 -0
  38. package/dist/engine/renderer.js +241 -14
  39. package/dist/engine/renderer.js.map +1 -1
  40. package/dist/engine/renderer.test.js +230 -202
  41. package/dist/engine/renderer.test.js.map +1 -1
  42. package/dist/engine/server.d.ts +2 -0
  43. package/dist/engine/server.js +57 -6
  44. package/dist/engine/server.js.map +1 -1
  45. package/dist/engine/server.test.js +105 -4
  46. package/dist/engine/server.test.js.map +1 -1
  47. package/dist/engine/watcher.d.ts +17 -1
  48. package/dist/engine/watcher.js +42 -5
  49. package/dist/engine/watcher.js.map +1 -1
  50. package/dist/engine/watcher.test.d.ts +1 -0
  51. package/dist/engine/watcher.test.js +62 -0
  52. package/dist/engine/watcher.test.js.map +1 -0
  53. package/dist/engine/websocket.d.ts +2 -0
  54. package/dist/engine/websocket.hub.test.js +24 -0
  55. package/dist/engine/websocket.hub.test.js.map +1 -1
  56. package/dist/engine/websocket.js +6 -1
  57. package/dist/engine/websocket.js.map +1 -1
  58. package/dist/index.d.ts +3 -1
  59. package/dist/index.js +1 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/style.css +150 -0
  62. package/dist/theme/lastriko.css +150 -0
  63. package/package.json +5 -1
@@ -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('renders toast for TOAST messages (except connection id sentinel)', () => {
113
- const { document, restore } = setupDom();
114
- try {
115
- const sockets = [];
116
- const manager = createWSManager({
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":"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"}
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, ComponentHandle, ConnectionScope, FileUploadHandle, FileUploadOpts, GridOpts, ImageGridProps, ImageProps, LoadingOpts, MetricHandle, MetricOpts, NumberInputHandle, NumberInputOpts, ProgressHandle, ProgressOpts, PromptEditorHandle, PromptEditorOpts, SelectHandle, SelectOption, SelectOpts, ShellOpts, ShellRegions, SliderHandle, SliderOpts, StreamHandle, StreamTextOpts, TabDef, TabsOpts, TableHandle, TableProps, TableRow, TextHandle, TextInputHandle, TextInputOpts, ToggleHandle, ToggleOpts, ToastOpts, UIContext as IUIContext } from './types';
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): ComponentHandle<Record<string, unknown>, string>;
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.map((option) => (typeof option === 'string' ? { label: option, value: option } : option));
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 initial = opts.defaultTab ?? tabs[0]?.label ?? '';
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
- valueAtom.set(next);
406
- handle.props.active = next;
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
  }