@lotics/app-sdk 0.35.0 → 0.36.1
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 +11 -0
- package/dist/src/hooks.d.ts +52 -0
- package/dist/src/hooks.js +58 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -43,6 +43,17 @@ Pick by intent. (→ open the `.d.ts` for the exact signature.)
|
|
|
43
43
|
- **Upload a file** — **`useFileUpload()`** → `{ upload, uploading, error }`. Mints a presigned URL
|
|
44
44
|
and PUTs the bytes straight to storage; the file is inert until a workflow attaches it to a
|
|
45
45
|
`files` field. Emits `app_file_uploaded`.
|
|
46
|
+
- **Composer attachments (with instant preview)** — **`useAttachments()`** → `{ files, add, remove,
|
|
47
|
+
clear, uploading, fileIds }`. The chat's optimistic-preview UX in one hook: `add(File[])` shows a
|
|
48
|
+
local object-URL preview AT ONCE and uploads in the background; each `files[]` entry carries
|
|
49
|
+
`{ id, filename, mime_type, preview_url, status, file_id? }`; previews are revoked on remove/clear.
|
|
50
|
+
Picking is yours (button → `@lotics/ui` `pickFiles`, or paste/drop) — pass the `File[]` to `add`.
|
|
51
|
+
Wire to `@lotics/ui` `Composer`: `actionsButton` triggers the pick, then map each `AttachedFile`
|
|
52
|
+
to a `DisplayFile` (snake → camel — `mime_type`→`mimeType`, `preview_url`→`url` — the app owns this
|
|
53
|
+
data→UI adapter; the SDK never imports `@lotics/ui`) for
|
|
54
|
+
`<FileThumbnail file={{ id, filename, mimeType, url }} uploading={f.status === "uploading"} />`;
|
|
55
|
+
`sendDisabled` gates on `uploading` and the send payload is `fileIds`. Don't hand-roll
|
|
56
|
+
`createObjectURL`/upload/revoke per app.
|
|
46
57
|
- **List members** — **`useMembers(opts?)`** → `{ members: {id,name,email,image}[], loading, error }`
|
|
47
58
|
— the candidate set for an assign/picker. Gated: the app must declare a `member`-typed input
|
|
48
59
|
(`opts.group` restricts to a declared group). Render with `@lotics/ui` `MemberSelect` / `MemberChip`.
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -314,6 +314,58 @@ interface FileUploadState {
|
|
|
314
314
|
* ```
|
|
315
315
|
*/
|
|
316
316
|
export declare function useFileUpload(): FileUploadState;
|
|
317
|
+
/** A file attached to a composer — its local preview is available immediately,
|
|
318
|
+
* its stored `file_id` once the background upload completes. */
|
|
319
|
+
export interface AttachedFile {
|
|
320
|
+
/** Stable local id — the React key and the `remove(id)` handle. */
|
|
321
|
+
id: string;
|
|
322
|
+
filename: string;
|
|
323
|
+
mime_type: string;
|
|
324
|
+
/** Local object-URL preview, available the INSTANT the file is added (before upload). */
|
|
325
|
+
preview_url: string;
|
|
326
|
+
status: "uploading" | "ready" | "error";
|
|
327
|
+
/** The stored file id, set once `status` is `"ready"` — pass to a workflow/agent. */
|
|
328
|
+
file_id?: string;
|
|
329
|
+
}
|
|
330
|
+
interface AttachmentsState {
|
|
331
|
+
/** The current attachments, in the order added. */
|
|
332
|
+
files: AttachedFile[];
|
|
333
|
+
/** Add picked/pasted/dropped files — each shows its local preview at once and
|
|
334
|
+
* uploads in the background. Picking is the app's choice: wire a button to
|
|
335
|
+
* `@lotics/ui` `pickFiles`, or a paste/drop handler, then call this. */
|
|
336
|
+
add: (files: File[]) => void;
|
|
337
|
+
/** Remove one attachment and revoke its preview URL. */
|
|
338
|
+
remove: (id: string) => void;
|
|
339
|
+
/** Remove all attachments and revoke their preview URLs. */
|
|
340
|
+
clear: () => void;
|
|
341
|
+
/** True while any attachment is still uploading — gate Send on it. */
|
|
342
|
+
uploading: boolean;
|
|
343
|
+
/** Stored file ids of the completed uploads — the workflow/agent payload. */
|
|
344
|
+
fileIds: string[];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Composer attachments with the chat's optimistic-preview UX: a local object-URL
|
|
348
|
+
* preview shows the INSTANT a file is added, the upload runs in the background,
|
|
349
|
+
* and the stored `file_id` lands in `files` when it completes. Previews are
|
|
350
|
+
* revoked on remove/clear. Picking is the app's (button / paste / drop) — pass
|
|
351
|
+
* the resulting `File[]` to `add`; the hook owns the preview → upload → id →
|
|
352
|
+
* revoke lifecycle so apps don't re-implement it.
|
|
353
|
+
*
|
|
354
|
+
* ```tsx
|
|
355
|
+
* const { files, add, remove, clear, uploading, fileIds } = useAttachments();
|
|
356
|
+
* const design = useWorkflow("design");
|
|
357
|
+
* // attach: <Button icon="paperclip" onPress={() => pickFiles({ accept: "image/*" }).then(add)} />
|
|
358
|
+
* // preview: map each AttachedFile to a @lotics/ui DisplayFile (snake_case → camelCase) — the
|
|
359
|
+
* // app owns this data→UI adapter; the SDK never imports @lotics/ui:
|
|
360
|
+
* // files.map((f) => (
|
|
361
|
+
* // <FileThumbnail
|
|
362
|
+
* // file={{ id: f.id, filename: f.filename, mimeType: f.mime_type, url: f.preview_url }}
|
|
363
|
+
* // uploading={f.status === "uploading"} onRemove={() => remove(f.id)} />
|
|
364
|
+
* // ))
|
|
365
|
+
* // send: design({ photo: fileIds[0] }); clear();
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
export declare function useAttachments(): AttachmentsState;
|
|
317
369
|
interface MembersState {
|
|
318
370
|
/** Members of the app's organization, for assign / member-picker UIs. */
|
|
319
371
|
members: ResolvedMember[];
|
package/dist/src/hooks.js
CHANGED
|
@@ -246,6 +246,64 @@ export function useFileUpload() {
|
|
|
246
246
|
}, []);
|
|
247
247
|
return { upload, uploading: inFlight > 0, error };
|
|
248
248
|
}
|
|
249
|
+
let attachSeq = 0;
|
|
250
|
+
/**
|
|
251
|
+
* Composer attachments with the chat's optimistic-preview UX: a local object-URL
|
|
252
|
+
* preview shows the INSTANT a file is added, the upload runs in the background,
|
|
253
|
+
* and the stored `file_id` lands in `files` when it completes. Previews are
|
|
254
|
+
* revoked on remove/clear. Picking is the app's (button / paste / drop) — pass
|
|
255
|
+
* the resulting `File[]` to `add`; the hook owns the preview → upload → id →
|
|
256
|
+
* revoke lifecycle so apps don't re-implement it.
|
|
257
|
+
*
|
|
258
|
+
* ```tsx
|
|
259
|
+
* const { files, add, remove, clear, uploading, fileIds } = useAttachments();
|
|
260
|
+
* const design = useWorkflow("design");
|
|
261
|
+
* // attach: <Button icon="paperclip" onPress={() => pickFiles({ accept: "image/*" }).then(add)} />
|
|
262
|
+
* // preview: map each AttachedFile to a @lotics/ui DisplayFile (snake_case → camelCase) — the
|
|
263
|
+
* // app owns this data→UI adapter; the SDK never imports @lotics/ui:
|
|
264
|
+
* // files.map((f) => (
|
|
265
|
+
* // <FileThumbnail
|
|
266
|
+
* // file={{ id: f.id, filename: f.filename, mimeType: f.mime_type, url: f.preview_url }}
|
|
267
|
+
* // uploading={f.status === "uploading"} onRemove={() => remove(f.id)} />
|
|
268
|
+
* // ))
|
|
269
|
+
* // send: design({ photo: fileIds[0] }); clear();
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export function useAttachments() {
|
|
273
|
+
const { upload } = useFileUpload();
|
|
274
|
+
const [files, setFiles] = useState([]);
|
|
275
|
+
const add = useCallback((incoming) => {
|
|
276
|
+
for (const file of incoming) {
|
|
277
|
+
const id = `att_${(attachSeq += 1)}`;
|
|
278
|
+
const previewUrl = URL.createObjectURL(file);
|
|
279
|
+
setFiles((prev) => [
|
|
280
|
+
...prev,
|
|
281
|
+
{ id, filename: file.name, mime_type: file.type, preview_url: previewUrl, status: "uploading" },
|
|
282
|
+
]);
|
|
283
|
+
upload(file)
|
|
284
|
+
.then((uploaded) => setFiles((prev) => prev.map((f) => (f.id === id ? { ...f, status: "ready", file_id: uploaded.id } : f))))
|
|
285
|
+
.catch(() => setFiles((prev) => prev.map((f) => (f.id === id ? { ...f, status: "error" } : f))));
|
|
286
|
+
}
|
|
287
|
+
}, [upload]);
|
|
288
|
+
const remove = useCallback((id) => {
|
|
289
|
+
setFiles((prev) => {
|
|
290
|
+
const target = prev.find((f) => f.id === id);
|
|
291
|
+
if (target)
|
|
292
|
+
URL.revokeObjectURL(target.preview_url);
|
|
293
|
+
return prev.filter((f) => f.id !== id);
|
|
294
|
+
});
|
|
295
|
+
}, []);
|
|
296
|
+
const clear = useCallback(() => {
|
|
297
|
+
setFiles((prev) => {
|
|
298
|
+
for (const f of prev)
|
|
299
|
+
URL.revokeObjectURL(f.preview_url);
|
|
300
|
+
return [];
|
|
301
|
+
});
|
|
302
|
+
}, []);
|
|
303
|
+
const uploading = files.some((f) => f.status === "uploading");
|
|
304
|
+
const fileIds = files.flatMap((f) => (f.status === "ready" && f.file_id ? [f.file_id] : []));
|
|
305
|
+
return { files, add, remove, clear, uploading, fileIds };
|
|
306
|
+
}
|
|
249
307
|
/**
|
|
250
308
|
* List the members of the app's organization — the candidate set for an
|
|
251
309
|
* "assign to a member" picker. Each member is `{ id, name, email, image }`
|
package/dist/src/index.d.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { mount } from "./mount.js";
|
|
18
18
|
export type { MountOptions } from "./mount.js";
|
|
19
|
-
export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useMembers, useAgentRun, useAgentRuns, } from "./hooks.js";
|
|
20
|
-
export type { UploadedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunRecord, AgentRunState, AgentRunStep, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
|
|
19
|
+
export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useAttachments, useMembers, useAgentRun, useAgentRuns, } from "./hooks.js";
|
|
20
|
+
export type { UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, AgentRunOptions, UseAgentRun, AgentRunRecord, AgentRunState, AgentRunStep, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
|
|
21
21
|
export { useComments, useCommentCounts } from "./comments.js";
|
|
22
22
|
export type { AppComment, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
|
|
23
23
|
export { useViewer } from "./viewer.js";
|
package/dist/src/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* not raw HTML/CSS. See `docs/apps.md` → "Styling & components".
|
|
16
16
|
*/
|
|
17
17
|
export { mount } from "./mount.js";
|
|
18
|
-
export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useMembers, useAgentRun, useAgentRuns, } from "./hooks.js";
|
|
18
|
+
export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useAttachments, useMembers, useAgentRun, useAgentRuns, } from "./hooks.js";
|
|
19
19
|
export { useComments, useCommentCounts } from "./comments.js";
|
|
20
20
|
export { useViewer } from "./viewer.js";
|
|
21
21
|
export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
|