@civitai/blocks-react 0.63.2 → 0.65.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/README.md +188 -6
- package/dist/hooks/useBuzzWorkflow.d.ts +20 -0
- package/dist/hooks/useBuzzWorkflow.js +32 -12
- package/dist/hooks/useCreatePostFromApp.d.ts +7 -2
- package/dist/hooks/useCreatePostFromApp.js +8 -3
- package/dist/hooks/useNestedDocument.d.ts +73 -0
- package/dist/hooks/useNestedDocument.js +160 -0
- package/dist/hooks/usePrepareTrainingDataset.d.ts +99 -0
- package/dist/hooks/usePrepareTrainingDataset.js +157 -0
- package/dist/hooks/usePublishGenerationOutputs.d.ts +6 -0
- package/dist/hooks/usePublishGenerationOutputs.js +6 -0
- package/dist/hooks/useRunTraining.d.ts +151 -0
- package/dist/hooks/useRunTraining.js +212 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/internal/liveHost.d.ts +18 -2
- package/dist/internal/liveHost.js +89 -23
- package/dist/internal/mockHost.d.ts +146 -2
- package/dist/internal/mockHost.js +309 -0
- package/dist/transport/requestTimeouts.d.ts +19 -0
- package/dist/transport/requestTimeouts.js +30 -0
- package/dist/transport/validate.d.ts +27 -0
- package/dist/transport/validate.js +189 -0
- package/package.json +5 -5
|
@@ -0,0 +1,99 @@
|
|
|
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
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* The consent-gated scope the training flow requires — `ai:write:budgeted`. The
|
|
9
|
+
* server refuses a dataset request from a token without it (it is the spend
|
|
10
|
+
* scope, and preparing is gated exactly like the run it prepares for). From
|
|
11
|
+
* {@link BLOCK_SCOPES}, never a literal: a typo would make the automatic consent
|
|
12
|
+
* prompt silently do nothing.
|
|
13
|
+
*/
|
|
14
|
+
const TRAINING_SCOPES = [BLOCK_SCOPES.AI_WRITE_BUDGETED];
|
|
15
|
+
/**
|
|
16
|
+
* The host's CLOSED refusal codes for `PREPARE_TRAINING_DATASET`, as a runtime
|
|
17
|
+
* list. Mirrors civitai/civitai's `PREPARE_TRAINING_DATASET_HOST_ERRORS`.
|
|
18
|
+
*
|
|
19
|
+
* 🔴 THE ERROR CHANNEL IS NOT AN ENUM: it also carries free-text server
|
|
20
|
+
* messages, so "is this a code?" is a membership question at runtime — see
|
|
21
|
+
* {@link isPrepareTrainingDatasetErrorCode}.
|
|
22
|
+
*/
|
|
23
|
+
export const PREPARE_TRAINING_DATASET_ERROR_CODES = [
|
|
24
|
+
'review-mode',
|
|
25
|
+
'block is not ready',
|
|
26
|
+
'sign in to train',
|
|
27
|
+
'invalid training dataset',
|
|
28
|
+
'no block token',
|
|
29
|
+
];
|
|
30
|
+
const CODE_SET = new Set(PREPARE_TRAINING_DATASET_ERROR_CODES);
|
|
31
|
+
/** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
|
|
32
|
+
export function isPrepareTrainingDatasetErrorCode(error) {
|
|
33
|
+
return CODE_SET.has(error);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A dataset-preparation failure.
|
|
37
|
+
*
|
|
38
|
+
* `.code` is the host's refusal code when the HOST refused, `undefined` for a
|
|
39
|
+
* server message or a transport timeout. Check `.timedOut` before rendering
|
|
40
|
+
* `.message` — a timeout's message is an SDK-internal string.
|
|
41
|
+
*
|
|
42
|
+
* Preparing charges nothing, so no failure here costs the viewer Buzz.
|
|
43
|
+
*/
|
|
44
|
+
export class PrepareTrainingDatasetError extends Error {
|
|
45
|
+
/** The closed host refusal code, or `undefined` for a server/transport error. */
|
|
46
|
+
code;
|
|
47
|
+
/**
|
|
48
|
+
* No reply arrived within the bound. The server may still have prepared a
|
|
49
|
+
* dataset; nothing was charged either way, and preparing again simply makes a
|
|
50
|
+
* new handle.
|
|
51
|
+
*/
|
|
52
|
+
timedOut;
|
|
53
|
+
/**
|
|
54
|
+
* There is no session — the viewer is signed out (`sign in to train`), or their
|
|
55
|
+
* session ended (a bare `UNAUTHORIZED`). Route into `useRequestSignIn()`.
|
|
56
|
+
*/
|
|
57
|
+
signInRequired;
|
|
58
|
+
constructor(error, opts) {
|
|
59
|
+
super(error);
|
|
60
|
+
this.name = 'PrepareTrainingDatasetError';
|
|
61
|
+
this.timedOut = opts?.timedOut === true;
|
|
62
|
+
if (isPrepareTrainingDatasetErrorCode(error))
|
|
63
|
+
this.code = error;
|
|
64
|
+
this.signInRequired = error === 'sign in to train' || error === 'UNAUTHORIZED';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Step 1 of the App Blocks `kind: 'training'` flow: turn the viewer's own images
|
|
69
|
+
* and your captions into a dataset handle for `WorkflowBodyTraining.datasetId`,
|
|
70
|
+
* through the host-mediated `PREPARE_TRAINING_DATASET` → `TRAINING_DATASET_RESULT`
|
|
71
|
+
* bridge.
|
|
72
|
+
*
|
|
73
|
+
* 🔴 AVAILABILITY: page apps only, behind the host flag `app-blocks-training-kind`
|
|
74
|
+
* (ships off), with `ai:write:budgeted` declared in the manifest and granted by a
|
|
75
|
+
* signed-in viewer. Refused from `dev:live` and from review sessions — build the
|
|
76
|
+
* flow against the mock host. See `WorkflowBodyTraining` in `@civitai/app-sdk`.
|
|
77
|
+
*
|
|
78
|
+
* Only the viewer's OWN scanned, unflagged images within the page's maturity
|
|
79
|
+
* ceiling are admitted; captions are moderated. Bounds: 1–50 items, captions up
|
|
80
|
+
* to 1,000 characters (`BLOCK_TRAINING_DATASET_MAX_ITEMS`,
|
|
81
|
+
* `BLOCK_TRAINING_CAPTION_MAX_CHARS`); the host refuses anything else with
|
|
82
|
+
* `invalid training dataset` before calling the server.
|
|
83
|
+
*
|
|
84
|
+
* No dialog and no charge. When the token lacks `ai:write:budgeted`, the call
|
|
85
|
+
* opens the host's consent prompt and retries once on a grant (opt out with
|
|
86
|
+
* `{ autoRequestConsent: false }`).
|
|
87
|
+
*
|
|
88
|
+
* Sent under the 120s server-work bound, not the 30s protocol default: the server
|
|
89
|
+
* imports up to 50 images into the orchestrator within its own 60s budget.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const { prepareDataset } = usePrepareTrainingDataset();
|
|
93
|
+
* try {
|
|
94
|
+
* const dataset = await prepareDataset(
|
|
95
|
+
* picked.map((img) => ({ imageId: img.id, caption: img.caption })),
|
|
96
|
+
* );
|
|
97
|
+
* if (dataset.rejected.length > 0) showNotice(`${dataset.rejected.length} image(s) left out`);
|
|
98
|
+
* } catch (e) {
|
|
99
|
+
* // Includes "none of the requested images can be used for training".
|
|
100
|
+
* if (e instanceof PrepareTrainingDatasetError) showError(e.message);
|
|
101
|
+
* }
|
|
102
|
+
*/
|
|
103
|
+
export function usePrepareTrainingDataset() {
|
|
104
|
+
const [pending, setPending] = useState(false);
|
|
105
|
+
const [error, setError] = useState(null);
|
|
106
|
+
// The consent wait can outlive the component; see `useCreatePostFromApp`.
|
|
107
|
+
const mountedRef = useRef(true);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
mountedRef.current = true;
|
|
110
|
+
return () => {
|
|
111
|
+
mountedRef.current = false;
|
|
112
|
+
};
|
|
113
|
+
}, []);
|
|
114
|
+
/** ONE round-trip + its result contract. Re-invoked verbatim on a consent retry. */
|
|
115
|
+
const prepareOnce = useCallback(async (items) => {
|
|
116
|
+
const reply = await sendTypedRequest(getTransport(), { type: 'PREPARE_TRAINING_DATASET', payload: { items } }, 'TRAINING_DATASET_RESULT', { timeoutMs: WORKFLOW_REQUEST_TIMEOUT_MS }).catch((err) => {
|
|
117
|
+
// Stamped INSIDE the closure `withConsentRetry` re-invokes, so its rule 4
|
|
118
|
+
// sees it: a 120s wait is not repeated behind a consent prompt.
|
|
119
|
+
if (err instanceof RequestTimeoutError) {
|
|
120
|
+
throw new PrepareTrainingDatasetError(err.message, { timedOut: true });
|
|
121
|
+
}
|
|
122
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
123
|
+
});
|
|
124
|
+
if (reply.error || !reply.result) {
|
|
125
|
+
// `||`, not `??`: the validator shape-checks `error`, so `''` is a valid
|
|
126
|
+
// reply that must not become an Error with an empty message. The
|
|
127
|
+
// fallback is deliberately NOT a host code — `invalid training dataset`
|
|
128
|
+
// would claim "nothing reached the server", which an empty error does
|
|
129
|
+
// not tell us.
|
|
130
|
+
throw new PrepareTrainingDatasetError(reply.error || 'training dataset could not be prepared');
|
|
131
|
+
}
|
|
132
|
+
return reply.result;
|
|
133
|
+
}, []);
|
|
134
|
+
const prepareDataset = useCallback(async (items, options) => {
|
|
135
|
+
if (mountedRef.current) {
|
|
136
|
+
setPending(true);
|
|
137
|
+
setError(null);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return await withConsentRetry(getTransport(), TRAINING_SCOPES, () => prepareOnce(items), options, () => mountedRef.current);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const wrapped = err instanceof PrepareTrainingDatasetError
|
|
144
|
+
? err
|
|
145
|
+
: new PrepareTrainingDatasetError(err instanceof Error ? err.message : String(err));
|
|
146
|
+
if (mountedRef.current)
|
|
147
|
+
setError(wrapped);
|
|
148
|
+
throw wrapped;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
if (mountedRef.current)
|
|
152
|
+
setPending(false);
|
|
153
|
+
}
|
|
154
|
+
}, [prepareOnce]);
|
|
155
|
+
return { prepareDataset, pending, error };
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=usePrepareTrainingDataset.js.map
|
|
@@ -49,6 +49,12 @@ export interface UsePublishGenerationOutputs {
|
|
|
49
49
|
* a human the request carries {@link HUMAN_INTERACTION_TIMEOUT_MS}, not the
|
|
50
50
|
* default protocol timeout.
|
|
51
51
|
*
|
|
52
|
+
* `dev:live` refuses this bridge: publishing requires the viewer's signed-in
|
|
53
|
+
* civitai.com session, which the local harness does not have, so `publish()`
|
|
54
|
+
* rejects with a message saying so. Test publishing against the mock host —
|
|
55
|
+
* `createMockHost` or `Harness` from `@civitai/blocks-react/testing`, with the
|
|
56
|
+
* `publishImageIds` / `publishError` options.
|
|
57
|
+
*
|
|
52
58
|
* @example
|
|
53
59
|
* const { publish } = usePublishGenerationOutputs();
|
|
54
60
|
* const imageIds = await publish({ workflowId: w.workflowId, imageIndexes: [0, 2] });
|
|
@@ -16,6 +16,12 @@ import { sendTypedRequest } from '../transport/transport.js';
|
|
|
16
16
|
* a human the request carries {@link HUMAN_INTERACTION_TIMEOUT_MS}, not the
|
|
17
17
|
* default protocol timeout.
|
|
18
18
|
*
|
|
19
|
+
* `dev:live` refuses this bridge: publishing requires the viewer's signed-in
|
|
20
|
+
* civitai.com session, which the local harness does not have, so `publish()`
|
|
21
|
+
* rejects with a message saying so. Test publishing against the mock host —
|
|
22
|
+
* `createMockHost` or `Harness` from `@civitai/blocks-react/testing`, with the
|
|
23
|
+
* `publishImageIds` / `publishError` options.
|
|
24
|
+
*
|
|
19
25
|
* @example
|
|
20
26
|
* const { publish } = usePublishGenerationOutputs();
|
|
21
27
|
* const imageIds = await publish({ workflowId: w.workflowId, imageIndexes: [0, 2] });
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { BlockRunTrainingHostError, BlockTrainingQuote, BlockWorkflowSnapshot, WorkflowBodyTraining } from '@civitai/app-sdk/blocks';
|
|
2
|
+
export type { BlockRunTrainingHostError, BlockTrainingQuote, WorkflowBodyTraining };
|
|
3
|
+
/**
|
|
4
|
+
* A training body ready to RUN: {@link WorkflowBodyTraining} with the `quoteId`
|
|
5
|
+
* its estimate returned. Required here so a body without one is a compile error
|
|
6
|
+
* rather than a host refusal.
|
|
7
|
+
*/
|
|
8
|
+
export type QuotedTrainingBody = WorkflowBodyTraining & {
|
|
9
|
+
quoteId: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The host's CLOSED refusal codes for `RUN_TRAINING`, as a runtime list. Mirrors
|
|
13
|
+
* civitai/civitai's `RUN_TRAINING_HOST_ERRORS`.
|
|
14
|
+
*
|
|
15
|
+
* 🔴 THE ERROR CHANNEL IS NOT AN ENUM: server messages (an expired or used quote,
|
|
16
|
+
* a body that differs from the quoted one) arrive in the same field — see
|
|
17
|
+
* {@link isRunTrainingErrorCode}.
|
|
18
|
+
*/
|
|
19
|
+
export declare const RUN_TRAINING_ERROR_CODES: readonly ["review-mode", "block is not ready", "sign in to train", "invalid training request", "no block token", "declined", "submission-unconfirmed"];
|
|
20
|
+
/**
|
|
21
|
+
* The `code` a {@link RunTrainingError} can carry: one of the host's closed
|
|
22
|
+
* refusal codes, or `'refused'` — assigned by THIS SDK, never sent by the host,
|
|
23
|
+
* for a submit the server refused at a spend cap or as temporarily unavailable
|
|
24
|
+
* (see {@link RunTrainingError.refused}).
|
|
25
|
+
*/
|
|
26
|
+
export type RunTrainingErrorCode = BlockRunTrainingHostError | 'refused';
|
|
27
|
+
/** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
|
|
28
|
+
export declare function isRunTrainingErrorCode(error: string): error is BlockRunTrainingHostError;
|
|
29
|
+
/**
|
|
30
|
+
* A training run that did not come back with a snapshot.
|
|
31
|
+
*
|
|
32
|
+
* 🔴 READ THE FLAGS BEFORE SAYING ANYTHING ABOUT MONEY — they are the only part of
|
|
33
|
+
* this error with a money meaning, and they point in opposite directions:
|
|
34
|
+
*
|
|
35
|
+
* - `.declined` — the viewer dismissed the consent dialog. NO RUN WAS
|
|
36
|
+
* SUBMITTED, guaranteed by the host's consent latch. Render nothing.
|
|
37
|
+
* - `.unconfirmed` — the run MAY BE RUNNING AND CHARGED: the host sent the
|
|
38
|
+
* submit and could not learn its outcome (`submission-unconfirmed`), or no
|
|
39
|
+
* reply arrived at all (`.timedOut` — the viewer may have clicked Train on a
|
|
40
|
+
* dialog this block had stopped waiting for). Do NOT retry automatically and
|
|
41
|
+
* do NOT tell the viewer it failed: check their trainings first
|
|
42
|
+
* (`useAppWorkflows()` lists this app's runs), because re-running the same
|
|
43
|
+
* body after the server did start it is a SECOND, separately charged run.
|
|
44
|
+
* - `.refused` (`code: 'refused'`) — the server REFUSED the submit at a spend
|
|
45
|
+
* cap after the viewer confirmed (the viewer's daily or private-run Buzz cap,
|
|
46
|
+
* the per-app consent budget, the app's daily spend or rate limit, a dev
|
|
47
|
+
* session cap). Its reservation was refunded: NO RUN, nothing charged.
|
|
48
|
+
* `.message` is the server's reason (e.g. `daily Buzz cap reached: …`) and
|
|
49
|
+
* `.snapshot` the refusal it arrived in. Retrying needs a new estimate and
|
|
50
|
+
* usually a later time; buying Buzz does not lift these caps.
|
|
51
|
+
* - `.signInRequired` — no session. Route into `useRequestSignIn()`.
|
|
52
|
+
* - anything else — a refusal before any submit (an expired or used quote, a
|
|
53
|
+
* body that no longer matches its quote, an ineligible image). Estimate again
|
|
54
|
+
* before retrying; `.message` is the host's or server's text.
|
|
55
|
+
*/
|
|
56
|
+
export declare class RunTrainingError extends Error {
|
|
57
|
+
/**
|
|
58
|
+
* The closed host refusal code, `'refused'` for a cap refusal, or `undefined`
|
|
59
|
+
* for a server/transport error.
|
|
60
|
+
*/
|
|
61
|
+
readonly code?: RunTrainingErrorCode;
|
|
62
|
+
/**
|
|
63
|
+
* The server refused the submit (a spend cap, or a temporary-availability deny)
|
|
64
|
+
* and refunded it — no run exists. The quote is used up: estimate again.
|
|
65
|
+
* See the class docs; `.message` is the server's reason.
|
|
66
|
+
*/
|
|
67
|
+
readonly refused: boolean;
|
|
68
|
+
/** The refusal snapshot the host replied with, when {@link refused}. */
|
|
69
|
+
readonly snapshot?: BlockWorkflowSnapshot;
|
|
70
|
+
/** The viewer dismissed the consent dialog — no run was submitted. */
|
|
71
|
+
readonly declined: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* The run may exist and may have been charged — `submission-unconfirmed`, or a
|
|
74
|
+
* transport timeout. Check the viewer's trainings before retrying.
|
|
75
|
+
*/
|
|
76
|
+
readonly unconfirmed: boolean;
|
|
77
|
+
/** No reply arrived within the 10-minute consent bound. Implies {@link unconfirmed}. */
|
|
78
|
+
readonly timedOut: boolean;
|
|
79
|
+
/** There is no session (`sign in to train`, or a bare `UNAUTHORIZED`). */
|
|
80
|
+
readonly signInRequired: boolean;
|
|
81
|
+
constructor(error: string, opts?: {
|
|
82
|
+
timedOut?: boolean;
|
|
83
|
+
refusedSnapshot?: BlockWorkflowSnapshot;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** What {@link useRunTraining} returns. */
|
|
87
|
+
export interface UseRunTraining {
|
|
88
|
+
/**
|
|
89
|
+
* Ask the host to run a quoted training body. Civitai shows the viewer a
|
|
90
|
+
* consent dialog with the server's price and details; on their click it
|
|
91
|
+
* submits, and this resolves with the submitted workflow's snapshot — poll it
|
|
92
|
+
* with `useBuzzWorkflow().watch(snapshot.workflowId)`.
|
|
93
|
+
*
|
|
94
|
+
* REJECTS with a {@link RunTrainingError} — including `declined` (no run),
|
|
95
|
+
* `refused` (a spend-cap or temporary-availability refusal; no run) and
|
|
96
|
+
* `unconfirmed` (a run may
|
|
97
|
+
* exist). Read the flags before rendering anything. A resolved snapshot is
|
|
98
|
+
* always a submitted run with a pollable `workflowId`.
|
|
99
|
+
*/
|
|
100
|
+
runTraining: (body: QuotedTrainingBody) => Promise<BlockWorkflowSnapshot>;
|
|
101
|
+
/** `true` while a request is in flight (including the viewer's dialog). */
|
|
102
|
+
pending: boolean;
|
|
103
|
+
/** The last request's failure, or `null`. Cleared at the start of the next call. */
|
|
104
|
+
error: RunTrainingError | null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Run an App Blocks `kind: 'training'` LoRA training — the only way one starts —
|
|
108
|
+
* through the host-mediated `RUN_TRAINING` → `TRAINING_RESULT` bridge.
|
|
109
|
+
*
|
|
110
|
+
* 🔴 AVAILABILITY: page apps only, behind the host flag `app-blocks-training-kind`
|
|
111
|
+
* (ships off), with `ai:write:budgeted` declared and granted by a signed-in
|
|
112
|
+
* viewer. Refused from `dev:live` and from review sessions — build the flow
|
|
113
|
+
* against the mock host. See `WorkflowBodyTraining` in `@civitai/app-sdk`.
|
|
114
|
+
*
|
|
115
|
+
* The flow: `usePrepareTrainingDataset()` → `useBuzzWorkflow().estimate(body)`
|
|
116
|
+
* (its `trainingQuote.quoteId`) → `runTraining({ ...body, quoteId })` →
|
|
117
|
+
* `useBuzzWorkflow().watch(workflowId)`. Change nothing in the body between the
|
|
118
|
+
* estimate and the run: the server checks it against the quoted one.
|
|
119
|
+
*
|
|
120
|
+
* 🔴 THE CONSENT DIALOG IS CIVITAI'S, AND IT IS THE CHARGE. Every number on it is
|
|
121
|
+
* read back from the server's stored quote — never from your body. A run may cost
|
|
122
|
+
* more than the token's per-call budget (the viewer confirms the exact price), up
|
|
123
|
+
* to `BLOCK_TRAINING_MAX_BUZZ_PER_RUN` (5,000 Buzz). Images are the viewer's own.
|
|
124
|
+
*
|
|
125
|
+
* 🔴 NO AUTOMATIC CONSENT RETRY, unlike the other consent-gated hooks — on
|
|
126
|
+
* purpose. `RUN_TRAINING` carries no idempotency key and has an outcome
|
|
127
|
+
* (`submission-unconfirmed`) where a run may exist, so this hook never re-sends.
|
|
128
|
+
* It is also unreachable in practice: a block holding a `quoteId` got it from an
|
|
129
|
+
* estimate that already required `ai:write:budgeted`.
|
|
130
|
+
*
|
|
131
|
+
* Sent under the 10-minute human-interaction bound: the host replies only when
|
|
132
|
+
* the viewer clicks or dismisses its dialog.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* const { estimate, watch } = useBuzzWorkflow();
|
|
136
|
+
* const { runTraining } = useRunTraining();
|
|
137
|
+
* const quote = (await estimate(body)).trainingQuote!;
|
|
138
|
+
* try {
|
|
139
|
+
* const started = await runTraining({ ...body, quoteId: quote.quoteId });
|
|
140
|
+
* await watch(started.workflowId, { onUpdate: render });
|
|
141
|
+
* } catch (e) {
|
|
142
|
+
* if (!(e instanceof RunTrainingError)) throw e;
|
|
143
|
+
* if (e.declined) return; // no run — say nothing
|
|
144
|
+
* if (e.signInRequired) return requestSignIn();
|
|
145
|
+
* if (e.unconfirmed) return showCheckYourTrainings(); // may be running — never auto-retry
|
|
146
|
+
* if (e.refused) return showError(e.message); // a cap / availability refusal; no run, nothing charged
|
|
147
|
+
* showError('Could not start training. Get a new price and try again.');
|
|
148
|
+
* }
|
|
149
|
+
*/
|
|
150
|
+
export declare function useRunTraining(): UseRunTraining;
|
|
151
|
+
//# sourceMappingURL=useRunTraining.d.ts.map
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
|
+
import { getTransport } from '../transport/singleton.js';
|
|
4
|
+
import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
|
|
5
|
+
/**
|
|
6
|
+
* The host's CLOSED refusal codes for `RUN_TRAINING`, as a runtime list. Mirrors
|
|
7
|
+
* civitai/civitai's `RUN_TRAINING_HOST_ERRORS`.
|
|
8
|
+
*
|
|
9
|
+
* 🔴 THE ERROR CHANNEL IS NOT AN ENUM: server messages (an expired or used quote,
|
|
10
|
+
* a body that differs from the quoted one) arrive in the same field — see
|
|
11
|
+
* {@link isRunTrainingErrorCode}.
|
|
12
|
+
*/
|
|
13
|
+
export const RUN_TRAINING_ERROR_CODES = [
|
|
14
|
+
'review-mode',
|
|
15
|
+
'block is not ready',
|
|
16
|
+
'sign in to train',
|
|
17
|
+
'invalid training request',
|
|
18
|
+
'no block token',
|
|
19
|
+
'declined',
|
|
20
|
+
'submission-unconfirmed',
|
|
21
|
+
];
|
|
22
|
+
const CODE_SET = new Set(RUN_TRAINING_ERROR_CODES);
|
|
23
|
+
/**
|
|
24
|
+
* The `workflowId` the host's training submit stamps on a snapshot that is NOT a
|
|
25
|
+
* run — its cap refusals resolve `{ workflowId: 'failed', status: 'failed', cost,
|
|
26
|
+
* error }` (`submitTrainingWorkflow` in civitai/civitai `blocks.router.ts`).
|
|
27
|
+
* Compared with `===`: a real orchestrator id is a workflow that may exist.
|
|
28
|
+
*/
|
|
29
|
+
const HOST_REFUSAL_WORKFLOW_ID = 'failed';
|
|
30
|
+
/** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
|
|
31
|
+
export function isRunTrainingErrorCode(error) {
|
|
32
|
+
return CODE_SET.has(error);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A training run that did not come back with a snapshot.
|
|
36
|
+
*
|
|
37
|
+
* 🔴 READ THE FLAGS BEFORE SAYING ANYTHING ABOUT MONEY — they are the only part of
|
|
38
|
+
* this error with a money meaning, and they point in opposite directions:
|
|
39
|
+
*
|
|
40
|
+
* - `.declined` — the viewer dismissed the consent dialog. NO RUN WAS
|
|
41
|
+
* SUBMITTED, guaranteed by the host's consent latch. Render nothing.
|
|
42
|
+
* - `.unconfirmed` — the run MAY BE RUNNING AND CHARGED: the host sent the
|
|
43
|
+
* submit and could not learn its outcome (`submission-unconfirmed`), or no
|
|
44
|
+
* reply arrived at all (`.timedOut` — the viewer may have clicked Train on a
|
|
45
|
+
* dialog this block had stopped waiting for). Do NOT retry automatically and
|
|
46
|
+
* do NOT tell the viewer it failed: check their trainings first
|
|
47
|
+
* (`useAppWorkflows()` lists this app's runs), because re-running the same
|
|
48
|
+
* body after the server did start it is a SECOND, separately charged run.
|
|
49
|
+
* - `.refused` (`code: 'refused'`) — the server REFUSED the submit at a spend
|
|
50
|
+
* cap after the viewer confirmed (the viewer's daily or private-run Buzz cap,
|
|
51
|
+
* the per-app consent budget, the app's daily spend or rate limit, a dev
|
|
52
|
+
* session cap). Its reservation was refunded: NO RUN, nothing charged.
|
|
53
|
+
* `.message` is the server's reason (e.g. `daily Buzz cap reached: …`) and
|
|
54
|
+
* `.snapshot` the refusal it arrived in. Retrying needs a new estimate and
|
|
55
|
+
* usually a later time; buying Buzz does not lift these caps.
|
|
56
|
+
* - `.signInRequired` — no session. Route into `useRequestSignIn()`.
|
|
57
|
+
* - anything else — a refusal before any submit (an expired or used quote, a
|
|
58
|
+
* body that no longer matches its quote, an ineligible image). Estimate again
|
|
59
|
+
* before retrying; `.message` is the host's or server's text.
|
|
60
|
+
*/
|
|
61
|
+
export class RunTrainingError extends Error {
|
|
62
|
+
/**
|
|
63
|
+
* The closed host refusal code, `'refused'` for a cap refusal, or `undefined`
|
|
64
|
+
* for a server/transport error.
|
|
65
|
+
*/
|
|
66
|
+
code;
|
|
67
|
+
/**
|
|
68
|
+
* The server refused the submit (a spend cap, or a temporary-availability deny)
|
|
69
|
+
* and refunded it — no run exists. The quote is used up: estimate again.
|
|
70
|
+
* See the class docs; `.message` is the server's reason.
|
|
71
|
+
*/
|
|
72
|
+
refused;
|
|
73
|
+
/** The refusal snapshot the host replied with, when {@link refused}. */
|
|
74
|
+
snapshot;
|
|
75
|
+
/** The viewer dismissed the consent dialog — no run was submitted. */
|
|
76
|
+
declined;
|
|
77
|
+
/**
|
|
78
|
+
* The run may exist and may have been charged — `submission-unconfirmed`, or a
|
|
79
|
+
* transport timeout. Check the viewer's trainings before retrying.
|
|
80
|
+
*/
|
|
81
|
+
unconfirmed;
|
|
82
|
+
/** No reply arrived within the 10-minute consent bound. Implies {@link unconfirmed}. */
|
|
83
|
+
timedOut;
|
|
84
|
+
/** There is no session (`sign in to train`, or a bare `UNAUTHORIZED`). */
|
|
85
|
+
signInRequired;
|
|
86
|
+
constructor(error, opts) {
|
|
87
|
+
super(error);
|
|
88
|
+
this.name = 'RunTrainingError';
|
|
89
|
+
this.timedOut = opts?.timedOut === true;
|
|
90
|
+
this.refused = opts?.refusedSnapshot !== undefined;
|
|
91
|
+
if (opts?.refusedSnapshot !== undefined) {
|
|
92
|
+
this.code = 'refused';
|
|
93
|
+
this.snapshot = opts.refusedSnapshot;
|
|
94
|
+
}
|
|
95
|
+
else if (isRunTrainingErrorCode(error)) {
|
|
96
|
+
this.code = error;
|
|
97
|
+
}
|
|
98
|
+
this.declined = error === 'declined';
|
|
99
|
+
this.unconfirmed = this.timedOut || error === 'submission-unconfirmed';
|
|
100
|
+
this.signInRequired = error === 'sign in to train' || error === 'UNAUTHORIZED';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Run an App Blocks `kind: 'training'` LoRA training — the only way one starts —
|
|
105
|
+
* through the host-mediated `RUN_TRAINING` → `TRAINING_RESULT` bridge.
|
|
106
|
+
*
|
|
107
|
+
* 🔴 AVAILABILITY: page apps only, behind the host flag `app-blocks-training-kind`
|
|
108
|
+
* (ships off), with `ai:write:budgeted` declared and granted by a signed-in
|
|
109
|
+
* viewer. Refused from `dev:live` and from review sessions — build the flow
|
|
110
|
+
* against the mock host. See `WorkflowBodyTraining` in `@civitai/app-sdk`.
|
|
111
|
+
*
|
|
112
|
+
* The flow: `usePrepareTrainingDataset()` → `useBuzzWorkflow().estimate(body)`
|
|
113
|
+
* (its `trainingQuote.quoteId`) → `runTraining({ ...body, quoteId })` →
|
|
114
|
+
* `useBuzzWorkflow().watch(workflowId)`. Change nothing in the body between the
|
|
115
|
+
* estimate and the run: the server checks it against the quoted one.
|
|
116
|
+
*
|
|
117
|
+
* 🔴 THE CONSENT DIALOG IS CIVITAI'S, AND IT IS THE CHARGE. Every number on it is
|
|
118
|
+
* read back from the server's stored quote — never from your body. A run may cost
|
|
119
|
+
* more than the token's per-call budget (the viewer confirms the exact price), up
|
|
120
|
+
* to `BLOCK_TRAINING_MAX_BUZZ_PER_RUN` (5,000 Buzz). Images are the viewer's own.
|
|
121
|
+
*
|
|
122
|
+
* 🔴 NO AUTOMATIC CONSENT RETRY, unlike the other consent-gated hooks — on
|
|
123
|
+
* purpose. `RUN_TRAINING` carries no idempotency key and has an outcome
|
|
124
|
+
* (`submission-unconfirmed`) where a run may exist, so this hook never re-sends.
|
|
125
|
+
* It is also unreachable in practice: a block holding a `quoteId` got it from an
|
|
126
|
+
* estimate that already required `ai:write:budgeted`.
|
|
127
|
+
*
|
|
128
|
+
* Sent under the 10-minute human-interaction bound: the host replies only when
|
|
129
|
+
* the viewer clicks or dismisses its dialog.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* const { estimate, watch } = useBuzzWorkflow();
|
|
133
|
+
* const { runTraining } = useRunTraining();
|
|
134
|
+
* const quote = (await estimate(body)).trainingQuote!;
|
|
135
|
+
* try {
|
|
136
|
+
* const started = await runTraining({ ...body, quoteId: quote.quoteId });
|
|
137
|
+
* await watch(started.workflowId, { onUpdate: render });
|
|
138
|
+
* } catch (e) {
|
|
139
|
+
* if (!(e instanceof RunTrainingError)) throw e;
|
|
140
|
+
* if (e.declined) return; // no run — say nothing
|
|
141
|
+
* if (e.signInRequired) return requestSignIn();
|
|
142
|
+
* if (e.unconfirmed) return showCheckYourTrainings(); // may be running — never auto-retry
|
|
143
|
+
* if (e.refused) return showError(e.message); // a cap / availability refusal; no run, nothing charged
|
|
144
|
+
* showError('Could not start training. Get a new price and try again.');
|
|
145
|
+
* }
|
|
146
|
+
*/
|
|
147
|
+
export function useRunTraining() {
|
|
148
|
+
const [pending, setPending] = useState(false);
|
|
149
|
+
const [error, setError] = useState(null);
|
|
150
|
+
// This request can outlive the component by up to ten minutes (the dialog may
|
|
151
|
+
// stay open), so a `pending` toggled on an unmounted control is state nobody
|
|
152
|
+
// can clear.
|
|
153
|
+
const mountedRef = useRef(true);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
mountedRef.current = true;
|
|
156
|
+
return () => {
|
|
157
|
+
mountedRef.current = false;
|
|
158
|
+
};
|
|
159
|
+
}, []);
|
|
160
|
+
const runTraining = useCallback(async (body) => {
|
|
161
|
+
if (mountedRef.current) {
|
|
162
|
+
setPending(true);
|
|
163
|
+
setError(null);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const reply = await sendTypedRequest(getTransport(), { type: 'RUN_TRAINING', payload: { body } }, 'TRAINING_RESULT', { timeoutMs: HUMAN_INTERACTION_TIMEOUT_MS }).catch((err) => {
|
|
167
|
+
if (err instanceof RequestTimeoutError) {
|
|
168
|
+
throw new RunTrainingError(err.message, { timedOut: true });
|
|
169
|
+
}
|
|
170
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
171
|
+
});
|
|
172
|
+
if (reply.error || !reply.snapshot) {
|
|
173
|
+
// `||`, not `??`: `error: ''` is a shape-valid reply. Its fallback is the
|
|
174
|
+
// CAUTIOUS code — with neither a snapshot nor a reason, nothing says a
|
|
175
|
+
// run was not started, so the block must not be told it was safe to
|
|
176
|
+
// retry.
|
|
177
|
+
throw new RunTrainingError(reply.error || 'submission-unconfirmed');
|
|
178
|
+
}
|
|
179
|
+
// 🔴 A RESOLVED SNAPSHOT IS NOT ALWAYS A RUN. The host's training submit
|
|
180
|
+
// answers its spend-cap and temporary-availability refusals (refunded,
|
|
181
|
+
// nothing started) with a
|
|
182
|
+
// resolved `{ workflowId: 'failed', status: 'failed', cost, error }`.
|
|
183
|
+
// Resolving that would hand the caller a workflow id to `watch()` that
|
|
184
|
+
// names nothing, and drop the reason. Exact match on the host's
|
|
185
|
+
// sentinel: a failed snapshot with a REAL id is a workflow that may
|
|
186
|
+
// exist, and keeps resolving so it can be watched.
|
|
187
|
+
if (reply.snapshot.workflowId === HOST_REFUSAL_WORKFLOW_ID &&
|
|
188
|
+
reply.snapshot.status === 'failed') {
|
|
189
|
+
throw new RunTrainingError(reply.snapshot.error || 'training run refused', {
|
|
190
|
+
refusedSnapshot: reply.snapshot,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return reply.snapshot;
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const wrapped = err instanceof RunTrainingError
|
|
197
|
+
? err
|
|
198
|
+
: new RunTrainingError(err instanceof Error ? err.message : String(err));
|
|
199
|
+
if (mountedRef.current)
|
|
200
|
+
setError(wrapped);
|
|
201
|
+
// Thrown even when unmounted: the caller's `await` must still learn the
|
|
202
|
+
// outcome of a possibly-charged run.
|
|
203
|
+
throw wrapped;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
if (mountedRef.current)
|
|
207
|
+
setPending(false);
|
|
208
|
+
}
|
|
209
|
+
}, []);
|
|
210
|
+
return { runTraining, pending, error };
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=useRunTraining.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,10 @@ export { useCollectionFollow, CollectionFollowError, COLLECTION_FOLLOW_ERROR_COD
|
|
|
67
67
|
export type { UseCollectionFollow } from './hooks/useCollectionFollow.js';
|
|
68
68
|
export { useCreatePostFromApp, CreatePostError, CREATE_POST_ERROR_CODES, isCreatePostErrorCode, } from './hooks/useCreatePostFromApp.js';
|
|
69
69
|
export type { UseCreatePostFromApp, BlockPostSource, BlockCreatePostRequest, BlockCreatePostResult, BlockCreatePostHostError, } from './hooks/useCreatePostFromApp.js';
|
|
70
|
+
export { usePrepareTrainingDataset, PrepareTrainingDatasetError, PREPARE_TRAINING_DATASET_ERROR_CODES, isPrepareTrainingDatasetErrorCode, } from './hooks/usePrepareTrainingDataset.js';
|
|
71
|
+
export type { UsePrepareTrainingDataset, BlockPrepareTrainingDatasetHostError, BlockTrainingDatasetItem, BlockTrainingDatasetResult, BlockTrainingRejectionReason, } from './hooks/usePrepareTrainingDataset.js';
|
|
72
|
+
export { useRunTraining, RunTrainingError, RUN_TRAINING_ERROR_CODES, isRunTrainingErrorCode, } from './hooks/useRunTraining.js';
|
|
73
|
+
export type { UseRunTraining, QuotedTrainingBody, RunTrainingErrorCode, BlockRunTrainingHostError, BlockTrainingQuote, WorkflowBodyTraining, } from './hooks/useRunTraining.js';
|
|
70
74
|
export type { BlockBuzzTransaction, BlockBuzzAccount, BlockDailyCompensationResource, BlockWildcardPack, BlockWildcardPackErrorCode, AppWorkflow, AppWorkflowImage, AppWorkflowsParams, BlockGatedImage, BlockCollectionFollowErrorCode, BlockCollectionFollowResult, } from '@civitai/app-sdk/blocks';
|
|
71
75
|
export { useCheckpointPicker } from './hooks/useCheckpointPicker.js';
|
|
72
76
|
export type { UseCheckpointPicker } from './hooks/useCheckpointPicker.js';
|
|
@@ -98,6 +102,8 @@ export type { SfwGateProps } from './hooks/SfwGate.js';
|
|
|
98
102
|
export { useDirectLoad } from './hooks/useDirectLoad.js';
|
|
99
103
|
export type { UseDirectLoad, UseDirectLoadOptions } from './hooks/useDirectLoad.js';
|
|
100
104
|
export { hostToRunUrl, DIRECT_LOAD_TIMEOUT_MS } from './transport/directLoad.js';
|
|
105
|
+
export { useNestedDocument } from './hooks/useNestedDocument.js';
|
|
106
|
+
export type { NestedDocumentStatus, UseNestedDocument, UseNestedDocumentOptions, } from './hooks/useNestedDocument.js';
|
|
101
107
|
export { useAppStorage } from './hooks/useAppStorage.js';
|
|
102
108
|
export type { AppStorageKeyEntry, AppStorageListResult, AppStorageQuota, UseAppStorage, } from './hooks/useAppStorage.js';
|
|
103
109
|
export { useSharedStorage } from './hooks/useSharedStorage.js';
|