@civitai/blocks-react 0.76.1 → 0.77.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.
@@ -27,6 +27,23 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
27
27
  * no deep hook paths, so the rename touches no reachable API.
28
28
  */
29
29
  export const HUMAN_INTERACTION_TIMEOUT_MS = 10 * 60_000;
30
+ /**
31
+ * The bound for `OPEN_IMAGE_UPLOAD { purpose: 'trainingData' }` (civitai/civitai#6461),
32
+ * which is longer than {@link HUMAN_INTERACTION_TIMEOUT_MS} on purpose.
33
+ *
34
+ * The host replies ONCE, when its modal closes — after the viewer picked up to 50
35
+ * images AND the host uploaded every original. That second half is network work
36
+ * the viewer does not control, so ten minutes from `open()` can expire while the
37
+ * host is still uploading. The block would then reject, drop the late reply, and
38
+ * never learn the keys the host now remembers for `RUN_TRAINING`. The host always
39
+ * answers on close (images, or a bare cancel), so the bound only has to end a wait
40
+ * on a host that will never answer; an hour does that without cutting off a slow
41
+ * upload.
42
+ *
43
+ * Still in the `'human'` bucket of {@link REQUEST_TIMEOUT_CLASS}: it is at least
44
+ * the human bound, which is what that bucket guarantees.
45
+ */
46
+ export const TRAINING_DATA_UPLOAD_TIMEOUT_MS = 60 * 60_000;
30
47
  /**
31
48
  * The bound for a `'protocol'` request whose SERVER WORK is itself slow — the
32
49
  * host forwards it to the orchestrator and replies only once that answers. No
@@ -35,14 +52,9 @@ export const HUMAN_INTERACTION_TIMEOUT_MS = 10 * 60_000;
35
52
  *
36
53
  * Callers: the workflow trio in `hooks/useBuzzWorkflow.ts` (a submit is a whatif
37
54
  * preflight plus the real submit plus a prompt audit), and
38
- * `PREPARE_TRAINING_DATASET` in `hooks/usePrepareTrainingDataset.ts`, whose
39
- * server moderates the captions and imports up to 50 images into the
40
- * orchestrator, the import alone under a 60s budget
41
- * (`BLOCK_TRAINING_IMPORT_BUDGET_MS` in civitai/civitai's
42
- * `block-training-dataset.service.ts`) — so the 30s default would reject a
43
- * healthy prepare mid-import.
55
+ * `ESTIMATE_WORKFLOW_BATCH` in `hooks/useBatchEstimate.ts`.
44
56
  *
45
- * Was a module-local constant in `useBuzzWorkflow.ts`; moved here so the second
57
+ * Was a module-local constant in `useBuzzWorkflow.ts`; moved here so every
46
58
  * caller shares the number instead of re-typing it.
47
59
  */
48
60
  export const WORKFLOW_REQUEST_TIMEOUT_MS = 120_000;
@@ -130,11 +142,6 @@ const REQUEST_TIMEOUT_CLASS = {
130
142
  APP_STORAGE_SET: 'protocol',
131
143
  CANCEL_APP_WORKFLOW: 'protocol',
132
144
  CANCEL_WORKFLOW: 'protocol',
133
- // No dialog — preparing a dataset charges nothing and the host replies when
134
- // the server does. Server-bound, so `'protocol'`; but sent under
135
- // `WORKFLOW_REQUEST_TIMEOUT_MS`, not the 30s default, because the server's
136
- // image import alone may take 60s. Same reasoning as the workflow trio below.
137
- PREPARE_TRAINING_DATASET: 'protocol',
138
145
  // The workflow trio is server-bound, not person-bound. It carries its OWN
139
146
  // longer bound (`WORKFLOW_REQUEST_TIMEOUT_MS` in `hooks/useBuzzWorkflow.ts`)
140
147
  // for orchestrator latency — a different reason for a different number.
@@ -245,7 +245,8 @@ export declare function isValidViewerResult(p: unknown): p is {
245
245
  * value, `url` non-empty).
246
246
  * • `'generationSource'` → the source `{ url, width, height }` (positive finite
247
247
  * dimensions, non-empty url; no imageId/nsfwLevel).
248
- * The guard accepts EITHER shape (the result carries no purpose discriminator).
248
+ * • `'trainingData'` → `{ images: [{ url, air }] }` (non-empty strings).
249
+ * The guard accepts ANY of the shapes (the result carries no purpose discriminator).
249
250
  * Kept in lockstep with the host's `IMAGE_UPLOAD_RESULT` (civitai/civitai
250
251
  * `PageBlockHost.tsx` / `BlockImageUploadModal.tsx` /
251
252
  * `BlockGenerationSourceUploadModal.tsx`).
@@ -265,6 +266,11 @@ export declare function isValidImageUploadResult(p: unknown): p is {
265
266
  status: 'pending';
266
267
  imageId: number;
267
268
  url: string;
269
+ } | {
270
+ images: Array<{
271
+ url: string;
272
+ air: string;
273
+ }>;
268
274
  };
269
275
  error?: string;
270
276
  };
@@ -442,22 +448,6 @@ export declare function isValidEntityReactionsResult(p: unknown): boolean;
442
448
  * so a drop at least times out visibly.
443
449
  */
444
450
  export declare function isValidCreatePostResult(p: unknown): boolean;
445
- /**
446
- * Reply to `PREPARE_TRAINING_DATASET`. A well-formed reply carries EITHER a
447
- * `result` (`{ datasetId: non-empty string, count: integer ≥ 0, rejected: [{
448
- * imageId: number, reason: string }] }`) OR an `error`; one with neither is
449
- * malformed and dropped.
450
- *
451
- * 🔴 `error` IS SHAPE-CHECKED ONLY — the same decision as
452
- * {@link isValidCreatePostResult}: the host's closed codes share the field with
453
- * any server message it forwards, and a membership check would drop those
454
- * replies before correlation, leaving the request to its timer.
455
- *
456
- * `rejected[].reason` is checked as a STRING, not against today's six members —
457
- * the host adding a reason must not make every prepare reply that carries it a
458
- * dropped one (the forward-compatibility rule in {@link isValidWorkflowSnapshot}).
459
- */
460
- export declare function isValidTrainingDatasetResult(p: unknown): boolean;
461
451
  /**
462
452
  * Reply to `RUN_TRAINING`. A well-formed reply carries EITHER a `snapshot` (a
463
453
  * valid {@link isValidWorkflowSnapshot} — the block polls its `workflowId`) OR an
@@ -770,6 +770,18 @@ function isPendingUpload(s) {
770
770
  return false;
771
771
  return true;
772
772
  }
773
+ /**
774
+ * The `purpose:'trainingData'` `IMAGE_UPLOAD_RESULT.selected` shape
775
+ * (civitai/civitai#6461): `{ images: [{ url, air }] }`, each `url` and `air` a
776
+ * non-empty string. Mirrors the host's `TrainingDataUpload`. `air` is opaque —
777
+ * its key format is the host's to check, at the estimate — so only its type is
778
+ * pinned here.
779
+ */
780
+ function isTrainingDataUpload(s) {
781
+ if (!Array.isArray(s.images))
782
+ return false;
783
+ return s.images.every((img) => isObject(img) && isNonEmptyString(img.url) && isNonEmptyString(img.air));
784
+ }
773
785
  /**
774
786
  * Reply to a block-initiated `OPEN_IMAGE_UPLOAD`. `selected` is ABSENT when the
775
787
  * user dismissed the upload modal (the block's hook resolves to `null`). When
@@ -779,7 +791,8 @@ function isPendingUpload(s) {
779
791
  * value, `url` non-empty).
780
792
  * • `'generationSource'` → the source `{ url, width, height }` (positive finite
781
793
  * dimensions, non-empty url; no imageId/nsfwLevel).
782
- * The guard accepts EITHER shape (the result carries no purpose discriminator).
794
+ * • `'trainingData'` → `{ images: [{ url, air }] }` (non-empty strings).
795
+ * The guard accepts ANY of the shapes (the result carries no purpose discriminator).
783
796
  * Kept in lockstep with the host's `IMAGE_UPLOAD_RESULT` (civitai/civitai
784
797
  * `PageBlockHost.tsx` / `BlockImageUploadModal.tsx` /
785
798
  * `BlockGenerationSourceUploadModal.tsx`).
@@ -797,13 +810,18 @@ export function isValidImageUploadResult(p) {
797
810
  if (p.error !== undefined && typeof p.error !== 'string')
798
811
  return false;
799
812
  // `selected` absent → cancelled upload (valid). When present, it must match
800
- // one of the purpose-keyed shapes (moderated / generationSource / pending).
813
+ // one of the purpose-keyed shapes (moderated / generationSource / pending /
814
+ // trainingData).
801
815
  if (p.selected !== undefined) {
802
816
  const s = p.selected;
803
817
  if (!isObject(s))
804
818
  return false;
805
- if (!isModeratedUpload(s) && !isGenerationSourceUpload(s) && !isPendingUpload(s))
819
+ if (!isModeratedUpload(s) &&
820
+ !isGenerationSourceUpload(s) &&
821
+ !isPendingUpload(s) &&
822
+ !isTrainingDataUpload(s)) {
806
823
  return false;
824
+ }
807
825
  }
808
826
  return true;
809
827
  }
@@ -1533,48 +1551,6 @@ export function isValidCreatePostResult(p) {
1533
1551
  return false;
1534
1552
  return true;
1535
1553
  }
1536
- /**
1537
- * Reply to `PREPARE_TRAINING_DATASET`. A well-formed reply carries EITHER a
1538
- * `result` (`{ datasetId: non-empty string, count: integer ≥ 0, rejected: [{
1539
- * imageId: number, reason: string }] }`) OR an `error`; one with neither is
1540
- * malformed and dropped.
1541
- *
1542
- * 🔴 `error` IS SHAPE-CHECKED ONLY — the same decision as
1543
- * {@link isValidCreatePostResult}: the host's closed codes share the field with
1544
- * any server message it forwards, and a membership check would drop those
1545
- * replies before correlation, leaving the request to its timer.
1546
- *
1547
- * `rejected[].reason` is checked as a STRING, not against today's six members —
1548
- * the host adding a reason must not make every prepare reply that carries it a
1549
- * dropped one (the forward-compatibility rule in {@link isValidWorkflowSnapshot}).
1550
- */
1551
- export function isValidTrainingDatasetResult(p) {
1552
- if (!isObject(p))
1553
- return false;
1554
- if (!isWireRequestIdShape(p.requestId))
1555
- return false;
1556
- if (p.error !== undefined && typeof p.error !== 'string')
1557
- return false;
1558
- if (p.result !== undefined) {
1559
- const r = p.result;
1560
- if (!isObject(r))
1561
- return false;
1562
- if (!isNonEmptyString(r.datasetId))
1563
- return false;
1564
- if (!Number.isInteger(r.count) || r.count < 0)
1565
- return false;
1566
- if (!Array.isArray(r.rejected))
1567
- return false;
1568
- for (const x of r.rejected) {
1569
- if (!isObject(x) || !isFiniteNumber(x.imageId) || typeof x.reason !== 'string') {
1570
- return false;
1571
- }
1572
- }
1573
- }
1574
- if (p.result === undefined && p.error === undefined)
1575
- return false;
1576
- return true;
1577
- }
1578
1554
  /**
1579
1555
  * Reply to `RUN_TRAINING`. A well-formed reply carries EITHER a `snapshot` (a
1580
1556
  * valid {@link isValidWorkflowSnapshot} — the block polls its `workflowId`) OR an
@@ -2349,8 +2325,6 @@ export function payloadValidatorFor(type) {
2349
2325
  // Same caveat as CREATE_POST_RESULT: presence is compiler-enforced, the
2350
2326
  // identity is pinned by test (`validate.test.ts`). TRAINING_RESULT settles a
2351
2327
  // charged run.
2352
- case 'TRAINING_DATASET_RESULT':
2353
- return isValidTrainingDatasetResult;
2354
2328
  case 'TRAINING_RESULT':
2355
2329
  return isValidTrainingResult;
2356
2330
  // Settles a charged job, like TRAINING_RESULT directly above.
@@ -1,3 +1,3 @@
1
1
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
2
- export declare const VERSION = "0.76.1";
2
+ export declare const VERSION = "0.77.0";
3
3
  //# sourceMappingURL=version.generated.d.ts.map
@@ -1,4 +1,4 @@
1
1
  /* AUTOGENERATED from package.json by scripts/gen-version.mjs. DO NOT EDIT. */
2
2
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
3
- export const VERSION = "0.76.1";
3
+ export const VERSION = "0.77.0";
4
4
  //# sourceMappingURL=version.generated.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.76.1",
3
+ "version": "0.77.0",
4
4
  "description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -58,7 +58,7 @@
58
58
  "typescript": "^5.9.2",
59
59
  "vite": "^8.0.14",
60
60
  "vitest": "^4.1.11",
61
- "@civitai/app-sdk": "^0.75.0"
61
+ "@civitai/app-sdk": "^0.77.0"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"
@@ -1,99 +0,0 @@
1
- import type { BlockPrepareTrainingDatasetHostError, BlockTrainingDatasetItem, BlockTrainingDatasetResult, BlockTrainingRejectionReason } from '@civitai/app-sdk/blocks';
2
- import type { ConsentRetryOptions } from './consentRetryOptions.js';
3
- export type { BlockPrepareTrainingDatasetHostError, BlockTrainingDatasetItem, BlockTrainingDatasetResult, BlockTrainingRejectionReason, };
4
- /**
5
- * The host's CLOSED refusal codes for `PREPARE_TRAINING_DATASET`, as a runtime
6
- * list. Mirrors civitai/civitai's `PREPARE_TRAINING_DATASET_HOST_ERRORS`.
7
- *
8
- * 🔴 THE ERROR CHANNEL IS NOT AN ENUM: it also carries free-text server
9
- * messages, so "is this a code?" is a membership question at runtime — see
10
- * {@link isPrepareTrainingDatasetErrorCode}.
11
- */
12
- export declare const PREPARE_TRAINING_DATASET_ERROR_CODES: readonly ["review-mode", "block is not ready", "sign in to train", "invalid training dataset", "no block token"];
13
- /** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
14
- export declare function isPrepareTrainingDatasetErrorCode(error: string): error is BlockPrepareTrainingDatasetHostError;
15
- /**
16
- * A dataset-preparation failure.
17
- *
18
- * `.code` is the host's refusal code when the HOST refused, `undefined` for a
19
- * server message or a transport timeout. Check `.timedOut` before rendering
20
- * `.message` — a timeout's message is an SDK-internal string.
21
- *
22
- * Preparing charges nothing, so no failure here costs the viewer Buzz.
23
- */
24
- export declare class PrepareTrainingDatasetError extends Error {
25
- /** The closed host refusal code, or `undefined` for a server/transport error. */
26
- readonly code?: BlockPrepareTrainingDatasetHostError;
27
- /**
28
- * No reply arrived within the bound. The server may still have prepared a
29
- * dataset; nothing was charged either way, and preparing again simply makes a
30
- * new handle.
31
- */
32
- readonly timedOut: boolean;
33
- /**
34
- * There is no session — the viewer is signed out (`sign in to train`), or their
35
- * session ended (a bare `UNAUTHORIZED`). Route into `useRequestSignIn()`.
36
- */
37
- readonly signInRequired: boolean;
38
- constructor(error: string, opts?: {
39
- timedOut?: boolean;
40
- });
41
- }
42
- /** What {@link usePrepareTrainingDataset} returns. */
43
- export interface UsePrepareTrainingDataset {
44
- /**
45
- * Ask the host to prepare a training dataset from the viewer's OWN images.
46
- * Resolves with the opaque `datasetId`, the admitted `count` and the
47
- * `rejected` images. REJECTS with a {@link PrepareTrainingDatasetError}.
48
- *
49
- * A resolved result admitted AT LEAST ONE image (`count >= 1`) — when nothing
50
- * is admitted the server refuses instead, and this rejects with its message
51
- * (`none of the requested images can be used for training` / `… could be
52
- * prepared for training`, no `.code`). Read `rejected` for what was left out:
53
- * `pending-scan` and `import-unavailable` are retryable; the rest are not.
54
- * The per-image reasons are not available when everything was rejected.
55
- */
56
- prepareDataset: (items: BlockTrainingDatasetItem[], options?: ConsentRetryOptions) => Promise<BlockTrainingDatasetResult>;
57
- /** `true` while a request is in flight. */
58
- pending: boolean;
59
- /** The last request's failure, or `null`. Cleared at the start of the next call. */
60
- error: PrepareTrainingDatasetError | null;
61
- }
62
- /**
63
- * Step 1 of the App Blocks `kind: 'training'` flow: turn the viewer's own images
64
- * and your captions into a dataset handle for `WorkflowBodyTraining.datasetId`,
65
- * through the host-mediated `PREPARE_TRAINING_DATASET` → `TRAINING_DATASET_RESULT`
66
- * bridge.
67
- *
68
- * 🔴 AVAILABILITY: page apps only, behind the host flag `app-blocks-training-kind`
69
- * (ships off), with `ai:write:budgeted` declared in the manifest and granted by a
70
- * signed-in viewer. Refused from `dev:live` and from review sessions — build the
71
- * flow against the mock host. See `WorkflowBodyTraining` in `@civitai/app-sdk`.
72
- *
73
- * Only the viewer's OWN scanned, unflagged images within the page's maturity
74
- * ceiling are admitted; captions are moderated. Bounds: 1–50 items, captions up
75
- * to 1,000 characters (`BLOCK_TRAINING_DATASET_MAX_ITEMS`,
76
- * `BLOCK_TRAINING_CAPTION_MAX_CHARS`); the host refuses anything else with
77
- * `invalid training dataset` before calling the server.
78
- *
79
- * No dialog and no charge. When the token lacks `ai:write:budgeted`, the call
80
- * opens the host's consent prompt and retries once on a grant (opt out with
81
- * `{ autoRequestConsent: false }`).
82
- *
83
- * Sent under the 120s server-work bound, not the 30s protocol default: the server
84
- * imports up to 50 images into the orchestrator within its own 60s budget.
85
- *
86
- * @example
87
- * const { prepareDataset } = usePrepareTrainingDataset();
88
- * try {
89
- * const dataset = await prepareDataset(
90
- * picked.map((img) => ({ imageId: img.id, caption: img.caption })),
91
- * );
92
- * if (dataset.rejected.length > 0) showNotice(`${dataset.rejected.length} image(s) left out`);
93
- * } catch (e) {
94
- * // Includes "none of the requested images can be used for training".
95
- * if (e instanceof PrepareTrainingDatasetError) showError(e.message);
96
- * }
97
- */
98
- export declare function usePrepareTrainingDataset(): UsePrepareTrainingDataset;
99
- //# sourceMappingURL=usePrepareTrainingDataset.d.ts.map
@@ -1,159 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
- import { BLOCK_SCOPES } from '@civitai/app-sdk/blocks';
3
- import { withConsentRetry } from '../internal/withConsentRetry.js';
4
- import { WORKFLOW_REQUEST_TIMEOUT_MS } from '../transport/requestTimeouts.js';
5
- import { getTransport } from '../transport/singleton.js';
6
- import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
7
- import { useTransportConnection } from './useTransportConnection.js';
8
- /**
9
- * The consent-gated scope the training flow requires — `ai:write:budgeted`. The
10
- * server refuses a dataset request from a token without it (it is the spend
11
- * scope, and preparing is gated exactly like the run it prepares for). From
12
- * {@link BLOCK_SCOPES}, never a literal: a typo would make the automatic consent
13
- * prompt silently do nothing.
14
- */
15
- const TRAINING_SCOPES = [BLOCK_SCOPES.AI_WRITE_BUDGETED];
16
- /**
17
- * The host's CLOSED refusal codes for `PREPARE_TRAINING_DATASET`, as a runtime
18
- * list. Mirrors civitai/civitai's `PREPARE_TRAINING_DATASET_HOST_ERRORS`.
19
- *
20
- * 🔴 THE ERROR CHANNEL IS NOT AN ENUM: it also carries free-text server
21
- * messages, so "is this a code?" is a membership question at runtime — see
22
- * {@link isPrepareTrainingDatasetErrorCode}.
23
- */
24
- export const PREPARE_TRAINING_DATASET_ERROR_CODES = [
25
- 'review-mode',
26
- 'block is not ready',
27
- 'sign in to train',
28
- 'invalid training dataset',
29
- 'no block token',
30
- ];
31
- const CODE_SET = new Set(PREPARE_TRAINING_DATASET_ERROR_CODES);
32
- /** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
33
- export function isPrepareTrainingDatasetErrorCode(error) {
34
- return CODE_SET.has(error);
35
- }
36
- /**
37
- * A dataset-preparation failure.
38
- *
39
- * `.code` is the host's refusal code when the HOST refused, `undefined` for a
40
- * server message or a transport timeout. Check `.timedOut` before rendering
41
- * `.message` — a timeout's message is an SDK-internal string.
42
- *
43
- * Preparing charges nothing, so no failure here costs the viewer Buzz.
44
- */
45
- export class PrepareTrainingDatasetError extends Error {
46
- /** The closed host refusal code, or `undefined` for a server/transport error. */
47
- code;
48
- /**
49
- * No reply arrived within the bound. The server may still have prepared a
50
- * dataset; nothing was charged either way, and preparing again simply makes a
51
- * new handle.
52
- */
53
- timedOut;
54
- /**
55
- * There is no session — the viewer is signed out (`sign in to train`), or their
56
- * session ended (a bare `UNAUTHORIZED`). Route into `useRequestSignIn()`.
57
- */
58
- signInRequired;
59
- constructor(error, opts) {
60
- super(error);
61
- this.name = 'PrepareTrainingDatasetError';
62
- this.timedOut = opts?.timedOut === true;
63
- if (isPrepareTrainingDatasetErrorCode(error))
64
- this.code = error;
65
- this.signInRequired = error === 'sign in to train' || error === 'UNAUTHORIZED';
66
- }
67
- }
68
- /**
69
- * Step 1 of the App Blocks `kind: 'training'` flow: turn the viewer's own images
70
- * and your captions into a dataset handle for `WorkflowBodyTraining.datasetId`,
71
- * through the host-mediated `PREPARE_TRAINING_DATASET` → `TRAINING_DATASET_RESULT`
72
- * bridge.
73
- *
74
- * 🔴 AVAILABILITY: page apps only, behind the host flag `app-blocks-training-kind`
75
- * (ships off), with `ai:write:budgeted` declared in the manifest and granted by a
76
- * signed-in viewer. Refused from `dev:live` and from review sessions — build the
77
- * flow against the mock host. See `WorkflowBodyTraining` in `@civitai/app-sdk`.
78
- *
79
- * Only the viewer's OWN scanned, unflagged images within the page's maturity
80
- * ceiling are admitted; captions are moderated. Bounds: 1–50 items, captions up
81
- * to 1,000 characters (`BLOCK_TRAINING_DATASET_MAX_ITEMS`,
82
- * `BLOCK_TRAINING_CAPTION_MAX_CHARS`); the host refuses anything else with
83
- * `invalid training dataset` before calling the server.
84
- *
85
- * No dialog and no charge. When the token lacks `ai:write:budgeted`, the call
86
- * opens the host's consent prompt and retries once on a grant (opt out with
87
- * `{ autoRequestConsent: false }`).
88
- *
89
- * Sent under the 120s server-work bound, not the 30s protocol default: the server
90
- * imports up to 50 images into the orchestrator within its own 60s budget.
91
- *
92
- * @example
93
- * const { prepareDataset } = usePrepareTrainingDataset();
94
- * try {
95
- * const dataset = await prepareDataset(
96
- * picked.map((img) => ({ imageId: img.id, caption: img.caption })),
97
- * );
98
- * if (dataset.rejected.length > 0) showNotice(`${dataset.rejected.length} image(s) left out`);
99
- * } catch (e) {
100
- * // Includes "none of the requested images can be used for training".
101
- * if (e instanceof PrepareTrainingDatasetError) showError(e.message);
102
- * }
103
- */
104
- export function usePrepareTrainingDataset() {
105
- useTransportConnection();
106
- const [pending, setPending] = useState(false);
107
- const [error, setError] = useState(null);
108
- // The consent wait can outlive the component; see `useCreatePostFromApp`.
109
- const mountedRef = useRef(true);
110
- useEffect(() => {
111
- mountedRef.current = true;
112
- return () => {
113
- mountedRef.current = false;
114
- };
115
- }, []);
116
- /** ONE round-trip + its result contract. Re-invoked verbatim on a consent retry. */
117
- const prepareOnce = useCallback(async (items) => {
118
- const reply = await sendTypedRequest(getTransport(), { type: 'PREPARE_TRAINING_DATASET', payload: { items } }, 'TRAINING_DATASET_RESULT', { timeoutMs: WORKFLOW_REQUEST_TIMEOUT_MS }).catch((err) => {
119
- // Stamped INSIDE the closure `withConsentRetry` re-invokes, so its rule 4
120
- // sees it: a 120s wait is not repeated behind a consent prompt.
121
- if (err instanceof RequestTimeoutError) {
122
- throw new PrepareTrainingDatasetError(err.message, { timedOut: true });
123
- }
124
- throw err instanceof Error ? err : new Error(String(err));
125
- });
126
- if (reply.error || !reply.result) {
127
- // `||`, not `??`: the validator shape-checks `error`, so `''` is a valid
128
- // reply that must not become an Error with an empty message. The
129
- // fallback is deliberately NOT a host code — `invalid training dataset`
130
- // would claim "nothing reached the server", which an empty error does
131
- // not tell us.
132
- throw new PrepareTrainingDatasetError(reply.error || 'training dataset could not be prepared');
133
- }
134
- return reply.result;
135
- }, []);
136
- const prepareDataset = useCallback(async (items, options) => {
137
- if (mountedRef.current) {
138
- setPending(true);
139
- setError(null);
140
- }
141
- try {
142
- return await withConsentRetry(getTransport(), TRAINING_SCOPES, () => prepareOnce(items), options, () => mountedRef.current);
143
- }
144
- catch (err) {
145
- const wrapped = err instanceof PrepareTrainingDatasetError
146
- ? err
147
- : new PrepareTrainingDatasetError(err instanceof Error ? err.message : String(err));
148
- if (mountedRef.current)
149
- setError(wrapped);
150
- throw wrapped;
151
- }
152
- finally {
153
- if (mountedRef.current)
154
- setPending(false);
155
- }
156
- }, [prepareOnce]);
157
- return { prepareDataset, pending, error };
158
- }
159
- //# sourceMappingURL=usePrepareTrainingDataset.js.map