@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.
- package/dist/cache.d.ts +7 -11
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +32 -53
- package/dist/cache.js.map +1 -1
- package/dist/client.d.ts +4 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -29
- package/dist/client.js.map +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +3 -3
- package/dist/components/card.js.map +1 -1
- package/dist/components/editor.d.ts +31 -18
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +81 -131
- package/dist/components/editor.js.map +1 -1
- package/dist/components/icons.d.ts +2 -2
- package/dist/components/icons.d.ts.map +1 -1
- package/dist/components/icons.js +2 -2
- package/dist/components/icons.js.map +1 -1
- package/dist/components/panel.d.ts +13 -16
- package/dist/components/panel.d.ts.map +1 -1
- package/dist/components/panel.js +13 -97
- package/dist/components/panel.js.map +1 -1
- package/dist/components/shared.d.ts +2 -6
- package/dist/components/shared.d.ts.map +1 -1
- package/dist/components/shared.js +2 -6
- package/dist/components/shared.js.map +1 -1
- package/dist/components/view.d.ts +7 -8
- package/dist/components/view.d.ts.map +1 -1
- package/dist/components/view.js +16 -15
- package/dist/components/view.js.map +1 -1
- package/dist/context.d.ts +14 -9
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js.map +1 -1
- package/dist/editor-session.d.ts +77 -165
- package/dist/editor-session.d.ts.map +1 -1
- package/dist/editor-session.js +164 -586
- package/dist/editor-session.js.map +1 -1
- package/dist/hooks.d.ts +2 -23
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +24 -116
- package/dist/hooks.js.map +1 -1
- package/dist/i18n.d.ts +4 -66
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +6 -68
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -8
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +12 -12
- package/dist/provider.js.map +1 -1
- package/dist/styles.d.ts +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +4 -4
- package/package.json +2 -2
- package/src/cache.ts +32 -55
- package/src/client.ts +7 -55
- package/src/components/card.tsx +6 -7
- package/src/components/editor.tsx +208 -308
- package/src/components/icons.tsx +2 -2
- package/src/components/panel.tsx +43 -300
- package/src/components/shared.tsx +3 -12
- package/src/components/view.tsx +26 -29
- package/src/context.ts +16 -11
- package/src/editor-session.ts +197 -675
- package/src/hooks.ts +25 -140
- package/src/i18n.ts +6 -68
- package/src/index.ts +7 -39
- package/src/provider.tsx +13 -13
- package/src/styles.ts +4 -4
- package/dist/components/approval.d.ts +0 -34
- package/dist/components/approval.d.ts.map +0 -1
- package/dist/components/approval.js +0 -80
- package/dist/components/approval.js.map +0 -1
- package/dist/components/version-diff.d.ts +0 -35
- package/dist/components/version-diff.d.ts.map +0 -1
- package/dist/components/version-diff.js +0 -85
- package/dist/components/version-diff.js.map +0 -1
- package/dist/line-diff.d.ts +0 -25
- package/dist/line-diff.d.ts.map +0 -1
- package/dist/line-diff.js +0 -96
- package/dist/line-diff.js.map +0 -1
- package/src/components/approval.tsx +0 -219
- package/src/components/version-diff.tsx +0 -266
- package/src/line-diff.ts +0 -111
package/src/components/panel.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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' | '
|
|
35
|
-
export type ArtifactPanelPart = 'header' | 'title' | 'toolbar' | 'tabs' | 'body' | 'code'
|
|
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
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
63
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
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-
|
|
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">{
|
|
205
|
-
<
|
|
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
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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 {
|
|
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
|
|
206
|
-
return ctx.registry.has(
|
|
196
|
+
export function formatLabel(ctx: ArtkitContextValue, format: string): string {
|
|
197
|
+
return ctx.registry.has(format) ? ctx.registry.get(format).label : format;
|
|
207
198
|
}
|
package/src/components/view.tsx
CHANGED
|
@@ -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
|
|
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 `
|
|
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
|
-
|
|
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,
|
|
35
|
-
if (!ctx.registry.has(
|
|
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
|
-
|
|
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
|
|
79
|
+
const format = props.format ?? committed?.format;
|
|
83
80
|
const partialContent = useMemo(
|
|
84
|
-
() => (showPartial &&
|
|
85
|
-
[ctx,
|
|
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 || !
|
|
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
|
-
<
|
|
109
|
-
|
|
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-
|
|
137
|
+
data-format={committed.format}
|
|
141
138
|
data-version={committed.version}
|
|
142
139
|
>
|
|
143
140
|
<div {...slot(props.slots, 'body', 'ak-view-body')}>
|
|
144
|
-
<
|
|
145
|
-
|
|
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
|
|
155
|
-
|
|
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
|
|
159
|
+
export function FormatRenderer(props: FormatRendererProps) {
|
|
163
160
|
const ctx = useArtkit();
|
|
164
|
-
if (!ctx.hasRenderer(props.
|
|
161
|
+
if (!ctx.hasRenderer(props.format)) {
|
|
165
162
|
return (
|
|
166
163
|
<div className={props.className}>
|
|
167
|
-
<p className="ak-notice">{ctx.t('noRenderer', {
|
|
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.
|
|
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({
|
|
183
|
+
function LoadedRenderer({ format, content, ctx: renderCtx, className }: FormatRendererProps) {
|
|
187
184
|
const ctx = useArtkit();
|
|
188
|
-
const { Renderer, css } = use(ctx.loadRenderer(
|
|
185
|
+
const { Renderer, css } = use(ctx.loadRenderer(format));
|
|
189
186
|
return (
|
|
190
187
|
<>
|
|
191
188
|
{css ? (
|
|
192
|
-
<style href={`artkit-kind-${
|
|
189
|
+
<style href={`artkit-kind-${format}-${hashString(css)}`} precedence="artkit" nonce={ctx.nonce}>
|
|
193
190
|
{css}
|
|
194
191
|
</style>
|
|
195
192
|
) : null}
|