@artkit-ai/react 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/cache.d.ts +7 -11
  2. package/dist/cache.d.ts.map +1 -1
  3. package/dist/cache.js +32 -53
  4. package/dist/cache.js.map +1 -1
  5. package/dist/client.d.ts +4 -15
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +2 -29
  8. package/dist/client.js.map +1 -1
  9. package/dist/components/card.d.ts +2 -2
  10. package/dist/components/card.d.ts.map +1 -1
  11. package/dist/components/card.js +3 -3
  12. package/dist/components/card.js.map +1 -1
  13. package/dist/components/editor.d.ts +31 -18
  14. package/dist/components/editor.d.ts.map +1 -1
  15. package/dist/components/editor.js +81 -131
  16. package/dist/components/editor.js.map +1 -1
  17. package/dist/components/icons.d.ts +2 -2
  18. package/dist/components/icons.d.ts.map +1 -1
  19. package/dist/components/icons.js +2 -2
  20. package/dist/components/icons.js.map +1 -1
  21. package/dist/components/panel.d.ts +13 -16
  22. package/dist/components/panel.d.ts.map +1 -1
  23. package/dist/components/panel.js +13 -97
  24. package/dist/components/panel.js.map +1 -1
  25. package/dist/components/shared.d.ts +2 -6
  26. package/dist/components/shared.d.ts.map +1 -1
  27. package/dist/components/shared.js +2 -6
  28. package/dist/components/shared.js.map +1 -1
  29. package/dist/components/view.d.ts +7 -8
  30. package/dist/components/view.d.ts.map +1 -1
  31. package/dist/components/view.js +16 -15
  32. package/dist/components/view.js.map +1 -1
  33. package/dist/context.d.ts +14 -9
  34. package/dist/context.d.ts.map +1 -1
  35. package/dist/context.js.map +1 -1
  36. package/dist/editor-session.d.ts +77 -165
  37. package/dist/editor-session.d.ts.map +1 -1
  38. package/dist/editor-session.js +164 -586
  39. package/dist/editor-session.js.map +1 -1
  40. package/dist/hooks.d.ts +2 -23
  41. package/dist/hooks.d.ts.map +1 -1
  42. package/dist/hooks.js +24 -116
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/i18n.d.ts +4 -66
  45. package/dist/i18n.d.ts.map +1 -1
  46. package/dist/i18n.js +6 -68
  47. package/dist/i18n.js.map +1 -1
  48. package/dist/index.d.ts +6 -9
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +4 -8
  51. package/dist/index.js.map +1 -1
  52. package/dist/provider.d.ts +1 -1
  53. package/dist/provider.d.ts.map +1 -1
  54. package/dist/provider.js +12 -12
  55. package/dist/provider.js.map +1 -1
  56. package/dist/styles.d.ts +1 -1
  57. package/dist/styles.d.ts.map +1 -1
  58. package/dist/styles.js +4 -4
  59. package/package.json +2 -2
  60. package/src/cache.ts +32 -55
  61. package/src/client.ts +7 -55
  62. package/src/components/card.tsx +6 -7
  63. package/src/components/editor.tsx +208 -308
  64. package/src/components/icons.tsx +2 -2
  65. package/src/components/panel.tsx +43 -300
  66. package/src/components/shared.tsx +3 -12
  67. package/src/components/view.tsx +26 -29
  68. package/src/context.ts +16 -11
  69. package/src/editor-session.ts +197 -675
  70. package/src/hooks.ts +25 -140
  71. package/src/i18n.ts +6 -68
  72. package/src/index.ts +7 -39
  73. package/src/provider.tsx +13 -13
  74. package/src/styles.ts +4 -4
  75. package/dist/components/approval.d.ts +0 -34
  76. package/dist/components/approval.d.ts.map +0 -1
  77. package/dist/components/approval.js +0 -80
  78. package/dist/components/approval.js.map +0 -1
  79. package/dist/components/version-diff.d.ts +0 -35
  80. package/dist/components/version-diff.d.ts.map +0 -1
  81. package/dist/components/version-diff.js +0 -85
  82. package/dist/components/version-diff.js.map +0 -1
  83. package/dist/line-diff.d.ts +0 -25
  84. package/dist/line-diff.d.ts.map +0 -1
  85. package/dist/line-diff.js +0 -96
  86. package/dist/line-diff.js.map +0 -1
  87. package/src/components/approval.tsx +0 -219
  88. package/src/components/version-diff.tsx +0 -266
  89. package/src/line-diff.ts +0 -111
@@ -1,5 +1,5 @@
1
- import type { Actor, Artifact, ArtifactStatus, VersionEntry } from '@artkit-ai/core';
2
- import { Archive, Check, ChevronDown, Copy, Download, Eye, History, Rocket, Send, X } from 'lucide-react';
1
+ import type { Artifact } from '@artkit-ai/core';
2
+ import { ChevronDown, Copy, Download, X } from 'lucide-react';
3
3
  import {
4
4
  type KeyboardEvent,
5
5
  type ReactNode,
@@ -10,29 +10,25 @@ import {
10
10
  useRef,
11
11
  useState,
12
12
  } from 'react';
13
- import type { StatusAction } from '../client.js';
14
13
  import { useArtkit } from '../context.js';
15
- import { useArtifact, useArtifactExport, useArtifactHistory } from '../hooks.js';
14
+ import type { DraftSnapshot } from '../editor-session.js';
15
+ import { useArtifact, useArtifactExport } from '../hooks.js';
16
16
  import type { UiMessageKey } from '../i18n.js';
17
- import { RejectForm } from './approval.js';
18
- import { ArtifactEditor, type EditorPermission } from './editor.js';
17
+ import { ArtifactEditor, type ArtifactEditorProps } from './editor.js';
19
18
  import {
20
- ChangeList,
21
19
  cx,
22
20
  ErrorMessage,
23
- formatDate,
24
- kindLabel,
21
+ formatLabel,
25
22
  Loading,
26
23
  rootAttributes,
27
24
  type Slots,
28
- StatusBadge,
29
25
  slot,
30
26
  useAnnouncer,
31
27
  } from './shared.js';
32
28
  import { ArtifactView } from './view.js';
33
29
 
34
- export type PanelTab = 'view' | 'code' | 'history';
35
- export type ArtifactPanelPart = 'header' | 'title' | 'toolbar' | 'tabs' | 'body' | 'code' | 'history';
30
+ export type PanelTab = 'view' | 'edit' | 'code';
31
+ export type ArtifactPanelPart = 'header' | 'title' | 'toolbar' | 'tabs' | 'body' | 'code';
36
32
 
37
33
  export interface ArtifactPanelProps {
38
34
  id?: string;
@@ -40,16 +36,10 @@ export interface ArtifactPanelProps {
40
36
  tab?: PanelTab;
41
37
  defaultTab?: PanelTab;
42
38
  onTabChange?(tab: PanelTab): void;
43
- /** Selected version (undefined = latest). Controlled when `onVersionChange` is given with it. */
44
- version?: number;
45
- onVersionChange?(version: number | undefined): void;
46
- /** `edit` shows the kind editor in the View tab for the latest version (default `read`). */
47
- permission?: EditorPermission;
48
- /** Streaming preview and lock state, forwarded to the view/editor. */
49
- partial?: unknown;
50
- lockedBy?: Actor | null;
51
- /** Lifecycle actions offered; defaults depend on the status. `false` hides them. */
52
- actions?: StatusAction[] | false;
39
+ /** Save action of the editor tab: the application decides what saving means. */
40
+ onSave?: ArtifactEditorProps['onSave'];
41
+ /** Draft changes, forwarded from the editor tab. */
42
+ onChange?(snapshot: DraftSnapshot): void;
53
43
  /** Export formats; defaults to the provider's server formats plus browser exporters. */
54
44
  exportFormats?: string[];
55
45
  onClose?(): void;
@@ -59,28 +49,16 @@ export interface ArtifactPanelProps {
59
49
  slots?: Slots<ArtifactPanelPart>;
60
50
  }
61
51
 
62
- const DEFAULT_ACTIONS: Record<ArtifactStatus, StatusAction[]> = {
63
- draft: ['submit', 'archive'],
64
- pending_approval: ['approve', 'reject', 'archive'],
65
- published: ['archive'],
66
- archived: [],
67
- };
52
+ const TABS: PanelTab[] = ['view', 'edit', 'code'];
53
+ const TAB_LABEL: Record<PanelTab, UiMessageKey> = { view: 'tabView', edit: 'tabEdit', code: 'tabCode' };
68
54
 
69
- const ACTION_FROM: Record<StatusAction, ArtifactStatus[]> = {
70
- submit: ['draft'],
71
- approve: ['pending_approval'],
72
- reject: ['pending_approval'],
73
- publish: ['draft', 'pending_approval'],
74
- archive: ['draft', 'pending_approval', 'published'],
75
- };
76
-
77
- const TABS: PanelTab[] = ['view', 'code', 'history'];
78
- const TAB_LABEL: Record<PanelTab, UiMessageKey> = { view: 'tabView', code: 'tabCode', history: 'tabHistory' };
79
-
80
- /** Assistant-style side panel: View, Code and History tabs, version selector, export and lifecycle (spec §13.1). */
55
+ /**
56
+ * Side panel of one artifact: View, Edit and Code tabs and the export menu (spec §13.1). There is no history, approval
57
+ * or archive here: the application owns versions and publication.
58
+ */
81
59
  export function ArtifactPanel(props: ArtifactPanelProps) {
82
60
  const ctx = useArtkit();
83
- const { t, client, cache } = ctx;
61
+ const { t } = ctx;
84
62
  const id = props.id ?? props.artifact?.id;
85
63
  const latestQuery = useArtifact(id, props.artifact ? { initial: props.artifact } : {});
86
64
  const latest = latestQuery.artifact;
@@ -91,26 +69,8 @@ export function ArtifactPanel(props: ArtifactPanelProps) {
91
69
  setOwnTab(next);
92
70
  props.onTabChange?.(next);
93
71
  };
94
- const [ownVersion, setOwnVersion] = useState<number | undefined>(undefined);
95
- const selectedVersion = props.onVersionChange ? props.version : (props.version ?? ownVersion);
96
- const selectVersion = (next: number | undefined) => {
97
- setOwnVersion(next);
98
- props.onVersionChange?.(next);
99
- };
100
- const viewingLatest = selectedVersion === undefined || selectedVersion === latest?.version;
101
- const selectedQuery = useArtifact(
102
- viewingLatest ? undefined : id,
103
- selectedVersion === undefined ? {} : { version: selectedVersion },
104
- );
105
- const shown = viewingLatest ? latest : selectedQuery.artifact;
106
-
107
- const history = useArtifactHistory(id);
108
- const exporter = useArtifactExport(id ?? '', latest?.kind);
72
+ const exporter = useArtifactExport(id ?? '', latest?.format);
109
73
  const [live, announce] = useAnnouncer();
110
- const [statusBusy, setStatusBusy] = useState<StatusAction | undefined>(undefined);
111
- const [actionError, setActionError] = useState<unknown>(undefined);
112
- const [rejecting, setRejecting] = useState(false);
113
- const [reason, setReason] = useState('');
114
74
  const baseId = useId();
115
75
 
116
76
  if (!latest || !id) {
@@ -128,43 +88,9 @@ export function ArtifactPanel(props: ArtifactPanelProps) {
128
88
  );
129
89
  }
130
90
 
131
- const actions = (props.actions === false ? [] : (props.actions ?? DEFAULT_ACTIONS[latest.status])).filter(
132
- (action) => ACTION_FROM[action].includes(latest.status),
133
- );
134
-
135
- async function runAction(action: StatusAction, message?: string) {
136
- if (!latest) return;
137
- setStatusBusy(action);
138
- setActionError(undefined);
139
- try {
140
- const updated = await client.setStatus(latest.id, action, message);
141
- cache.put(updated);
142
- announce(t('statusChanged', { status: t(`status_${updated.status}` as UiMessageKey) }));
143
- setRejecting(false);
144
- setReason('');
145
- } catch (error) {
146
- setActionError(error);
147
- } finally {
148
- setStatusBusy(undefined);
149
- }
150
- }
151
-
152
- async function restore(version: number) {
153
- try {
154
- const result = await history.restore(version);
155
- announce(t('restored', { version, newVersion: result.version }));
156
- selectVersion(undefined);
157
- } catch (error) {
158
- setActionError(error);
159
- }
160
- }
161
-
162
91
  async function exportAs(format: string) {
163
92
  try {
164
- const result = await exporter.exportArtifact(
165
- format,
166
- viewingLatest ? {} : { version: selectedVersion! },
167
- );
93
+ const result = await exporter.exportArtifact(format, {});
168
94
  announce(t('exported', { filename: result.filename }));
169
95
  } catch {
170
96
  // The hook keeps the error; it is rendered below.
@@ -186,7 +112,6 @@ export function ArtifactPanel(props: ArtifactPanelProps) {
186
112
  document.getElementById(tabId(next))?.focus();
187
113
  };
188
114
 
189
- const versionOptions = mergeVersions(history.entries, latest.version);
190
115
  const exportFormats = props.exportFormats ?? exporter.formats;
191
116
 
192
117
  return (
@@ -194,16 +119,22 @@ export function ArtifactPanel(props: ArtifactPanelProps) {
194
119
  className={cx('ak-root ak-panel', props.className)}
195
120
  {...rootAttributes(ctx, latest.themeId)}
196
121
  aria-labelledby={`${baseId}-title`}
197
- data-kind={latest.kind}
122
+ data-format={latest.format}
198
123
  >
199
124
  {live}
200
125
  <header {...slot(props.slots, 'header', 'ak-panel-header')}>
201
126
  <h2 id={`${baseId}-title`} {...slot(props.slots, 'title', 'ak-panel-title')}>
202
127
  {latest.title}
203
128
  </h2>
204
- <span className="ak-history-meta">{kindLabel(ctx, latest.kind)}</span>
205
- <StatusBadge status={latest.status} />
129
+ <span className="ak-history-meta">{formatLabel(ctx, latest.format)}</span>
130
+ <span className="ak-badge">{t('versionShort', { version: latest.version })}</span>
206
131
  {props.headerActions}
132
+ <ExportMenu
133
+ formats={exportFormats}
134
+ pending={exporter.pending}
135
+ onOpen={() => void exporter.loadFormats()}
136
+ onSelect={(format) => void exportAs(format)}
137
+ />
207
138
  {props.onClose ? (
208
139
  <button
209
140
  type="button"
@@ -216,86 +147,9 @@ export function ArtifactPanel(props: ArtifactPanelProps) {
216
147
  </button>
217
148
  ) : null}
218
149
  </header>
219
- <div {...slot(props.slots, 'toolbar', 'ak-panel-toolbar')}>
220
- <label htmlFor={`${baseId}-version`}>{t('version')}</label>
221
- <select
222
- id={`${baseId}-version`}
223
- className="ak-select"
224
- value={viewingLatest ? '' : String(selectedVersion)}
225
- onChange={(event) =>
226
- selectVersion(event.target.value === '' ? undefined : Number(event.target.value))
227
- }
228
- >
229
- <option value="">{`${t('versionShort', { version: latest.version })} (${t('latest')})`}</option>
230
- {versionOptions
231
- .filter((version) => version !== latest.version)
232
- .map((version) => (
233
- <option key={version} value={String(version)}>
234
- {t('versionShort', { version })}
235
- </option>
236
- ))}
237
- </select>
238
- <ExportMenu
239
- formats={exportFormats}
240
- pending={exporter.pending}
241
- onOpen={() => void exporter.loadFormats()}
242
- onSelect={(format) => void exportAs(format)}
243
- />
244
- {actions.map((action) =>
245
- action === 'reject' ? (
246
- <button
247
- key={action}
248
- type="button"
249
- className="ak-button"
250
- data-variant="danger"
251
- disabled={statusBusy !== undefined}
252
- onClick={() => setRejecting(true)}
253
- >
254
- <X aria-hidden="true" />
255
- {t('action_reject')}
256
- </button>
257
- ) : (
258
- <button
259
- key={action}
260
- type="button"
261
- className="ak-button"
262
- data-variant={
263
- action === 'approve' || action === 'submit' || action === 'publish' ? 'primary' : undefined
264
- }
265
- disabled={statusBusy !== undefined}
266
- onClick={() => void runAction(action)}
267
- >
268
- <ActionIcon action={action} />
269
- {t(`action_${action}` as UiMessageKey)}
270
- </button>
271
- ),
272
- )}
273
- </div>
274
- {rejecting ? (
275
- <div className="ak-panel-toolbar">
276
- <RejectForm
277
- busy={statusBusy !== undefined}
278
- requireReason
279
- reason={reason}
280
- onReasonChange={setReason}
281
- onSubmit={() => void runAction('reject', reason.trim())}
282
- onCancel={() => setRejecting(false)}
283
- />
284
- </div>
285
- ) : null}
286
- {actionError ? <ErrorMessage error={actionError} onDismiss={() => setActionError(undefined)} /> : null}
287
- {exporter.error ? <ErrorMessage error={exporter.error} /> : null}
288
- {exporter.warnings.length > 0 ? (
289
- <div className="ak-notice" role="status">
290
- {t('exportWarnings')}
291
- <ul>
292
- {exporter.warnings.map((warning) => (
293
- <li key={warning}>{warning}</li>
294
- ))}
295
- </ul>
296
- </div>
297
- ) : null}
298
- <div {...slot(props.slots, 'tabs', 'ak-tabs')} role="tablist" aria-label={t('tabsLabel')}>
150
+ {exporter.error ? <ErrorMessage error={exporter.error} className="ak-panel-notice" /> : null}
151
+
152
+ <div role="tablist" aria-label={latest.title} {...slot(props.slots, 'tabs', 'ak-tabs')}>
299
153
  {TABS.map((name) => (
300
154
  <button
301
155
  key={name}
@@ -313,55 +167,27 @@ export function ArtifactPanel(props: ArtifactPanelProps) {
313
167
  </button>
314
168
  ))}
315
169
  </div>
170
+
316
171
  <div
317
- {...slot(props.slots, 'body', 'ak-panel-body')}
318
- role="tabpanel"
319
172
  id={panelId(tab)}
173
+ role="tabpanel"
320
174
  aria-labelledby={tabId(tab)}
175
+ {...slot(props.slots, 'body', 'ak-panel-body')}
321
176
  >
322
- {tab === 'view' ? (
323
- viewingLatest && (props.permission === 'edit' || props.lockedBy) ? (
324
- <ArtifactEditor
325
- artifact={latest}
326
- permission={props.permission ?? 'read'}
327
- {...(props.lockedBy ? { lockedBy: props.lockedBy } : {})}
328
- {...(props.partial === undefined ? {} : { partial: props.partial })}
329
- />
330
- ) : shown ? (
331
- <ArtifactView
332
- artifact={shown}
333
- {...(viewingLatest && props.partial !== undefined ? { partial: props.partial } : {})}
334
- />
335
- ) : selectedQuery.error ? (
336
- <ErrorMessage error={selectedQuery.error} />
337
- ) : (
338
- <Loading />
339
- )
340
- ) : null}
341
- {tab === 'code' ? <CodeTab artifact={shown} slots={props.slots} /> : null}
342
- {tab === 'history' ? (
343
- <HistoryTab
344
- history={history}
345
- latestVersion={latest.version}
346
- selectedVersion={viewingLatest ? latest.version : selectedVersion}
347
- slots={props.slots}
348
- onView={(version) => {
349
- selectVersion(version === latest.version ? undefined : version);
350
- selectTab('view');
351
- }}
352
- onRestore={(version) => void restore(version)}
177
+ {tab === 'view' ? <ArtifactView artifact={latest} /> : null}
178
+ {tab === 'edit' ? (
179
+ <ArtifactEditor
180
+ artifact={latest}
181
+ {...(props.onSave ? { onSave: props.onSave } : {})}
182
+ {...(props.onChange ? { onChange: props.onChange } : {})}
353
183
  />
354
184
  ) : null}
185
+ {tab === 'code' ? <CodeTab artifact={latest} slots={props.slots} /> : null}
355
186
  </div>
356
187
  </aside>
357
188
  );
358
189
  }
359
190
 
360
- function mergeVersions(entries: VersionEntry[], latest: number): number[] {
361
- const versions = new Set<number>([latest, ...entries.map((entry) => entry.version)]);
362
- return [...versions].sort((a, b) => b - a);
363
- }
364
-
365
191
  function CodeTab({
366
192
  artifact,
367
193
  slots,
@@ -398,82 +224,6 @@ function CodeTab({
398
224
  );
399
225
  }
400
226
 
401
- function HistoryTab(props: {
402
- history: ReturnType<typeof useArtifactHistory>;
403
- latestVersion: number;
404
- selectedVersion: number | undefined;
405
- onView(version: number): void;
406
- onRestore(version: number): void;
407
- slots?: Slots<ArtifactPanelPart> | undefined;
408
- }) {
409
- const ctx = useArtkit();
410
- const { t } = ctx;
411
- const { history } = props;
412
- if (history.error && history.entries.length === 0)
413
- return <ErrorMessage error={history.error} onRetry={() => void history.refresh()} />;
414
- if (history.loading && history.entries.length === 0) return <Loading />;
415
- if (history.entries.length === 0) return <p className="ak-notice">{t('historyEmpty')}</p>;
416
- return (
417
- <div>
418
- <ol {...slot(props.slots, 'history', 'ak-history')} reversed>
419
- {history.entries.map((entry) => (
420
- <li
421
- key={entry.version}
422
- className="ak-history-entry"
423
- aria-current={entry.version === props.selectedVersion ? 'true' : undefined}
424
- >
425
- <div className="ak-history-head">
426
- <strong>{t('versionShort', { version: entry.version })}</strong>
427
- <span className="ak-history-meta">
428
- {formatDate(entry.createdAt, ctx.locale)} ·{' '}
429
- {t('by', { actor: t(`actor_${entry.actor.type}` as UiMessageKey) })}
430
- {entry.restoredFrom !== undefined
431
- ? ` · ${t('restoredFrom', { version: entry.restoredFrom })}`
432
- : ''}
433
- </span>
434
- <span className="ak-history-actions">
435
- <button
436
- type="button"
437
- className="ak-button"
438
- aria-label={t('viewVersion', { version: entry.version })}
439
- onClick={() => props.onView(entry.version)}
440
- >
441
- <Eye aria-hidden="true" />
442
- {t('tabView')}
443
- </button>
444
- {entry.version !== props.latestVersion ? (
445
- <button
446
- type="button"
447
- className="ak-button"
448
- aria-label={t('restoreVersion', { version: entry.version })}
449
- disabled={history.restoring !== undefined}
450
- onClick={() => props.onRestore(entry.version)}
451
- >
452
- <History aria-hidden="true" />
453
- {t('restore')}
454
- </button>
455
- ) : null}
456
- </span>
457
- </div>
458
- {entry.message ? <p className="ak-history-meta">{entry.message}</p> : null}
459
- {entry.diff.length > 0 ? <ChangeList changes={entry.diff} /> : null}
460
- </li>
461
- ))}
462
- </ol>
463
- {history.hasMore ? (
464
- <button
465
- type="button"
466
- className="ak-button"
467
- disabled={history.loading}
468
- onClick={() => void history.loadMore()}
469
- >
470
- {t('loadMore')}
471
- </button>
472
- ) : null}
473
- </div>
474
- );
475
- }
476
-
477
227
  function ExportMenu(props: {
478
228
  formats: string[];
479
229
  pending: string | undefined;
@@ -582,10 +332,3 @@ function ExportMenu(props: {
582
332
  </div>
583
333
  );
584
334
  }
585
-
586
- const ACTION_ICONS = { submit: Send, approve: Check, publish: Rocket, archive: Archive } as const;
587
-
588
- function ActionIcon({ action }: { action: string }) {
589
- const Icon = ACTION_ICONS[action as keyof typeof ACTION_ICONS];
590
- return Icon ? <Icon aria-hidden="true" /> : null;
591
- }
@@ -1,4 +1,4 @@
1
- import type { ArtifactStatus, ChangeSummary } from '@artkit-ai/core';
1
+ import type { ChangeSummary } from '@artkit-ai/core';
2
2
  import { isArtifactError } from '@artkit-ai/core';
3
3
  import { RotateCw } from 'lucide-react';
4
4
  import {
@@ -41,15 +41,6 @@ export function rootAttributes(ctx: ArtkitContextValue, themeId?: string): Recor
41
41
  };
42
42
  }
43
43
 
44
- export function StatusBadge({ status, className }: { status: ArtifactStatus; className?: string }) {
45
- const { t } = useArtkit();
46
- return (
47
- <span className={cx('ak-badge', className)} data-status={status}>
48
- {t(`status_${status}` as UiMessageKey)}
49
- </span>
50
- );
51
- }
52
-
53
44
  export function errorMessage(error: unknown, fallback: string): string {
54
45
  if (isArtifactError(error)) {
55
46
  const hint = error.issues.find((issue) => issue.hint)?.hint;
@@ -202,6 +193,6 @@ export function formatDate(value: string, locale: string): string {
202
193
  }
203
194
  }
204
195
 
205
- export function kindLabel(ctx: ArtkitContextValue, kind: string): string {
206
- return ctx.registry.has(kind) ? ctx.registry.get(kind).label : kind;
196
+ export function formatLabel(ctx: ArtkitContextValue, format: string): string {
197
+ return ctx.registry.has(format) ? ctx.registry.get(format).label : format;
207
198
  }
@@ -11,17 +11,16 @@ export type ArtifactViewPart = 'body' | 'badge' | 'loading' | 'error';
11
11
  export interface ArtifactViewProps {
12
12
  /** Committed artifact to show. */
13
13
  artifact?: Artifact;
14
- /** Alternatively, load by id (latest or `version`). */
14
+ /** Alternatively, load the artifact by id. */
15
15
  id?: string;
16
- version?: number;
17
16
  /**
18
17
  * Content still being streamed by the model: a partial object or the raw JSON text so far. It goes
19
- * through `kind.parsePartial` and shows as a provisional preview until a newer committed version
18
+ * through `format.parsePartial` and shows as a provisional preview until a newer committed version
20
19
  * arrives (spec §13.4).
21
20
  */
22
21
  partial?: unknown;
23
22
  /** Kind of the partial content when no artifact exists yet. */
24
- kind?: string;
23
+ format?: string;
25
24
  /** Theme and locale of the partial content when no artifact exists yet. */
26
25
  themeId?: string;
27
26
  locale?: string;
@@ -31,12 +30,13 @@ export interface ArtifactViewProps {
31
30
  }
32
31
 
33
32
  /** Turns streamed content into renderable content, or undefined while nothing complete arrived yet. */
34
- export function previewContent(ctx: ArtkitContextValue, kind: string, partial: unknown): unknown {
35
- if (!ctx.registry.has(kind)) return undefined;
33
+ export function previewContent(ctx: ArtkitContextValue, format: string, partial: unknown): unknown {
34
+ if (!ctx.registry.has(format)) return undefined;
36
35
  const raw = typeof partial === 'string' && /^\s*[[{]/.test(partial) ? parsePartialJson(partial) : partial;
37
36
  if (raw === undefined) return undefined;
38
37
  try {
39
- return ctx.registry.get(kind).parsePartial(raw);
38
+ // Adapters take the streamed text; structured partials (e.g. a workbook JSON) are serialized first.
39
+ return ctx.registry.get(format).parsePartial(typeof raw === 'string' ? raw : JSON.stringify(raw));
40
40
  } catch {
41
41
  return undefined;
42
42
  }
@@ -62,10 +62,7 @@ export function renderContext(
62
62
  export function ArtifactView(props: ArtifactViewProps) {
63
63
  const ctx = useArtkit();
64
64
  const { t } = ctx;
65
- const loaded = useArtifact(
66
- props.artifact ? undefined : props.id,
67
- props.version === undefined ? {} : { version: props.version },
68
- );
65
+ const loaded = useArtifact(props.artifact ? undefined : props.id);
69
66
  const committed = props.artifact ?? loaded.artifact;
70
67
 
71
68
  // Remember which committed version was on screen when streaming started: any other version arriving
@@ -79,10 +76,10 @@ export function ArtifactView(props: ArtifactViewProps) {
79
76
  props.partial !== undefined &&
80
77
  (streamStart.current === null || committed?.version === streamStart.current.version);
81
78
 
82
- const kind = props.kind ?? committed?.kind;
79
+ const format = props.format ?? committed?.format;
83
80
  const partialContent = useMemo(
84
- () => (showPartial && kind ? previewContent(ctx, kind, props.partial) : undefined),
85
- [ctx, kind, props.partial, showPartial],
81
+ () => (showPartial && format ? previewContent(ctx, format, props.partial) : undefined),
82
+ [ctx, format, props.partial, showPartial],
86
83
  );
87
84
 
88
85
  const themeId = committed?.themeId ?? props.themeId;
@@ -101,12 +98,12 @@ export function ArtifactView(props: ArtifactViewProps) {
101
98
  >
102
99
  {t('provisional')}
103
100
  </span>
104
- {partialContent === undefined || !kind ? (
101
+ {partialContent === undefined || !format ? (
105
102
  <Loading label={t('generating')} {...slot(props.slots, 'loading', '')} />
106
103
  ) : (
107
104
  <div {...slot(props.slots, 'body', 'ak-view-body')}>
108
- <KindRenderer
109
- kind={kind}
105
+ <FormatRenderer
106
+ format={format!}
110
107
  content={partialContent}
111
108
  ctx={renderContext(ctx, themeId, locale, mode, true)}
112
109
  />
@@ -137,12 +134,12 @@ export function ArtifactView(props: ArtifactViewProps) {
137
134
  className={rootClass}
138
135
  {...attrs}
139
136
  lang={committed.locale}
140
- data-kind={committed.kind}
137
+ data-format={committed.format}
141
138
  data-version={committed.version}
142
139
  >
143
140
  <div {...slot(props.slots, 'body', 'ak-view-body')}>
144
- <KindRenderer
145
- kind={committed.kind}
141
+ <FormatRenderer
142
+ format={committed.format}
146
143
  content={committed.content}
147
144
  ctx={renderContext(ctx, themeId, locale, mode, false)}
148
145
  />
@@ -151,20 +148,20 @@ export function ArtifactView(props: ArtifactViewProps) {
151
148
  );
152
149
  }
153
150
 
154
- interface KindRendererProps {
155
- kind: string;
151
+ interface FormatRendererProps {
152
+ format: string;
156
153
  content: unknown;
157
154
  ctx: RenderContext;
158
155
  className?: string;
159
156
  }
160
157
 
161
158
  /** Lazy kind renderer with a JSON fallback when the kind has no renderer loader. */
162
- export function KindRenderer(props: KindRendererProps) {
159
+ export function FormatRenderer(props: FormatRendererProps) {
163
160
  const ctx = useArtkit();
164
- if (!ctx.hasRenderer(props.kind)) {
161
+ if (!ctx.hasRenderer(props.format)) {
165
162
  return (
166
163
  <div className={props.className}>
167
- <p className="ak-notice">{ctx.t('noRenderer', { kind: props.kind })}</p>
164
+ <p className="ak-notice">{ctx.t('noRenderer', { format: props.format })}</p>
168
165
  <pre className="ak-code">
169
166
  <code>{JSON.stringify(props.content, null, 2)}</code>
170
167
  </pre>
@@ -173,7 +170,7 @@ export function KindRenderer(props: KindRendererProps) {
173
170
  }
174
171
  return (
175
172
  <ErrorBoundary
176
- onReset={() => ctx.resetLoad('renderer', props.kind)}
173
+ onReset={() => ctx.resetLoad('renderer', props.format)}
177
174
  fallback={(error, reset) => <ErrorMessage error={error} onRetry={reset} />}
178
175
  >
179
176
  <Suspense fallback={<Loading />}>
@@ -183,13 +180,13 @@ export function KindRenderer(props: KindRendererProps) {
183
180
  );
184
181
  }
185
182
 
186
- function LoadedRenderer({ kind, content, ctx: renderCtx, className }: KindRendererProps) {
183
+ function LoadedRenderer({ format, content, ctx: renderCtx, className }: FormatRendererProps) {
187
184
  const ctx = useArtkit();
188
- const { Renderer, css } = use(ctx.loadRenderer(kind));
185
+ const { Renderer, css } = use(ctx.loadRenderer(format));
189
186
  return (
190
187
  <>
191
188
  {css ? (
192
- <style href={`artkit-kind-${kind}-${hashString(css)}`} precedence="artkit" nonce={ctx.nonce}>
189
+ <style href={`artkit-kind-${format}-${hashString(css)}`} precedence="artkit" nonce={ctx.nonce}>
193
190
  {css}
194
191
  </style>
195
192
  ) : null}