@artkit-ai/react 0.0.0-stage → 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/LICENSE +202 -0
- package/README.md +145 -2
- package/dist/cache.d.ts +31 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +95 -0
- package/dist/cache.js.map +1 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +233 -0
- package/dist/client.js.map +1 -0
- package/dist/components/card.d.ts +22 -0
- package/dist/components/card.d.ts.map +1 -0
- package/dist/components/card.js +16 -0
- package/dist/components/card.js.map +1 -0
- package/dist/components/editor.d.ts +41 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +111 -0
- package/dist/components/editor.js.map +1 -0
- package/dist/components/icons.d.ts +8 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +14 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/components/panel.d.ts +31 -0
- package/dist/components/panel.d.ts.map +1 -0
- package/dist/components/panel.js +132 -0
- package/dist/components/panel.js.map +1 -0
- package/dist/components/shared.d.ts +67 -0
- package/dist/components/shared.d.ts.map +1 -0
- package/dist/components/shared.js +100 -0
- package/dist/components/shared.js.map +1 -0
- package/dist/components/view.d.ts +39 -0
- package/dist/components/view.d.ts.map +1 -0
- package/dist/components/view.js +76 -0
- package/dist/components/view.js.map +1 -0
- package/dist/context.d.ts +64 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +13 -0
- package/dist/context.js.map +1 -0
- package/dist/controls.d.ts +145 -0
- package/dist/controls.d.ts.map +1 -0
- package/dist/controls.js +188 -0
- package/dist/controls.js.map +1 -0
- package/dist/download.d.ts +4 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +20 -0
- package/dist/download.js.map +1 -0
- package/dist/editor-session.d.ts +96 -0
- package/dist/editor-session.d.ts.map +1 -0
- package/dist/editor-session.js +192 -0
- package/dist/editor-session.js.map +1 -0
- package/dist/hooks.d.ts +64 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +205 -0
- package/dist/hooks.js.map +1 -0
- package/dist/i18n.d.ts +135 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +145 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/kit-css.d.ts +3 -0
- package/dist/kit-css.d.ts.map +1 -0
- package/dist/kit-css.js +5 -0
- package/dist/kit-css.js.map +1 -0
- package/dist/partial-json.d.ts +8 -0
- package/dist/partial-json.d.ts.map +1 -0
- package/dist/partial-json.js +176 -0
- package/dist/partial-json.js.map +1 -0
- package/dist/provider.d.ts +42 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +95 -0
- package/dist/provider.js.map +1 -0
- package/dist/styles.d.ts +25 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +203 -0
- package/dist/styles.js.map +1 -0
- package/dist/toolbar.d.ts +90 -0
- package/dist/toolbar.d.ts.map +1 -0
- package/dist/toolbar.js +233 -0
- package/dist/toolbar.js.map +1 -0
- package/package.json +50 -3
- package/src/cache.ts +109 -0
- package/src/client.ts +342 -0
- package/src/components/card.tsx +86 -0
- package/src/components/editor.tsx +292 -0
- package/src/components/icons.tsx +14 -0
- package/src/components/panel.tsx +334 -0
- package/src/components/shared.tsx +198 -0
- package/src/components/view.tsx +196 -0
- package/src/context.ts +87 -0
- package/src/controls.tsx +587 -0
- package/src/download.ts +23 -0
- package/src/editor-session.ts +260 -0
- package/src/hooks.ts +296 -0
- package/src/i18n.ts +155 -0
- package/src/index.ts +128 -0
- package/src/kit-css.ts +5 -0
- package/src/partial-json.ts +160 -0
- package/src/provider.tsx +176 -0
- package/src/styles.ts +222 -0
- package/src/toolbar.tsx +536 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Artifact,
|
|
3
|
+
ArtifactError,
|
|
4
|
+
type ArtifactFilter,
|
|
5
|
+
type ArtifactService,
|
|
6
|
+
type ArtifactSummary,
|
|
7
|
+
type BatchResult,
|
|
8
|
+
type CommitResult,
|
|
9
|
+
type CreateRequest,
|
|
10
|
+
ERROR_CODES,
|
|
11
|
+
type ErrorCode,
|
|
12
|
+
type ErrorIssue,
|
|
13
|
+
isArtifactError,
|
|
14
|
+
type Lock,
|
|
15
|
+
type Page,
|
|
16
|
+
type PageRequest,
|
|
17
|
+
type ReadMode,
|
|
18
|
+
type ArtifactReadResult as ReadResult,
|
|
19
|
+
type Scope,
|
|
20
|
+
} from '@artkit-ai/core';
|
|
21
|
+
|
|
22
|
+
export interface RequestOptions {
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReadOptions extends RequestOptions {
|
|
27
|
+
mode?: ReadMode;
|
|
28
|
+
range?: string;
|
|
29
|
+
maxTokens?: number;
|
|
30
|
+
cursor?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Operations computed against `baseVersion`; a different current version answers VERSION_CONFLICT. */
|
|
34
|
+
export interface ApplyInput {
|
|
35
|
+
baseVersion: number;
|
|
36
|
+
operations: unknown[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ExportInput extends RequestOptions {
|
|
40
|
+
format: string;
|
|
41
|
+
options?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ClientExportResult {
|
|
45
|
+
bytes: Uint8Array;
|
|
46
|
+
mimeType: string;
|
|
47
|
+
filename: string;
|
|
48
|
+
warnings: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What the React layer talks to (spec §13.3). It never touches a store: credentials and authorization
|
|
53
|
+
* stay on the server (HTTP client) or in the scope given to an in-process service (service client).
|
|
54
|
+
*/
|
|
55
|
+
export interface ArtifactClient {
|
|
56
|
+
list(filter?: ArtifactFilter, page?: PageRequest, options?: RequestOptions): Promise<Page<ArtifactSummary>>;
|
|
57
|
+
create(request: CreateRequest, options?: RequestOptions): Promise<CommitResult>;
|
|
58
|
+
get(id: string, options?: RequestOptions): Promise<Artifact>;
|
|
59
|
+
read(id: string, options?: ReadOptions): Promise<ReadResult>;
|
|
60
|
+
apply(id: string, input: ApplyInput, options?: RequestOptions): Promise<CommitResult>;
|
|
61
|
+
/** Dry run on the current version: diff and lint without committing. */
|
|
62
|
+
preview(id: string, input: ApplyInput, options?: RequestOptions): Promise<BatchResult>;
|
|
63
|
+
lock(id: string, options?: RequestOptions & { ttlMs?: number }): Promise<Lock>;
|
|
64
|
+
unlock(id: string, lock: Lock, options?: RequestOptions): Promise<void>;
|
|
65
|
+
/** Current lock, or null when nobody holds one. */
|
|
66
|
+
getLock(id: string, options?: RequestOptions): Promise<Lock | null>;
|
|
67
|
+
export(id: string, input: ExportInput): Promise<ClientExportResult>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------------------------------
|
|
71
|
+
// HTTP client
|
|
72
|
+
// ---------------------------------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
type HeadersSource = HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
75
|
+
|
|
76
|
+
export interface HttpClientOptions {
|
|
77
|
+
/** Base path of the `@artkit-ai/node` routes (default `/api/artifacts`). */
|
|
78
|
+
baseUrl?: string;
|
|
79
|
+
/** Custom fetch (auth wrappers, tests, SSR). Defaults to the global `fetch`. */
|
|
80
|
+
fetch?: typeof fetch;
|
|
81
|
+
/** Extra headers, or a function returning them per request (e.g. a fresh bearer token). */
|
|
82
|
+
headers?: HeadersSource;
|
|
83
|
+
credentials?: RequestCredentials;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const STATUS_TO_CODE: Record<number, ErrorCode> = {
|
|
87
|
+
404: 'NOT_FOUND',
|
|
88
|
+
403: 'PERMISSION_DENIED',
|
|
89
|
+
409: 'VERSION_CONFLICT',
|
|
90
|
+
423: 'LOCKED',
|
|
91
|
+
413: 'LIMIT_EXCEEDED',
|
|
92
|
+
422: 'INVALID_CONTENT',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function isErrorCode(value: unknown): value is ErrorCode {
|
|
96
|
+
return typeof value === 'string' && (ERROR_CODES as readonly string[]).includes(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toIssues(value: unknown): ErrorIssue[] | undefined {
|
|
100
|
+
if (!Array.isArray(value)) return undefined;
|
|
101
|
+
const issues: ErrorIssue[] = [];
|
|
102
|
+
for (const item of value) {
|
|
103
|
+
if (item === null || typeof item !== 'object') continue;
|
|
104
|
+
const { path, message, hint } = item as Record<string, unknown>;
|
|
105
|
+
if (typeof message !== 'string') continue;
|
|
106
|
+
issues.push({
|
|
107
|
+
path: typeof path === 'string' ? path : '',
|
|
108
|
+
message,
|
|
109
|
+
...(typeof hint === 'string' ? { hint } : {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return issues;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Rebuilds an `ArtifactError` from an HTTP error response (`ModelErrorPayload` body, spec CONVENTIONS). */
|
|
116
|
+
export async function errorFromResponse(response: Response): Promise<ArtifactError> {
|
|
117
|
+
let body: unknown;
|
|
118
|
+
try {
|
|
119
|
+
body = await response.json();
|
|
120
|
+
} catch {
|
|
121
|
+
body = undefined;
|
|
122
|
+
}
|
|
123
|
+
const error = (body as { error?: Record<string, unknown> } | undefined)?.error;
|
|
124
|
+
const fallbackCode: ErrorCode = STATUS_TO_CODE[response.status] ?? 'INTERNAL_ERROR';
|
|
125
|
+
if (error && typeof error === 'object' && typeof error.message === 'string') {
|
|
126
|
+
const issues = toIssues(error.issues);
|
|
127
|
+
const details =
|
|
128
|
+
error.details && typeof error.details === 'object' && !Array.isArray(error.details)
|
|
129
|
+
? (error.details as Record<string, unknown>)
|
|
130
|
+
: undefined;
|
|
131
|
+
return new ArtifactError(isErrorCode(error.code) ? error.code : fallbackCode, error.message, {
|
|
132
|
+
...(issues ? { issues } : {}),
|
|
133
|
+
details: { ...details, status: response.status },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return new ArtifactError(fallbackCode, `Request failed with status ${response.status}.`, {
|
|
137
|
+
details: { status: response.status },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Parses `Content-Disposition`, preferring the RFC 5987 `filename*` form. */
|
|
142
|
+
export function filenameFromDisposition(header: string | null): string | undefined {
|
|
143
|
+
if (!header) return undefined;
|
|
144
|
+
const extended = /filename\*\s*=\s*(?:UTF-8|utf-8)?''([^;]+)/.exec(header);
|
|
145
|
+
if (extended?.[1]) {
|
|
146
|
+
try {
|
|
147
|
+
return sanitizeFilename(decodeURIComponent(extended[1].trim().replace(/^"|"$/g, '')));
|
|
148
|
+
} catch {
|
|
149
|
+
// Fall through to the plain form when the encoded name is malformed.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const plain = /filename\s*=\s*("([^"]*)"|[^;]+)/.exec(header);
|
|
153
|
+
const value = plain?.[2] ?? plain?.[1];
|
|
154
|
+
return value ? sanitizeFilename(value.trim()) : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The name ends up in a download attribute: strip any path a hostile server could smuggle in.
|
|
158
|
+
function sanitizeFilename(name: string): string | undefined {
|
|
159
|
+
const base = name.split(/[\\/]/).pop()?.trim();
|
|
160
|
+
return base ? base : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseWarnings(header: string | null): string[] {
|
|
164
|
+
if (!header) return [];
|
|
165
|
+
try {
|
|
166
|
+
const parsed: unknown = JSON.parse(decodeURIComponent(header));
|
|
167
|
+
return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : [];
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isAbortError(error: unknown): boolean {
|
|
174
|
+
return typeof error === 'object' && error !== null && (error as { name?: unknown }).name === 'AbortError';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Reference client for the `@artkit-ai/node` routes (HTTP contract in docs/CONVENTIONS.md). */
|
|
178
|
+
export function createHttpClient(options: HttpClientOptions = {}): ArtifactClient {
|
|
179
|
+
const base = (options.baseUrl ?? '/api/artifacts').replace(/\/+$/, '');
|
|
180
|
+
const doFetch =
|
|
181
|
+
options.fetch ?? ((input: RequestInfo | URL, init?: RequestInit) => globalThis.fetch(input, init));
|
|
182
|
+
|
|
183
|
+
const path = (id?: string, suffix?: string) =>
|
|
184
|
+
`${base}${id === undefined ? '' : `/${encodeURIComponent(id)}`}${suffix ? `/${suffix}` : ''}`;
|
|
185
|
+
|
|
186
|
+
function withQuery(url: string, query: Record<string, string | number | undefined>): string {
|
|
187
|
+
const params = new URLSearchParams();
|
|
188
|
+
for (const [key, value] of Object.entries(query))
|
|
189
|
+
if (value !== undefined && value !== '') params.set(key, String(value));
|
|
190
|
+
const text = params.toString();
|
|
191
|
+
return text ? `${url}?${text}` : url;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function send(method: string, url: string, body?: unknown, signal?: AbortSignal): Promise<Response> {
|
|
195
|
+
const headers = new Headers(
|
|
196
|
+
typeof options.headers === 'function' ? await options.headers() : options.headers,
|
|
197
|
+
);
|
|
198
|
+
if (body !== undefined) headers.set('Content-Type', 'application/json');
|
|
199
|
+
if (!headers.has('Accept')) headers.set('Accept', 'application/json');
|
|
200
|
+
let response: Response;
|
|
201
|
+
try {
|
|
202
|
+
response = await doFetch(url, {
|
|
203
|
+
method,
|
|
204
|
+
headers,
|
|
205
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
206
|
+
...(signal ? { signal } : {}),
|
|
207
|
+
...(options.credentials ? { credentials: options.credentials } : {}),
|
|
208
|
+
});
|
|
209
|
+
} catch (cause) {
|
|
210
|
+
if (isAbortError(cause)) throw cause;
|
|
211
|
+
throw new ArtifactError('INTERNAL_ERROR', 'Network request failed.', {
|
|
212
|
+
cause,
|
|
213
|
+
details: { network: true },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (!response.ok) throw await errorFromResponse(response);
|
|
217
|
+
return response;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function json<T>(method: string, url: string, body?: unknown, signal?: AbortSignal): Promise<T> {
|
|
221
|
+
const response = await send(method, url, body, signal);
|
|
222
|
+
if (response.status === 204) return undefined as T;
|
|
223
|
+
try {
|
|
224
|
+
return (await response.json()) as T;
|
|
225
|
+
} catch (cause) {
|
|
226
|
+
throw new ArtifactError('INTERNAL_ERROR', 'Invalid JSON response.', {
|
|
227
|
+
cause,
|
|
228
|
+
details: { status: response.status },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
list: (filter = {}, page = {}, opts) =>
|
|
235
|
+
json(
|
|
236
|
+
'GET',
|
|
237
|
+
withQuery(path(), { ...filter, limit: page.limit, cursor: page.cursor }),
|
|
238
|
+
undefined,
|
|
239
|
+
opts?.signal,
|
|
240
|
+
),
|
|
241
|
+
create: (request, opts) => json('POST', path(), request, opts?.signal),
|
|
242
|
+
get: (id, opts) => json('GET', path(id), undefined, opts?.signal),
|
|
243
|
+
read: (id, opts = {}) =>
|
|
244
|
+
json(
|
|
245
|
+
'GET',
|
|
246
|
+
withQuery(path(id, 'read'), {
|
|
247
|
+
mode: opts.mode,
|
|
248
|
+
range: opts.range,
|
|
249
|
+
maxTokens: opts.maxTokens,
|
|
250
|
+
cursor: opts.cursor,
|
|
251
|
+
}),
|
|
252
|
+
undefined,
|
|
253
|
+
opts.signal,
|
|
254
|
+
),
|
|
255
|
+
apply: (id, input, opts) => json('POST', path(id, 'apply'), input, opts?.signal),
|
|
256
|
+
preview: (id, input, opts) => json('POST', path(id, 'preview'), input, opts?.signal),
|
|
257
|
+
lock: (id, opts) =>
|
|
258
|
+
json('POST', path(id, 'lock'), opts?.ttlMs === undefined ? {} : { ttlMs: opts.ttlMs }, opts?.signal),
|
|
259
|
+
async unlock(id, lock, opts) {
|
|
260
|
+
await send('DELETE', path(id, 'lock'), lock, opts?.signal);
|
|
261
|
+
},
|
|
262
|
+
getLock: (id, opts) => json('GET', path(id, 'lock'), undefined, opts?.signal),
|
|
263
|
+
async export(id, input) {
|
|
264
|
+
const response = await send(
|
|
265
|
+
'POST',
|
|
266
|
+
path(id, 'export'),
|
|
267
|
+
{
|
|
268
|
+
format: input.format,
|
|
269
|
+
...(input.options ? { options: input.options } : {}),
|
|
270
|
+
},
|
|
271
|
+
input.signal,
|
|
272
|
+
);
|
|
273
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
274
|
+
const mimeType =
|
|
275
|
+
response.headers.get('Content-Type')?.split(';')[0]?.trim() || 'application/octet-stream';
|
|
276
|
+
return {
|
|
277
|
+
bytes,
|
|
278
|
+
mimeType,
|
|
279
|
+
filename:
|
|
280
|
+
filenameFromDisposition(response.headers.get('Content-Disposition')) ?? `${id}.${input.format}`,
|
|
281
|
+
warnings: parseWarnings(response.headers.get('X-Artkit-Warnings')),
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------------------------------
|
|
288
|
+
// In-process client
|
|
289
|
+
// ---------------------------------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
function normalizeError(error: unknown): unknown {
|
|
292
|
+
if (isArtifactError(error) || isAbortError(error)) return error;
|
|
293
|
+
return new ArtifactError('INTERNAL_ERROR', 'Internal error while processing the request.', {
|
|
294
|
+
cause: error,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Wraps a core `ArtifactService` in-process (playground, tests, local apps). The scope is fixed by the
|
|
300
|
+
* application, never by the UI; pass a function to resolve it per call (e.g. after a login change).
|
|
301
|
+
*/
|
|
302
|
+
export function createServiceClient(service: ArtifactService, scope: Scope | (() => Scope)): ArtifactClient {
|
|
303
|
+
const current = () => (typeof scope === 'function' ? scope() : scope);
|
|
304
|
+
const run = async <T>(fn: (s: Scope) => Promise<T>): Promise<T> => {
|
|
305
|
+
try {
|
|
306
|
+
return await fn(current());
|
|
307
|
+
} catch (error) {
|
|
308
|
+
throw normalizeError(error);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
list: (filter, page) => run((s) => service.list(s, filter, page)),
|
|
314
|
+
create: (request) => run((s) => service.create(s, request)),
|
|
315
|
+
get: (id) => run((s) => service.get(s, id)),
|
|
316
|
+
read: (id, opts = {}) =>
|
|
317
|
+
run((s) => {
|
|
318
|
+
const { signal: _signal, ...rest } = opts;
|
|
319
|
+
return service.read(s, id, rest);
|
|
320
|
+
}),
|
|
321
|
+
apply: (id, input) => run((s) => service.apply(s, { artifactId: id, ...input })),
|
|
322
|
+
preview: (id, input) => run((s) => service.preview(s, { artifactId: id, ...input })),
|
|
323
|
+
lock: (id, opts) => run((s) => service.lock(s, id, opts?.ttlMs)),
|
|
324
|
+
unlock: (_id, lock) => run((s) => service.unlock(s, lock)),
|
|
325
|
+
getLock: (id) => run((s) => service.getLock(s, id)),
|
|
326
|
+
export: (id, input) =>
|
|
327
|
+
run(async (s) => {
|
|
328
|
+
const result = await service.export(s, {
|
|
329
|
+
artifactId: id,
|
|
330
|
+
format: input.format,
|
|
331
|
+
...(input.options ? { options: input.options } : {}),
|
|
332
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
333
|
+
});
|
|
334
|
+
return {
|
|
335
|
+
bytes: result.bytes,
|
|
336
|
+
mimeType: result.mimeType,
|
|
337
|
+
filename: result.filename,
|
|
338
|
+
warnings: result.warnings,
|
|
339
|
+
};
|
|
340
|
+
}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ArtifactSummary } from '@artkit-ai/core';
|
|
2
|
+
import { ArrowUpRight } from 'lucide-react';
|
|
3
|
+
import { type ReactNode, useId } from 'react';
|
|
4
|
+
import { useArtkit } from '../context.js';
|
|
5
|
+
import { KindIcon } from './icons.js';
|
|
6
|
+
import { cx, formatLabel, rootAttributes, type Slots, slot } from './shared.js';
|
|
7
|
+
|
|
8
|
+
export type ArtifactCardPart = 'body' | 'title' | 'meta' | 'open';
|
|
9
|
+
|
|
10
|
+
export interface ArtifactCardProps {
|
|
11
|
+
/** Summary or full artifact (only the summary fields are used). */
|
|
12
|
+
artifact: Pick<ArtifactSummary, 'id' | 'format' | 'title' | 'version'>;
|
|
13
|
+
onOpen?(artifact: ArtifactCardProps['artifact']): void;
|
|
14
|
+
/** Render the open action as a link instead of a button. */
|
|
15
|
+
href?: string;
|
|
16
|
+
/** Shows a "generating" badge while the agent is still streaming the artifact. */
|
|
17
|
+
streaming?: boolean;
|
|
18
|
+
/** Extra actions rendered after the open action. */
|
|
19
|
+
actions?: ReactNode;
|
|
20
|
+
/** Icon or thumbnail rendered before the text. */
|
|
21
|
+
icon?: ReactNode;
|
|
22
|
+
className?: string;
|
|
23
|
+
slots?: Slots<ArtifactCardPart>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Compact card for chat messages: title, format, version and an open action (spec §13.1). */
|
|
27
|
+
export function ArtifactCard(props: ArtifactCardProps) {
|
|
28
|
+
const ctx = useArtkit();
|
|
29
|
+
const { t } = ctx;
|
|
30
|
+
const { artifact } = props;
|
|
31
|
+
const openLabel = t('openTitle', { title: artifact.title });
|
|
32
|
+
const titleId = useId();
|
|
33
|
+
return (
|
|
34
|
+
<article
|
|
35
|
+
className={cx('ak-root ak-card', props.className)}
|
|
36
|
+
{...rootAttributes(ctx)}
|
|
37
|
+
aria-labelledby={titleId}
|
|
38
|
+
data-format={artifact.format}
|
|
39
|
+
>
|
|
40
|
+
{props.icon ?? (
|
|
41
|
+
<span className="ak-card-icon">
|
|
42
|
+
<KindIcon format={artifact.format} />
|
|
43
|
+
</span>
|
|
44
|
+
)}
|
|
45
|
+
<div {...slot(props.slots, 'body', 'ak-card-body')}>
|
|
46
|
+
<h3 id={titleId} {...slot(props.slots, 'title', 'ak-card-title')}>
|
|
47
|
+
{artifact.title}
|
|
48
|
+
</h3>
|
|
49
|
+
<div {...slot(props.slots, 'meta', 'ak-card-meta')}>
|
|
50
|
+
<span>{formatLabel(ctx, artifact.format)}</span>
|
|
51
|
+
<span>
|
|
52
|
+
<span className="ak-sr-only">{`${t('version')} ${artifact.version}`}</span>
|
|
53
|
+
<span aria-hidden="true">{t('versionShort', { version: artifact.version })}</span>
|
|
54
|
+
</span>
|
|
55
|
+
{props.streaming ? (
|
|
56
|
+
<span className="ak-badge" data-variant="provisional" role="status">
|
|
57
|
+
{t('generating')}
|
|
58
|
+
</span>
|
|
59
|
+
) : null}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
{props.href ? (
|
|
63
|
+
<a
|
|
64
|
+
{...slot(props.slots, 'open', 'ak-button')}
|
|
65
|
+
href={props.href}
|
|
66
|
+
aria-label={openLabel}
|
|
67
|
+
onClick={props.onOpen ? () => props.onOpen?.(artifact) : undefined}
|
|
68
|
+
>
|
|
69
|
+
{t('open')}
|
|
70
|
+
<ArrowUpRight aria-hidden="true" />
|
|
71
|
+
</a>
|
|
72
|
+
) : props.onOpen ? (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
{...slot(props.slots, 'open', 'ak-button')}
|
|
76
|
+
aria-label={openLabel}
|
|
77
|
+
onClick={() => props.onOpen?.(artifact)}
|
|
78
|
+
>
|
|
79
|
+
{t('open')}
|
|
80
|
+
<ArrowUpRight aria-hidden="true" />
|
|
81
|
+
</button>
|
|
82
|
+
) : null}
|
|
83
|
+
{props.actions}
|
|
84
|
+
</article>
|
|
85
|
+
);
|
|
86
|
+
}
|