@lotics/app-sdk 0.56.1 → 0.57.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/AGENTS.md +1 -1
- package/dist/src/hooks.d.ts +14 -2
- package/dist/src/hooks.js +7 -5
- package/dist/src/rpc.js +3 -3
- package/dist/src/upload/optimize.d.ts +28 -5
- package/dist/src/upload/optimize.js +36 -14
- package/dist/src/upload/pipeline.d.ts +3 -0
- package/dist/src/upload/pipeline.js +3 -3
- package/docs/files.md +28 -3
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -17,7 +17,7 @@ signature; open the file.**
|
|
|
17
17
|
| [docs/queries.md](./docs/queries.md) | **The query engine authoring reference** — AST node kinds, per-field-type operator support, filters/params/pruning, free-text search, combining tables (join/union/link/`unnest`/`record_id`), shaping (aggregates, date buckets, windows), runtime refinement bounds, limits & the efficiency playbook. |
|
|
18
18
|
| [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, data discipline, the search-as-you-type + record-picker patterns. |
|
|
19
19
|
| [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract, typed inputs, diff-before-update, locked records, `useOptimistic`. |
|
|
20
|
-
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs, workflow-generated files, preview pairing, filter operators, the server-side delivery bounds. |
|
|
20
|
+
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs, workflow-generated files, preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
|
|
21
21
|
| [docs/members_and_options.md](./docs/members_and_options.md) | People + select options + comments — `useMembers`, `useFieldOptions`, `useViewer`, `useComments`, and the `@lotics/ui` components they feed. |
|
|
22
22
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
23
23
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics, auto query-refetch on chat mutation). **A `file` input carries its own content** — images/PDFs are perceived natively, Word/Excel/CSV/text are materialized into the run; no reader tool to declare. |
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ImageFidelity } from "./upload/optimize.js";
|
|
1
2
|
import { type AiContextValue } from "./rpc.js";
|
|
2
3
|
import { type AgentUIPart, type PendingChoice } from "./agent_stream.js";
|
|
3
4
|
import type { AppWorkflows, AppWorkflowResults, AppQueries, AppAgents, AppAgentResults } from "./types.js";
|
|
@@ -302,8 +303,17 @@ interface FileUploadState {
|
|
|
302
303
|
* Upload one file. Resolves to the stored file; pass `UploadedFile.id` into
|
|
303
304
|
* a `useWorkflow` call to attach it to a record. Rejects on failure — the
|
|
304
305
|
* file is never partially stored.
|
|
306
|
+
*
|
|
307
|
+
* `fidelity` says how much of the image must survive storage; it defaults to
|
|
308
|
+
* `"high"`, which keeps the text of a photographed document legible. Only
|
|
309
|
+
* photographs are affected — a PDF, Word or Excel file is stored untouched at
|
|
310
|
+
* every step. Pass `"standard"` for bulk visual capture (a forty-photo survey,
|
|
311
|
+
* where the volume is what costs you), or `"original"` when the pixels
|
|
312
|
+
* themselves are the evidence.
|
|
305
313
|
*/
|
|
306
|
-
upload: (file: File
|
|
314
|
+
upload: (file: File, options?: {
|
|
315
|
+
fidelity?: ImageFidelity;
|
|
316
|
+
}) => Promise<UploadedFile>;
|
|
307
317
|
/** True while any upload from this hook is in flight. */
|
|
308
318
|
uploading: boolean;
|
|
309
319
|
/** Message of the most recent failed upload, cleared when a new one starts. */
|
|
@@ -341,7 +351,9 @@ interface AttachmentsState {
|
|
|
341
351
|
/** Add picked/pasted/dropped files — each shows its local preview at once and
|
|
342
352
|
* uploads in the background. Picking is the app's choice: wire a button to
|
|
343
353
|
* `@lotics/ui` `pickFiles`, or a paste/drop handler, then call this. */
|
|
344
|
-
add: (files: File[]
|
|
354
|
+
add: (files: File[], options?: {
|
|
355
|
+
fidelity?: ImageFidelity;
|
|
356
|
+
}) => void;
|
|
345
357
|
/** Remove one attachment and revoke its preview URL. */
|
|
346
358
|
remove: (id: string) => void;
|
|
347
359
|
/** Remove all attachments and revoke their preview URLs. */
|
package/dist/src/hooks.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* `useState` — they have nothing to share.
|
|
17
17
|
*/
|
|
18
18
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
19
|
+
import { DEFAULT_IMAGE_FIDELITY } from "./upload/optimize.js";
|
|
19
20
|
import useSWR from "swr";
|
|
20
21
|
import useSWRInfinite from "swr/infinite";
|
|
21
22
|
import { rpc, rpcAgentRun, rpcAgentRunContinue, postHostNotification, subscribeHostRefetch, } from "./rpc.js";
|
|
@@ -242,12 +243,13 @@ export function usePaginatedQuery(alias, params, opts) {
|
|
|
242
243
|
export function useFileUpload() {
|
|
243
244
|
const [inFlight, setInFlight] = useState(0);
|
|
244
245
|
const [error, setError] = useState(null);
|
|
245
|
-
const upload = useCallback(async (file) => {
|
|
246
|
+
const upload = useCallback(async (file, options) => {
|
|
247
|
+
const fidelity = options?.fidelity ?? DEFAULT_IMAGE_FIDELITY;
|
|
246
248
|
setInFlight((n) => n + 1);
|
|
247
249
|
setError(null);
|
|
248
250
|
try {
|
|
249
|
-
const uploaded = await rpc("upload", { file });
|
|
250
|
-
captureAppEvent("app_file_uploaded", { mime_type: file.type });
|
|
251
|
+
const uploaded = await rpc("upload", { file, fidelity });
|
|
252
|
+
captureAppEvent("app_file_uploaded", { mime_type: file.type, fidelity });
|
|
251
253
|
return uploaded;
|
|
252
254
|
}
|
|
253
255
|
catch (err) {
|
|
@@ -287,7 +289,7 @@ let attachSeq = 0;
|
|
|
287
289
|
export function useAttachments() {
|
|
288
290
|
const { upload } = useFileUpload();
|
|
289
291
|
const [files, setFiles] = useState([]);
|
|
290
|
-
const add = useCallback((incoming) => {
|
|
292
|
+
const add = useCallback((incoming, options) => {
|
|
291
293
|
for (const file of incoming) {
|
|
292
294
|
const id = `att_${(attachSeq += 1)}`;
|
|
293
295
|
const previewUrl = URL.createObjectURL(file);
|
|
@@ -295,7 +297,7 @@ export function useAttachments() {
|
|
|
295
297
|
...prev,
|
|
296
298
|
{ id, filename: file.name, mime_type: file.type, preview_url: previewUrl, status: "uploading" },
|
|
297
299
|
]);
|
|
298
|
-
upload(file)
|
|
300
|
+
upload(file, options)
|
|
299
301
|
.then((uploaded) => setFiles((prev) => prev.map((f) => (f.id === id ? { ...f, status: "ready", file_id: uploaded.id } : f))))
|
|
300
302
|
.catch(() => setFiles((prev) => prev.map((f) => (f.id === id ? { ...f, status: "error" } : f))));
|
|
301
303
|
}
|
package/dist/src/rpc.js
CHANGED
|
@@ -525,7 +525,7 @@ function rpcStandalone(op, payload) {
|
|
|
525
525
|
case "agentRun.cancel":
|
|
526
526
|
return standaloneAgentRunCancel(payload);
|
|
527
527
|
case "upload":
|
|
528
|
-
return standaloneUpload(payload.file);
|
|
528
|
+
return standaloneUpload(payload.file, payload.fidelity);
|
|
529
529
|
case "members":
|
|
530
530
|
return standaloneMembers(payload);
|
|
531
531
|
case "context":
|
|
@@ -705,7 +705,7 @@ async function standaloneAgentRunCancel(p) {
|
|
|
705
705
|
});
|
|
706
706
|
return { ok: true };
|
|
707
707
|
}
|
|
708
|
-
async function standaloneUpload(file) {
|
|
708
|
+
async function standaloneUpload(file, fidelity) {
|
|
709
709
|
if (!(file instanceof File)) {
|
|
710
710
|
throw new Error("upload payload must include a File");
|
|
711
711
|
}
|
|
@@ -713,6 +713,6 @@ async function standaloneUpload(file) {
|
|
|
713
713
|
const uploaded = await runUploadPipeline(file, {
|
|
714
714
|
initUpload: (input) => apiCall("POST", `/v1/apps/${app_id}/files/upload-url`, input, { appId: app_id }),
|
|
715
715
|
completeUpload: (input) => apiCall("POST", `/v1/apps/${app_id}/files/complete`, input, { appId: app_id }),
|
|
716
|
-
});
|
|
716
|
+
}, { fidelity });
|
|
717
717
|
return uploaded;
|
|
718
718
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Browser-side image compression for app uploads.
|
|
3
3
|
*
|
|
4
4
|
* Phone-camera photos are huge (4–10 MB HEIC/JPEG) and no document needs
|
|
5
|
-
* megapixels. Resize to ≤
|
|
6
|
-
*
|
|
5
|
+
* megapixels. Resize to ≤2000px on the long edge — above the standard vision
|
|
6
|
+
* tier's 1568px so a high-resolution reader keeps real detail, but under the
|
|
7
|
+
* limit that rejects requests carrying more than 20 images — and
|
|
7
8
|
* re-encode as JPEG at q=0.9, which keeps small glyphs (container numbers,
|
|
8
9
|
* invoice lines) legible to a reader and to an extraction agent.
|
|
9
10
|
*
|
|
@@ -13,10 +14,32 @@
|
|
|
13
14
|
*
|
|
14
15
|
* Ported from `frontend/lib/upload_file_optimization.ts` so public-app forms
|
|
15
16
|
* get the same mobile-friendly upload behavior as the in-Lotics UI. Kept
|
|
16
|
-
* dependency-free (no logger, no
|
|
17
|
+
* dependency-free (no logger, no ImageFidelity abstraction) so the SDK ships
|
|
17
18
|
* as a single drop-in.
|
|
18
19
|
*/
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* How faithful the STORED image must be — a closed ordinal scale, not a taxonomy
|
|
22
|
+
* of subjects, so a caller can read the value and know what it costs.
|
|
23
|
+
* `@lotics/shared/image_policy` carries the same scale and the reasoning.
|
|
24
|
+
*/
|
|
25
|
+
export type ImageFidelity = "original" | "high" | "standard";
|
|
26
|
+
/**
|
|
27
|
+
* What an upload defaults to when the caller says nothing. MIRRORS
|
|
28
|
+
* `@lotics/shared/image_policy` — this package ships dependency-free and cannot
|
|
29
|
+
* import it — and a test fails when the two drift.
|
|
30
|
+
*
|
|
31
|
+
* `high` because the surfaces that do not think about this mostly carry text; a
|
|
32
|
+
* caller collecting in volume declares `"standard"` and pays a fraction as much.
|
|
33
|
+
*/
|
|
34
|
+
export declare const DEFAULT_IMAGE_FIDELITY: ImageFidelity;
|
|
35
|
+
export interface ImagePolicy {
|
|
36
|
+
maxDimensionPx: number;
|
|
37
|
+
jpegQuality: number;
|
|
38
|
+
}
|
|
39
|
+
/** The mirror, exposed so the drift guard can compare it with the shared
|
|
40
|
+
* source of truth. Not part of the package's public surface. */
|
|
41
|
+
export declare const IMAGE_POLICIES_FOR_TEST: Record<"high" | "standard", ImagePolicy>;
|
|
42
|
+
export type OptimizationReason = "optimized" | "skipped_original" | "skipped_unsupported_format" | "skipped_environment_unsupported" | "skipped_decode_unavailable" | "skipped_invalid_dimensions" | "skipped_small_dimensions" | "skipped_canvas_unavailable" | "skipped_canvas_type_mismatch";
|
|
20
43
|
export interface OptimizationResult {
|
|
21
44
|
file: File;
|
|
22
45
|
optimized: boolean;
|
|
@@ -28,4 +51,4 @@ export interface OptimizationResult {
|
|
|
28
51
|
targetWidth: number;
|
|
29
52
|
targetHeight: number;
|
|
30
53
|
}
|
|
31
|
-
export declare function optimizeImageForUpload(file: File): Promise<OptimizationResult>;
|
|
54
|
+
export declare function optimizeImageForUpload(file: File, fidelity?: ImageFidelity): Promise<OptimizationResult>;
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Browser-side image compression for app uploads.
|
|
3
3
|
*
|
|
4
4
|
* Phone-camera photos are huge (4–10 MB HEIC/JPEG) and no document needs
|
|
5
|
-
* megapixels. Resize to ≤
|
|
6
|
-
*
|
|
5
|
+
* megapixels. Resize to ≤2000px on the long edge — above the standard vision
|
|
6
|
+
* tier's 1568px so a high-resolution reader keeps real detail, but under the
|
|
7
|
+
* limit that rejects requests carrying more than 20 images — and
|
|
7
8
|
* re-encode as JPEG at q=0.9, which keeps small glyphs (container numbers,
|
|
8
9
|
* invoice lines) legible to a reader and to an extraction agent.
|
|
9
10
|
*
|
|
@@ -13,15 +14,36 @@
|
|
|
13
14
|
*
|
|
14
15
|
* Ported from `frontend/lib/upload_file_optimization.ts` so public-app forms
|
|
15
16
|
* get the same mobile-friendly upload behavior as the in-Lotics UI. Kept
|
|
16
|
-
* dependency-free (no logger, no
|
|
17
|
+
* dependency-free (no logger, no ImageFidelity abstraction) so the SDK ships
|
|
17
18
|
* as a single drop-in.
|
|
18
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* What an upload defaults to when the caller says nothing. MIRRORS
|
|
22
|
+
* `@lotics/shared/image_policy` — this package ships dependency-free and cannot
|
|
23
|
+
* import it — and a test fails when the two drift.
|
|
24
|
+
*
|
|
25
|
+
* `high` because the surfaces that do not think about this mostly carry text; a
|
|
26
|
+
* caller collecting in volume declares `"standard"` and pays a fraction as much.
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_IMAGE_FIDELITY = "high";
|
|
19
29
|
// MIRRORS `@lotics/shared/image_policy` — the SDK ships dependency-free so it
|
|
20
30
|
// cannot import it. Change both together.
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
// · standard — bulk visual evidence, NEVER read by a model, so tokens do not
|
|
32
|
+
// enter it: sized purely for upload time and storage across dozens of files.
|
|
33
|
+
// · high — read by a model or a person. Token cost depends on DIMENSION
|
|
34
|
+
// alone, so this buys quality (free in tokens) and keeps the dimension modest.
|
|
35
|
+
const IMAGE_POLICIES = {
|
|
36
|
+
standard: { maxDimensionPx: 1280, jpegQuality: 0.8 },
|
|
37
|
+
high: { maxDimensionPx: 1568, jpegQuality: 0.95 },
|
|
38
|
+
};
|
|
39
|
+
/** The mirror, exposed so the drift guard can compare it with the shared
|
|
40
|
+
* source of truth. Not part of the package's public surface. */
|
|
41
|
+
export const IMAGE_POLICIES_FOR_TEST = IMAGE_POLICIES;
|
|
23
42
|
const CONVERTIBLE_EXTENSION_PATTERN = /\.(heic|heif|png|webp)$/i;
|
|
24
|
-
export async function optimizeImageForUpload(file) {
|
|
43
|
+
export async function optimizeImageForUpload(file, fidelity = DEFAULT_IMAGE_FIDELITY) {
|
|
44
|
+
if (fidelity === "original")
|
|
45
|
+
return unchanged(file, "skipped_original");
|
|
46
|
+
const policy = IMAGE_POLICIES[fidelity];
|
|
25
47
|
const format = getOptimizableFormat(file);
|
|
26
48
|
const mimeType = format?.outputMimeType;
|
|
27
49
|
const unsupportedReason = getUnsupportedReason(file, mimeType);
|
|
@@ -37,11 +59,11 @@ export async function optimizeImageForUpload(file) {
|
|
|
37
59
|
closeSource(source);
|
|
38
60
|
return unchanged(file, "skipped_invalid_dimensions");
|
|
39
61
|
}
|
|
40
|
-
if (Math.max(width, height) <=
|
|
62
|
+
if (Math.max(width, height) <= policy.maxDimensionPx) {
|
|
41
63
|
closeSource(source);
|
|
42
64
|
return unchanged(file, "skipped_small_dimensions", width, height);
|
|
43
65
|
}
|
|
44
|
-
const { width: targetWidth, height: targetHeight } = scale(width, height);
|
|
66
|
+
const { width: targetWidth, height: targetHeight } = scale(width, height, policy.maxDimensionPx);
|
|
45
67
|
const canvas = document.createElement("canvas");
|
|
46
68
|
canvas.width = targetWidth;
|
|
47
69
|
canvas.height = targetHeight;
|
|
@@ -54,7 +76,7 @@ export async function optimizeImageForUpload(file) {
|
|
|
54
76
|
ctx.imageSmoothingQuality = "high";
|
|
55
77
|
ctx.drawImage(source, 0, 0, targetWidth, targetHeight);
|
|
56
78
|
closeSource(source);
|
|
57
|
-
const blob = await canvasToBlob(canvas, mimeType);
|
|
79
|
+
const blob = await canvasToBlob(canvas, mimeType, policy.jpegQuality);
|
|
58
80
|
if (blob.type !== mimeType) {
|
|
59
81
|
return unchanged(file, "skipped_canvas_type_mismatch", width, height, targetWidth, targetHeight);
|
|
60
82
|
}
|
|
@@ -131,11 +153,11 @@ function closeSource(source) {
|
|
|
131
153
|
if ("close" in source && typeof source.close === "function")
|
|
132
154
|
source.close();
|
|
133
155
|
}
|
|
134
|
-
function scale(width, height) {
|
|
156
|
+
function scale(width, height, maxDimensionPx) {
|
|
135
157
|
const largest = Math.max(width, height);
|
|
136
|
-
if (largest <=
|
|
158
|
+
if (largest <= maxDimensionPx)
|
|
137
159
|
return { width, height };
|
|
138
|
-
const factor =
|
|
160
|
+
const factor = maxDimensionPx / largest;
|
|
139
161
|
return {
|
|
140
162
|
width: Math.max(1, Math.round(width * factor)),
|
|
141
163
|
height: Math.max(1, Math.round(height * factor)),
|
|
@@ -172,7 +194,7 @@ async function loadImage(file) {
|
|
|
172
194
|
image.src = objectUrl;
|
|
173
195
|
});
|
|
174
196
|
}
|
|
175
|
-
async function canvasToBlob(canvas, mimeType) {
|
|
197
|
+
async function canvasToBlob(canvas, mimeType, jpegQuality) {
|
|
176
198
|
return new Promise((resolve, reject) => {
|
|
177
199
|
canvas.toBlob((blob) => {
|
|
178
200
|
if (!blob) {
|
|
@@ -180,6 +202,6 @@ async function canvasToBlob(canvas, mimeType) {
|
|
|
180
202
|
return;
|
|
181
203
|
}
|
|
182
204
|
resolve(blob);
|
|
183
|
-
}, mimeType,
|
|
205
|
+
}, mimeType, jpegQuality);
|
|
184
206
|
});
|
|
185
207
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* timeouts use platform-internal defaults — same conventions as the
|
|
14
14
|
* in-Lotics direct-upload pipeline.
|
|
15
15
|
*/
|
|
16
|
+
import { type ImageFidelity } from "./optimize.js";
|
|
16
17
|
interface UploadInitResponse {
|
|
17
18
|
upload_url: string;
|
|
18
19
|
file_id: string;
|
|
@@ -47,6 +48,8 @@ export interface UploadRpc {
|
|
|
47
48
|
}
|
|
48
49
|
export interface RunUploadPipelineOptions {
|
|
49
50
|
signal?: AbortSignal;
|
|
51
|
+
/** What the surface is collecting — selects the stored image resolution. */
|
|
52
|
+
fidelity?: ImageFidelity;
|
|
50
53
|
}
|
|
51
54
|
export declare function runUploadPipeline(file: File, rpc: UploadRpc, options?: RunUploadPipelineOptions): Promise<CompleteResponseFile>;
|
|
52
55
|
export {};
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
* timeouts use platform-internal defaults — same conventions as the
|
|
14
14
|
* in-Lotics direct-upload pipeline.
|
|
15
15
|
*/
|
|
16
|
-
import { optimizeImageForUpload } from "./optimize.js";
|
|
16
|
+
import { optimizeImageForUpload, DEFAULT_IMAGE_FIDELITY } from "./optimize.js";
|
|
17
17
|
import { putToStorageWithRetry } from "./transport.js";
|
|
18
18
|
export async function runUploadPipeline(file, rpc, options = {}) {
|
|
19
|
-
const { signal } = options;
|
|
19
|
+
const { signal, fidelity = DEFAULT_IMAGE_FIDELITY } = options;
|
|
20
20
|
// 1. Compress images. Non-image files return unchanged. Failures here are
|
|
21
21
|
// intentionally swallowed — the user shouldn't see an upload error
|
|
22
22
|
// because canvas threw; we just upload the original bytes.
|
|
23
23
|
let candidate = file;
|
|
24
24
|
try {
|
|
25
|
-
const optimized = await optimizeImageForUpload(file);
|
|
25
|
+
const optimized = await optimizeImageForUpload(file, fidelity);
|
|
26
26
|
candidate = optimized.file;
|
|
27
27
|
}
|
|
28
28
|
catch {
|
package/docs/files.md
CHANGED
|
@@ -14,6 +14,8 @@ discipline in [data fetching](./data_fetching.md).
|
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| Collect bytes from the visitor | `useFileUpload().upload(file)` | `UploadedFile` — a stored, **unattached** file id + presigned serving URLs |
|
|
16
16
|
| Collect several with live previews | `useAttachments()` | `AttachedFile[]` with instant local previews; `fileIds` when done |
|
|
17
|
+
|
|
18
|
+
`useAttachments().add(files, { fidelity })` takes the same `fidelity` as `upload`.
|
|
17
19
|
| Attach to a record | a declared workflow with a `{ type: "file" }` input | the workflow writes the id(s) into a `files` field — the **only** write path |
|
|
18
20
|
| Read back from records | `useQuery` + `readFiles(cell)` | `AppFile[]` — presigned `url`/`thumbnail_url` (24 h) + `size`/`created_at` |
|
|
19
21
|
| Receive a generated document | `useWorkflow` → `WorkflowResult.files` | presigned files auto-extracted from the run |
|
|
@@ -30,17 +32,40 @@ declared workflow (see [mutations](./mutations.md)).
|
|
|
30
32
|
const { upload, uploading, error } = useFileUpload();
|
|
31
33
|
const submit = useWorkflow("submitOrder");
|
|
32
34
|
|
|
33
|
-
const
|
|
34
|
-
await
|
|
35
|
+
const invoiceShot = await upload(file); // high (default)
|
|
36
|
+
const surveyPhoto = await upload(file, { fidelity: "standard" }); // bulk capture
|
|
37
|
+
await submit({ ...fields, invoice_file_id: invoiceShot.id });
|
|
35
38
|
```
|
|
36
39
|
|
|
37
40
|
Signature: `dist/src/hooks.d.ts`. Returns `{ upload, uploading, error }`:
|
|
38
41
|
|
|
39
|
-
- `upload(file: File): Promise<UploadedFile>` — resolves to the stored file; rejects on failure
|
|
42
|
+
- `upload(file: File, options?: { fidelity }): Promise<UploadedFile>` — resolves to the stored file; rejects on failure
|
|
40
43
|
(the file is never partially stored). `UploadedFile` is
|
|
41
44
|
`{ id, filename, mime_type, url?, thumbnail_url? }` — `url`/`thumbnail_url` are presigned
|
|
42
45
|
(24 h) and load directly in the sandboxed iframe, so a just-uploaded image previews without a
|
|
43
46
|
round-trip.
|
|
47
|
+
- `fidelity` — **how faithful the stored image must be.** An ordinal scale, so the value tells
|
|
48
|
+
you what it costs without looking up a mapping. The platform owns the pixels behind each step
|
|
49
|
+
and re-tunes them centrally as model tiers change.
|
|
50
|
+
|
|
51
|
+
Defaults to `"high"` — enough to resolve the text of a photographed document. Drop to
|
|
52
|
+
`"standard"` when collecting in volume.
|
|
53
|
+
|
|
54
|
+
**This only affects photographs.** A PDF, Word, Excel or CSV file is stored untouched at every
|
|
55
|
+
step, so `"original"` is not what keeps a document intact — it already is. Reach for it when
|
|
56
|
+
the PIXELS are the evidence. Note too that HEIC (what an iPhone camera produces) is transcoded
|
|
57
|
+
to JPEG server-side whatever you choose, because no browser can render it.
|
|
58
|
+
|
|
59
|
+
| `fidelity` | Use it when | Stored at |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `"standard"` | The image is only looked AT and archived — site and container surveys, the forty-image drop. Never read by a model, so it is sized purely for upload speed and storage. | 1280 px, q 0.80 |
|
|
62
|
+
| `"high"` *(default)* | The image is READ — invoices, release orders, screenshots of dense tables — by a model or a person. Near-lossless, because a model's token cost depends on dimension alone, so quality is free accuracy. | 1568 px, q 0.95 |
|
|
63
|
+
| `"original"` | The bytes themselves matter: evidence of record, or anything that may be re-read at a higher fidelity later. | unchanged |
|
|
64
|
+
|
|
65
|
+
Getting it wrong is never fatal — too low stores fewer pixels than the reader wanted, it never
|
|
66
|
+
produces a wrong answer. Rule of thumb: **if text on the image has to be legible, or a person
|
|
67
|
+
may zoom, use `"high"`.** Reach for `"original"` only when the file IS the record rather than
|
|
68
|
+
a picture of it — it is the one step whose meaning survives a change to these numbers.
|
|
44
69
|
- `uploading` — true while **any** upload from this hook is in flight.
|
|
45
70
|
- `error` — message of the most recent failed upload; cleared when a new one starts.
|
|
46
71
|
|