@wix/himalaya-cli 0.1381.0 → 0.1383.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/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/camera/src/index.d.ts +18 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/file-picker/src/index.d.ts +14 -4
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/files/src/index.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/photos/src/index.d.ts +9 -4
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/presigned-upload/src/index.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/index.d.ts +12 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/camera.mjs +17 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/file-picker.mjs +17 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/files.mjs +9 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +17 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/presigned-upload.mjs +2 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/share.mjs +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -1
- package/dist/cli.mjs +27 -26
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/sdk-reference.json +58 -10
- package/guides/web-page-sdk-reference.md +3 -3
- package/package.json +2 -2
- package/templates/beauty-salon-owner/config.ts +1 -1
- package/templates/beauty-salon-owner/dev/index.ts +3 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
- package/templates/community-club-owner/config.ts +1 -1
- package/templates/community-club-owner/dev/index.ts +3 -0
- package/templates/community-club-owner/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/community-club-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
- package/templates/events-meetups-owner/config.ts +1 -1
- package/templates/events-meetups-owner/dev/index.ts +3 -0
- package/templates/events-meetups-owner/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/events-meetups-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
- package/templates/gym-fitness-owner/config.ts +1 -1
- package/templates/gym-fitness-owner/dev/index.ts +3 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
- package/templates/nonprofit-cause-owner/config.ts +1 -1
- package/templates/nonprofit-cause-owner/dev/index.ts +3 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
- package/templates/owner-lite/config.ts +1 -1
- package/templates/owner-lite/dev/index.ts +3 -0
- package/templates/owner-lite/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/owner-lite/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +64 -13
- package/templates/podcast-creator-owner/config.ts +1 -1
- package/templates/podcast-creator-owner/dev/index.ts +3 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
- package/templates/portfolio-creative-owner/config.ts +1 -1
- package/templates/portfolio-creative-owner/dev/index.ts +3 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
- package/templates/restaurant-owner/config.ts +1 -1
- package/templates/restaurant-owner/dev/index.ts +3 -0
- package/templates/restaurant-owner/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/restaurant-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
- package/templates/retail-store-owner/config.ts +1 -1
- package/templates/retail-store-owner/dev/index.ts +3 -0
- package/templates/retail-store-owner/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/retail-store-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
- package/templates/yoga-studio-owner/config.ts +1 -1
- package/templates/yoga-studio-owner/dev/index.ts +3 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-attachments.ts +35 -18
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +114 -44
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-upload-queue.ts +47 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +64 -13
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* never silently dropped — the native Aria app drops unsupported files without a
|
|
19
19
|
* word, and an owner who picked something and sees nothing happen assumes the
|
|
20
20
|
* button is broken. */
|
|
21
|
+
import type { FileHandle } from "@himalaya/sdk-types";
|
|
22
|
+
|
|
21
23
|
export const ARIA_IMAGE_TYPES: readonly string[] = [
|
|
22
24
|
"image/png",
|
|
23
25
|
"image/jpeg",
|
|
@@ -25,6 +27,7 @@ export const ARIA_IMAGE_TYPES: readonly string[] = [
|
|
|
25
27
|
"image/webp",
|
|
26
28
|
"image/gif",
|
|
27
29
|
"image/heic",
|
|
30
|
+
"image/heif",
|
|
28
31
|
];
|
|
29
32
|
|
|
30
33
|
/** Document MIME types supported by the native Aria reference. */
|
|
@@ -33,24 +36,22 @@ export const ARIA_DOCUMENT_TYPES: readonly string[] = [
|
|
|
33
36
|
"application/vnd.ms-excel", "application/vnd.ms-powerpoint",
|
|
34
37
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
35
38
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
39
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
36
40
|
];
|
|
37
41
|
|
|
42
|
+
/** Android's actual DocumentsProvider reports CSV using this equivalent MIME.
|
|
43
|
+
* Include it in picker filters, then normalize it back to the wire's text/csv. */
|
|
44
|
+
export const ARIA_DOCUMENT_PICKER_TYPES: readonly string[] = [...ARIA_DOCUMENT_TYPES, "text/comma-separated-values"];
|
|
45
|
+
|
|
38
46
|
/** At most this many attachments ride one message. The native Aria app caps at
|
|
39
47
|
* 10; nothing on the wire says otherwise. */
|
|
40
48
|
export const ARIA_MAX_ATTACHMENTS = 10;
|
|
41
49
|
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* bound of the pre-signed upload capability, which it enforces on every arm).
|
|
48
|
-
* Checking it HERE is what turns that refusal into a sentence the owner can act
|
|
49
|
-
* on, instead of a chip that fails for no stated reason. */
|
|
50
|
-
export const ARIA_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
51
|
-
// Inline documents include base64 and JSON overhead under the 4 MiB frame bridge.
|
|
52
|
-
// Larger documents use the file-backed attachment follow-up.
|
|
53
|
-
export const ARIA_MAX_DOCUMENT_BYTES = 2 * 1024 * 1024;
|
|
50
|
+
/** Prepared/staged limits from first-party Composer policy b2205e5f. Full
|
|
51
|
+
* upload bytes stay in FileHandles; the inline bridge bound is separate. */
|
|
52
|
+
export const ARIA_MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
53
|
+
export const ARIA_MAX_DOCUMENT_BYTES = 25 * 1024 * 1024;
|
|
54
|
+
export const ARIA_MAX_STAGED_BYTES = 100 * 1024 * 1024;
|
|
54
55
|
|
|
55
56
|
/** A chip in the composer. Every field is always present — a `$item.` path that
|
|
56
57
|
* is missing on some rows does not substitute — and each `…Hidden` flag
|
|
@@ -64,7 +65,7 @@ export interface AriaAttachmentChip {
|
|
|
64
65
|
state: "uploading" | "ready" | "failed";
|
|
65
66
|
/** The chip's thumbnail as a `data:` URI, set only when the picked image is
|
|
66
67
|
* small; "" otherwise, which collapses the preview. The full bytes an upload
|
|
67
|
-
* and a retry send
|
|
68
|
+
* and a retry send stay in the host file substrate; only its handle rides the chip, which
|
|
68
69
|
* rides the service state and a large photo there would exceed the web
|
|
69
70
|
* runtime's frame limit. */
|
|
70
71
|
thumbUrl: string;
|
|
@@ -80,6 +81,9 @@ export interface AriaAttachmentChip {
|
|
|
80
81
|
/** `genie-upload://<id>` once the bytes have landed; "" until then. The send
|
|
81
82
|
* carries a chip only when this is set. */
|
|
82
83
|
ref: string;
|
|
84
|
+
/** Persistent substrate handle: retry after worker restart needs no inline bytes. */
|
|
85
|
+
file: FileHandle | null;
|
|
86
|
+
sizeBytes: number;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
/** What an upload reports back, delivered as a fresh action so it lands on the
|
|
@@ -120,6 +124,7 @@ export function normalizeMime(mime: string): string {
|
|
|
120
124
|
case "public.jpeg": return "image/jpeg";
|
|
121
125
|
case "public.png": return "image/png";
|
|
122
126
|
case "public.heic": return "image/heic";
|
|
127
|
+
case "public.heif": return "image/heif";
|
|
123
128
|
case "com.compuserve.gif": return "image/gif";
|
|
124
129
|
case "org.webmproject.webp": return "image/webp";
|
|
125
130
|
case "image/jpg": return "image/jpeg";
|
|
@@ -156,20 +161,29 @@ export function imageFileName(mimeType: string, n: number, given?: string): stri
|
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
/** Why a picked file cannot be attached, or "" when it can. */
|
|
159
|
-
export type AttachRefusal = "" | "type" | "size" | "count";
|
|
164
|
+
export type AttachRefusal = "" | "type" | "size" | "count" | "total";
|
|
160
165
|
|
|
161
|
-
|
|
166
|
+
/** Pinned first-party Composer policy, revision b2205e5f. The file here is
|
|
167
|
+
* already normalized: raster preparation happens in the host before staging. */
|
|
168
|
+
export function refusalForFile(bytes: number, mimeType: string, attached: number, stagedBytes = 0): AttachRefusal {
|
|
162
169
|
if (attached >= ARIA_MAX_ATTACHMENTS) return "count";
|
|
163
|
-
if (!
|
|
164
|
-
|
|
170
|
+
if (![...ARIA_IMAGE_TYPES, ...ARIA_DOCUMENT_TYPES].includes(mimeType) || !Number.isFinite(bytes) || bytes < 0) return "type";
|
|
171
|
+
const limit = mimeType.startsWith("image/") ? ARIA_MAX_IMAGE_BYTES : ARIA_MAX_DOCUMENT_BYTES;
|
|
172
|
+
if (bytes > limit) return "size";
|
|
173
|
+
if (bytes + stagedBytes > ARIA_MAX_STAGED_BYTES) return "total";
|
|
165
174
|
return "";
|
|
166
175
|
}
|
|
167
176
|
|
|
177
|
+
export function refusalFor(parsed: ParsedDataUri | null, mimeType: string, attached: number): AttachRefusal {
|
|
178
|
+
if (!parsed) return attached >= ARIA_MAX_ATTACHMENTS ? "count" : "type";
|
|
179
|
+
return refusalForFile(parsed.bytes, mimeType, attached);
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
/** A chip in a given state. One constructor so the flags can never disagree
|
|
169
183
|
* with the state they describe — a failed chip without its retry control is
|
|
170
184
|
* the shape of bug this rules out. */
|
|
171
185
|
export function chipIn(
|
|
172
|
-
base: Pick<AriaAttachmentChip, "id" | "name" | "mimeType" | "thumbUrl"
|
|
186
|
+
base: Pick<AriaAttachmentChip, "id" | "name" | "mimeType" | "thumbUrl"> & Partial<Pick<AriaAttachmentChip, "file" | "sizeBytes">>,
|
|
173
187
|
state: AriaAttachmentChip["state"],
|
|
174
188
|
labels: { uploading: string; failed: string },
|
|
175
189
|
ref = "",
|
|
@@ -182,9 +196,12 @@ export function chipIn(
|
|
|
182
196
|
"application/vnd.ms-excel": "XLS", "application/vnd.ms-powerpoint": "PPT",
|
|
183
197
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
|
|
184
198
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX",
|
|
199
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "PPTX",
|
|
185
200
|
};
|
|
186
201
|
return {
|
|
187
202
|
...base,
|
|
203
|
+
file: base.file ?? null,
|
|
204
|
+
sizeBytes: base.sizeBytes ?? 0,
|
|
188
205
|
state,
|
|
189
206
|
thumbHidden: !base.thumbUrl,
|
|
190
207
|
documentHidden: base.mimeType.startsWith("image/"),
|
|
@@ -62,16 +62,19 @@ import {
|
|
|
62
62
|
ARIA_MAX_ATTACHMENTS,
|
|
63
63
|
ARIA_MAX_IMAGE_BYTES,
|
|
64
64
|
ARIA_MAX_DOCUMENT_BYTES,
|
|
65
|
+
ARIA_MAX_STAGED_BYTES,
|
|
65
66
|
attachmentLine,
|
|
66
67
|
chipIn,
|
|
67
68
|
imageFileName,
|
|
68
69
|
attachmentMime,
|
|
69
70
|
parseDataUri,
|
|
70
|
-
|
|
71
|
+
refusalForFile,
|
|
71
72
|
sendBlocker,
|
|
72
73
|
type AriaAttachmentChip,
|
|
73
74
|
type AriaAttachmentOutcome,
|
|
74
75
|
} from "./aria-attachments.js";
|
|
76
|
+
import type { FileHandle } from "@himalaya/sdk-types";
|
|
77
|
+
import { createAriaUploadQueue } from "./aria-upload-queue.js";
|
|
75
78
|
import { runClientToolExchange, type AriaClientTools, type ConsentMemory } from "./aria-client-tools.js";
|
|
76
79
|
import { proseOfParts, scanEnvelopes, stripHidden } from "./aria-widgets.js";
|
|
77
80
|
import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
|
|
@@ -480,6 +483,7 @@ export interface AriaChatLabels {
|
|
|
480
483
|
attachTooMany?: (max: number) => string;
|
|
481
484
|
attachUnsupported?: Resolvable<string>;
|
|
482
485
|
attachTooLarge?: (maxMb: number) => string;
|
|
486
|
+
attachTotalTooLarge?: (maxMb: number) => string;
|
|
483
487
|
/** Send refused while a photo is still uploading, or failed to. */
|
|
484
488
|
attachWaitUploading?: Resolvable<string>;
|
|
485
489
|
attachWaitFailed?: Resolvable<string>;
|
|
@@ -538,18 +542,26 @@ export interface AriaDictationOptions {
|
|
|
538
542
|
* Both ports must REJECT (or resolve not-ok) on failure. A port that swallows
|
|
539
543
|
* an error and resolves as if it had worked would put a photo on the message
|
|
540
544
|
* that the service never received. */
|
|
545
|
+
export interface AriaPickedAttachment { dataUri?: string; file?: FileHandle; name?: string; mimeType?: string; previewUri?: string; refusal?: "type" | "size" | "count" | "total" }
|
|
541
546
|
export interface AriaAttachmentPorts {
|
|
542
547
|
/** Let the owner pick (or take) one photo. Resolves the image as a `data:`
|
|
543
548
|
* URI, or null when they cancelled. */
|
|
544
|
-
pick(source: "library" | "camera" | "document"
|
|
549
|
+
pick(source: "library" | "camera" | "document", budget?: { maxItems: number; maxTotalBytes: number }): Promise<AriaPickedAttachment | AriaPickedAttachment[] | null>;
|
|
545
550
|
/** PUT `base64` to a pre-signed url with `contentType`, carrying NO user
|
|
546
551
|
* credentials — the url authorizes itself. */
|
|
547
552
|
put(url: string, base64: string, contentType: string): Promise<{ ok: boolean; status: number }>;
|
|
553
|
+
/** File-backed transfer never copies a document into a worker base64 string. */
|
|
554
|
+
putFile?(url: string, file: FileHandle, contentType: string): Promise<{ ok: boolean; status: number }>;
|
|
555
|
+
/** Release only imported files refused by draft policy, never live/retry handles. */
|
|
556
|
+
discard?(file: FileHandle): Promise<void>;
|
|
548
557
|
/** Hand an upload's outcome back to the service as a NEW action
|
|
549
558
|
* (`attachmentSettled`). A fresh dispatch rather than a return value, because
|
|
550
559
|
* the upload outlives the action that started it — which must return at once
|
|
551
560
|
* so the chip paints while the bytes are still moving. */
|
|
552
561
|
settled(outcome: AriaAttachmentOutcome): void;
|
|
562
|
+
/** Flush this draft at attachment boundaries, before background/pop persistence.
|
|
563
|
+
* The supplied state excludes transient runtime directives. */
|
|
564
|
+
persistDraft?(state: AriaChatState): void;
|
|
553
565
|
}
|
|
554
566
|
|
|
555
567
|
/** Server-provided prompt cards — the chat-service's sibling knowledge service
|
|
@@ -1678,12 +1690,13 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1678
1690
|
const attachUploadingText = () => res(o.labels?.attachUploading ?? "Uploading…");
|
|
1679
1691
|
const attachFailedText = () => res(o.labels?.attachFailed ?? "Couldn't upload");
|
|
1680
1692
|
const chipLabels = () => ({ uploading: attachUploadingText(), failed: attachFailedText() });
|
|
1681
|
-
const refusalText = (why: "type" | "size" | "count",
|
|
1693
|
+
const refusalText = (why: "type" | "size" | "count" | "total", mimeType = "image/jpeg"): string =>
|
|
1682
1694
|
why === "count"
|
|
1683
1695
|
? o.labels?.attachTooMany?.(ARIA_MAX_ATTACHMENTS) ?? `You can attach up to ${ARIA_MAX_ATTACHMENTS} files.`
|
|
1696
|
+
: why === "total" ? o.labels?.attachTotalTooLarge?.(ARIA_MAX_STAGED_BYTES / (1024 * 1024)) ?? `Your attached files exceed ${ARIA_MAX_STAGED_BYTES / (1024 * 1024)} MB in total. Remove a file.`
|
|
1684
1697
|
: why === "size"
|
|
1685
|
-
? o.labels?.attachTooLarge?.(
|
|
1686
|
-
?? `That file is too large to attach (up to ${
|
|
1698
|
+
? o.labels?.attachTooLarge?.((mimeType.startsWith("image/") ? ARIA_MAX_IMAGE_BYTES : ARIA_MAX_DOCUMENT_BYTES) / (1024 * 1024))
|
|
1699
|
+
?? `That file is too large to attach (up to ${(mimeType.startsWith("image/") ? ARIA_MAX_IMAGE_BYTES : ARIA_MAX_DOCUMENT_BYTES) / (1024 * 1024)} MB).`
|
|
1687
1700
|
: res(o.labels?.attachUnsupported ?? "That file type can't be attached.");
|
|
1688
1701
|
const waitUploadingText = () => res(o.labels?.attachWaitUploading ?? "Wait for your file to finish uploading.");
|
|
1689
1702
|
const waitFailedText = () => res(o.labels?.attachWaitFailed ?? "Retry or remove the file that didn't upload.");
|
|
@@ -1699,7 +1712,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
1699
1712
|
* typing…", which a photo edit must never erase. */
|
|
1700
1713
|
const withoutNotice = <T extends { statusText: string }>(s: T): T => {
|
|
1701
1714
|
const notices = [waitUploadingText(), waitFailedText(), waitRemoveText(), needsTextText(),
|
|
1702
|
-
refusalText("type"), refusalText("size"), refusalText("size",
|
|
1715
|
+
refusalText("type"), refusalText("size"), refusalText("size", "application/pdf"), refusalText("count"), refusalText("total")];
|
|
1703
1716
|
return s.statusText && notices.includes(s.statusText) ? { ...s, statusText: "" } : s;
|
|
1704
1717
|
};
|
|
1705
1718
|
const toolFailedText = () => res(o.labels?.toolFailed ?? `${assistant()} couldn't finish that. Try again.`);
|
|
@@ -2035,6 +2048,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2035
2048
|
* hands the first action a PERSISTED state whose chips this worker has never
|
|
2036
2049
|
* seen; the latch starts empty and would otherwise wipe them. */
|
|
2037
2050
|
let chipsSeeded = false;
|
|
2051
|
+
let lastPersistedChips: AriaAttachmentChip[] | undefined;
|
|
2038
2052
|
/** Mints chip ids. Never derived from the file: two photos named
|
|
2039
2053
|
* `IMG_0001.jpg` are two chips. */
|
|
2040
2054
|
let chipSeq = 0;
|
|
@@ -2045,6 +2059,24 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2045
2059
|
* to 8 MB each, the chips alone would exceed that, and the action would fail
|
|
2046
2060
|
* before anything rendered. Pruned to the live chips in `gated`. */
|
|
2047
2061
|
const chipBytes = new Map<string, string>();
|
|
2062
|
+
// Account files belong to this draft until removed/sent. Each queued/active
|
|
2063
|
+
// transport leases its handle until it really exits, including cancelled work.
|
|
2064
|
+
const ownedFiles = new Map<string, FileHandle>();
|
|
2065
|
+
const fileUses = new Map<string, number>();
|
|
2066
|
+
const releasingFiles = new Set<string>();
|
|
2067
|
+
const pruneFiles = (): void => {
|
|
2068
|
+
const discard = o.attachments?.discard;
|
|
2069
|
+
if (!discard) return;
|
|
2070
|
+
const live = new Set(chips.flatMap(chip => chip.file ? [chip.file.uri] : []));
|
|
2071
|
+
for (const [uri, file] of ownedFiles) {
|
|
2072
|
+
if (live.has(uri) || (fileUses.get(uri) ?? 0) > 0 || releasingFiles.has(uri)) continue;
|
|
2073
|
+
releasingFiles.add(uri);
|
|
2074
|
+
void discard(file).then(() => ownedFiles.delete(uri)).catch(() => {
|
|
2075
|
+
// Keep ownership so a later gate can retry a failed cleanup.
|
|
2076
|
+
}).finally(() => releasingFiles.delete(uri));
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
const uploadQueue = createAriaUploadQueue<AriaAttachmentOutcome>();
|
|
2048
2080
|
/** A picked image this small doubles as its own chip thumbnail; a larger one
|
|
2049
2081
|
* shows the chip without a preview. Ten of these stay well under a frame. */
|
|
2050
2082
|
const THUMB_INLINE_MAX = 200_000;
|
|
@@ -2054,22 +2086,24 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2054
2086
|
chipsSeeded = true;
|
|
2055
2087
|
const persisted = Array.isArray(s.attachments) ? s.attachments : [];
|
|
2056
2088
|
// An upload does not survive the process that ran it, so a chip persisted
|
|
2057
|
-
// mid-upload would say "Uploading…" forever.
|
|
2058
|
-
//
|
|
2089
|
+
// mid-upload would say "Uploading…" forever. Its persisted file handle can
|
|
2090
|
+
// be retried in this owner scope. Legacy chips without a retained source
|
|
2091
|
+
// still offer Remove, but cannot offer a working retry.
|
|
2059
2092
|
chips = persisted
|
|
2060
2093
|
.filter((c) => c && typeof c.id === "string")
|
|
2061
2094
|
.map((c) => {
|
|
2062
2095
|
const chip = chipIn(c, c.state === "uploading" ? "failed" : c.state, chipLabels(), c.ref);
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
return chip.state === "failed" && !chip.thumbUrl ? { ...chip, retryHidden: true } : chip;
|
|
2096
|
+
return chip.state === "failed" && !chip.file && !parseDataUri(chip.thumbUrl)
|
|
2097
|
+
? { ...chip, retryHidden: true } : chip;
|
|
2066
2098
|
});
|
|
2067
2099
|
for (const c of chips) {
|
|
2068
2100
|
const n = Number(/^att-(\d+)$/.exec(c.id)?.[1] ?? 0);
|
|
2069
2101
|
if (n > chipSeq) chipSeq = n;
|
|
2070
|
-
//
|
|
2102
|
+
// Legacy inline consumers can retry a stored thumbnail. File-backed
|
|
2103
|
+
// consumers use c.file, including documents above the inline bridge limit.
|
|
2071
2104
|
if (c.thumbUrl && !chipBytes.has(c.id)) chipBytes.set(c.id, c.thumbUrl);
|
|
2072
2105
|
}
|
|
2106
|
+
for (const chip of chips) if (chip.file) ownedFiles.set(chip.file.uri, chip.file);
|
|
2073
2107
|
};
|
|
2074
2108
|
|
|
2075
2109
|
/** Is Send refused while photos are pending, and what does the owner see?
|
|
@@ -2103,20 +2137,32 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
2103
2137
|
const startUpload = (chip: AriaAttachmentChip): void => {
|
|
2104
2138
|
const ports = o.attachments;
|
|
2105
2139
|
if (!ports) return;
|
|
2140
|
+
const site = siteOf();
|
|
2141
|
+
const current = () => siteOf() === site && chips.some(c => c.id === chip.id && c.state === "uploading");
|
|
2106
2142
|
const run = async (): Promise<AriaAttachmentOutcome> => {
|
|
2143
|
+
if (!current()) return { id: chip.id, ok: false };
|
|
2107
2144
|
const parsed = parseDataUri(chipBytes.get(chip.id) ?? chip.thumbUrl);
|
|
2108
|
-
if (!parsed) return { id: chip.id, ok: false };
|
|
2145
|
+
if (!chip.file && !parsed) return { id: chip.id, ok: false };
|
|
2109
2146
|
const minted = await createFileUpload(chip.name, chip.mimeType);
|
|
2147
|
+
if (!current()) return { id: chip.id, ok: false };
|
|
2110
2148
|
const id = (minted?.upload?.id ?? "").trim();
|
|
2111
2149
|
const target = uploadTargetUrl(minted?.upload?.uploadUrl ?? "", chip.name);
|
|
2112
2150
|
if (!id || !target) return { id: chip.id, ok: false };
|
|
2113
|
-
const sent = await ports.
|
|
2151
|
+
const sent = chip.file && ports.putFile ? await ports.putFile(target, chip.file, chip.mimeType)
|
|
2152
|
+
: parsed ? await ports.put(target, parsed.base64, chip.mimeType) : null;
|
|
2114
2153
|
return sent?.ok ? { id: chip.id, ok: true, ref: genieUploadRef(id) } : { id: chip.id, ok: false };
|
|
2115
2154
|
};
|
|
2116
|
-
|
|
2117
|
-
(
|
|
2118
|
-
(
|
|
2119
|
-
|
|
2155
|
+
if (chip.file) {
|
|
2156
|
+
ownedFiles.set(chip.file.uri, chip.file);
|
|
2157
|
+
fileUses.set(chip.file.uri, (fileUses.get(chip.file.uri) ?? 0) + 1);
|
|
2158
|
+
}
|
|
2159
|
+
uploadQueue.enqueue(chip.id, run, outcome => ports.settled(outcome ?? { id: chip.id, ok: false }), () => {
|
|
2160
|
+
if (chip.file) {
|
|
2161
|
+
const uses = (fileUses.get(chip.file.uri) ?? 1) - 1;
|
|
2162
|
+
if (uses > 0) fileUses.set(chip.file.uri, uses); else fileUses.delete(chip.file.uri);
|
|
2163
|
+
}
|
|
2164
|
+
pruneFiles();
|
|
2165
|
+
});
|
|
2120
2166
|
};
|
|
2121
2167
|
|
|
2122
2168
|
/** One create at a time, per site.
|
|
@@ -3295,6 +3341,8 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3295
3341
|
// a cold start stamps the PERSISTED chips rather than an empty latch.
|
|
3296
3342
|
seedChips(actioned);
|
|
3297
3343
|
for (const id of [...chipBytes.keys()]) if (!chips.some((c) => c.id === id)) chipBytes.delete(id);
|
|
3344
|
+
uploadQueue.retain(new Set(chips.filter(c => c.state === "uploading").map(c => c.id)));
|
|
3345
|
+
pruneFiles();
|
|
3298
3346
|
const attachmentsHidden = chips.length === 0;
|
|
3299
3347
|
let out = actioned.attachments === chips && actioned.attachmentsHidden === attachmentsHidden
|
|
3300
3348
|
? actioned
|
|
@@ -3313,6 +3361,12 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
3313
3361
|
// `_poll` from here would navigate, or re-arm the timer, a second time.
|
|
3314
3362
|
committed = settled(out);
|
|
3315
3363
|
commits += 1;
|
|
3364
|
+
if (lastPersistedChips !== chips && o.attachments?.persistDraft) {
|
|
3365
|
+
try {
|
|
3366
|
+
o.attachments.persistDraft(committed);
|
|
3367
|
+
lastPersistedChips = chips;
|
|
3368
|
+
} catch { /* native snapshot writes are best-effort; retry at the next gate */ }
|
|
3369
|
+
}
|
|
3316
3370
|
return out;
|
|
3317
3371
|
};
|
|
3318
3372
|
|
|
@@ -4194,6 +4248,8 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
4194
4248
|
// Refused BEFORE the picker opens when there is no room: presenting a
|
|
4195
4249
|
// picker whose result will be thrown away wastes the owner's time.
|
|
4196
4250
|
if (chips.length >= ARIA_MAX_ATTACHMENTS) return { ...s, statusText: refusalText("count"), error: "" };
|
|
4251
|
+
const remainingBytes = ARIA_MAX_STAGED_BYTES - chips.reduce((sum, chip) => sum + chip.sizeBytes, 0);
|
|
4252
|
+
if (remainingBytes <= 0) return { ...s, statusText: refusalText("total"), error: "" };
|
|
4197
4253
|
const raw = typeof arg === "string" ? arg : firstString(arg?.source, arg?.value);
|
|
4198
4254
|
const source = raw.trim().toLowerCase() === "document" ? "document"
|
|
4199
4255
|
: raw.trim().toLowerCase() === "camera" ? "camera" : "library";
|
|
@@ -4207,34 +4263,48 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
4207
4263
|
const latest = (): AriaChatState => (commits !== at && committed ? committed : s);
|
|
4208
4264
|
let picked: Awaited<ReturnType<AriaAttachmentPorts["pick"]>>;
|
|
4209
4265
|
try {
|
|
4210
|
-
picked = await ports.pick(source);
|
|
4211
|
-
} catch
|
|
4212
|
-
if (/too large|size limit/i.test(String(error))) return { ...latest(), statusText: refusalText("size", source === "document" ? ARIA_MAX_DOCUMENT_BYTES : ARIA_MAX_IMAGE_BYTES), error: "" };
|
|
4266
|
+
picked = await ports.pick(source, { maxItems: ARIA_MAX_ATTACHMENTS - chips.length, maxTotalBytes: remainingBytes });
|
|
4267
|
+
} catch {
|
|
4213
4268
|
// A picker that could not open is not the owner's fault and there is no
|
|
4214
|
-
// file to talk about;
|
|
4269
|
+
// file to talk about; clear stale attachment advice without erasing
|
|
4270
|
+
// a newer live status, draft or transcript.
|
|
4215
4271
|
return withoutNotice(latest());
|
|
4216
4272
|
}
|
|
4217
|
-
if (!picked
|
|
4218
|
-
const
|
|
4219
|
-
|
|
4220
|
-
//
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4273
|
+
if (!picked) return latest();
|
|
4274
|
+
const batch = Array.isArray(picked) ? picked : [picked];
|
|
4275
|
+
// A cancelled picker returns an empty batch: nothing was attached or refused, so the
|
|
4276
|
+
// advice already on screen (a size refusal, a failed upload) stays where it is.
|
|
4277
|
+
if (!batch.length) return latest();
|
|
4278
|
+
let notice = "";
|
|
4279
|
+
for (const file of batch) {
|
|
4280
|
+
const parsed = file.dataUri ? parseDataUri(file.dataUri) : null;
|
|
4281
|
+
const name = file.name ?? file.file?.name;
|
|
4282
|
+
const mimeType = attachmentMime(file.mimeType || file.file?.mimeType || parsed?.mimeType || "", name);
|
|
4283
|
+
const sizeBytes = file.file?.sizeBytes ?? parsed?.bytes;
|
|
4284
|
+
const refused = file.refusal || (sizeBytes === undefined ? "type"
|
|
4285
|
+
: refusalForFile(sizeBytes, mimeType, chips.length, chips.reduce((sum, c) => sum + c.sizeBytes, 0)));
|
|
4286
|
+
if (refused) {
|
|
4287
|
+
notice = refusalText(refused, mimeType);
|
|
4288
|
+
if (file.file && ports.discard) {
|
|
4289
|
+
try { await ports.discard(file.file); } catch { /* preserve the refusal if cleanup is unavailable */ }
|
|
4290
|
+
}
|
|
4291
|
+
continue;
|
|
4292
|
+
}
|
|
4293
|
+
chipSeq += 1;
|
|
4294
|
+
const id = `att-${chipSeq}`;
|
|
4295
|
+
if (file.dataUri) chipBytes.set(id, file.dataUri);
|
|
4296
|
+
const preview = file.previewUri || (mimeType.startsWith("image/") && file.dataUri && file.dataUri.length <= THUMB_INLINE_MAX ? file.dataUri : "");
|
|
4297
|
+
const chip = chipIn({
|
|
4298
|
+
id, name: imageFileName(mimeType, chipSeq, name), mimeType,
|
|
4299
|
+
thumbUrl: mimeType.startsWith("image/") && preview.length <= THUMB_INLINE_MAX ? preview : "",
|
|
4300
|
+
file: file.file ?? null, sizeBytes,
|
|
4301
|
+
}, "uploading", chipLabels());
|
|
4302
|
+
chips = [...chips, chip];
|
|
4303
|
+
startUpload(chip);
|
|
4304
|
+
}
|
|
4305
|
+
// Stage valid files in a partially accepted batch and explain every
|
|
4306
|
+
// refusal; never silently discard an over-limit selection.
|
|
4307
|
+
return { ...withoutNotice(latest()), statusText: notice || withoutNotice(latest()).statusText, error: "" };
|
|
4238
4308
|
},
|
|
4239
4309
|
|
|
4240
4310
|
suggestionsSettled: (s, arg) => {
|
|
@@ -4287,7 +4357,7 @@ export function createAriaChat(o: AriaChatOptions): AriaChatCore {
|
|
|
4287
4357
|
retryAttachment: (s, arg) => {
|
|
4288
4358
|
const id = chipIdOf(arg);
|
|
4289
4359
|
const current = chips.find((c) => c.id === id);
|
|
4290
|
-
if (!current || current.state !== "failed" || (!chipBytes.get(id)
|
|
4360
|
+
if (!current || current.state !== "failed" || (!current.file && !parseDataUri(chipBytes.get(id) ?? current.thumbUrl))) return s;
|
|
4291
4361
|
// A NEW id for the new attempt, in the same place in the strip. Keeping the
|
|
4292
4362
|
// old one would let the first attempt's outcome, if it is still in the air,
|
|
4293
4363
|
// settle this one.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Three upload workers, matching first-party Aria b2205e5f. A removed
|
|
2
|
+
* in-flight task retains its slot until its transport actually exits: ignoring
|
|
3
|
+
* an outcome is not cancellation of native buffers/network activity. */
|
|
4
|
+
export function createAriaUploadQueue<T>(concurrency = 3) {
|
|
5
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error("invalid upload concurrency");
|
|
6
|
+
interface Task { id: string; cancelled: boolean; started: boolean; released: boolean; release?: () => void; run: () => Promise<T>; settle: (result: T | undefined, error?: unknown) => void }
|
|
7
|
+
const pending: Task[] = [];
|
|
8
|
+
const tasks = new Map<string, Task>();
|
|
9
|
+
let active = 0;
|
|
10
|
+
const release = (task: Task): void => {
|
|
11
|
+
if (task.released) return;
|
|
12
|
+
task.released = true;
|
|
13
|
+
try { task.release?.(); } catch { /* cleanup must not strand the queue */ }
|
|
14
|
+
};
|
|
15
|
+
const pump = (): void => {
|
|
16
|
+
while (active < concurrency && pending.length) {
|
|
17
|
+
const task = pending.shift()!;
|
|
18
|
+
if (task.cancelled) { release(task); continue; }
|
|
19
|
+
task.started = true;
|
|
20
|
+
active += 1;
|
|
21
|
+
void Promise.resolve().then(task.run).then(
|
|
22
|
+
result => { if (!task.cancelled) task.settle(result); },
|
|
23
|
+
error => { if (!task.cancelled) task.settle(undefined, error); },
|
|
24
|
+
).catch(() => { /* delivery must not strand a transport slot */ }).finally(() => {
|
|
25
|
+
active -= 1;
|
|
26
|
+
release(task);
|
|
27
|
+
if (tasks.get(task.id) === task) tasks.delete(task.id);
|
|
28
|
+
pump();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
enqueue(id: string, run: () => Promise<T>, settle: Task["settle"], release?: () => void): void {
|
|
34
|
+
if (tasks.has(id)) return;
|
|
35
|
+
const task = { id, cancelled: false, started: false, released: false, release, run, settle };
|
|
36
|
+
tasks.set(id, task); pending.push(task); pump();
|
|
37
|
+
},
|
|
38
|
+
retain(ids: ReadonlySet<string>): void {
|
|
39
|
+
for (const [id, task] of tasks) {
|
|
40
|
+
if (ids.has(id)) continue;
|
|
41
|
+
task.cancelled = true;
|
|
42
|
+
if (!task.started) release(task);
|
|
43
|
+
tasks.delete(id);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -26,11 +26,17 @@
|
|
|
26
26
|
|
|
27
27
|
import { readAsyncJobState } from "../_shared/aria-async-jobs.js";
|
|
28
28
|
import { defineService } from "@himalaya/state";
|
|
29
|
+
import { serializeForAgent } from "@himalaya/agent";
|
|
29
30
|
import { open } from "@himalaya/open-url";
|
|
30
31
|
import { writeText } from "@himalaya/clipboard";
|
|
31
32
|
import { availability, recognizeOnce, stop as stopSpeech } from "@himalaya/speech";
|
|
32
|
-
import { capturePhoto,
|
|
33
|
-
import {
|
|
33
|
+
import { capturePhoto, prepareImage } from "@himalaya/camera";
|
|
34
|
+
import { pickDetailed as pickPhotos } from "@himalaya/photos";
|
|
35
|
+
import { pickDetailed as pickFiles } from "@himalaya/file-picker";
|
|
36
|
+
import { write as writeFile, remove as removeFile } from "@himalaya/files";
|
|
37
|
+
import { put, putFile } from "@himalaya/presigned-upload";
|
|
38
|
+
import { ARIA_MAX_ATTACHMENTS, ARIA_MAX_DOCUMENT_BYTES, ARIA_MAX_STAGED_BYTES, ARIA_IMAGE_TYPES, ARIA_DOCUMENT_PICKER_TYPES, attachmentMime, parseDataUri } from "../_shared/aria-attachments.js";
|
|
39
|
+
import type { FileHandle } from "@himalaya/sdk-types";
|
|
34
40
|
import { alert } from "@himalaya/ui";
|
|
35
41
|
import { auth } from "@himalaya/wix-platform";
|
|
36
42
|
import {
|
|
@@ -42,6 +48,7 @@ import {
|
|
|
42
48
|
type AriaChatState,
|
|
43
49
|
type AriaDictationResult,
|
|
44
50
|
type AriaPromptChip,
|
|
51
|
+
type AriaPickedAttachment,
|
|
45
52
|
} from "../_shared/aria-chat.js";
|
|
46
53
|
import { WIDGET_SUBTREE_SLOT } from "../_shared/aria-generic-widget.js";
|
|
47
54
|
import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
|
|
@@ -265,6 +272,7 @@ const core = createAriaChat({
|
|
|
265
272
|
attachFailed: () => copyParam("ownerAria.attach.failed", "Couldn't upload"),
|
|
266
273
|
attachTooMany: (max) => copyTemplate("ownerAria.attach.tooMany", "You can attach up to {max} files.", { max }),
|
|
267
274
|
attachUnsupported: () => copyParam("ownerAria.attach.unsupported", "That file type can't be attached."),
|
|
275
|
+
attachTotalTooLarge: (mb) => copyTemplate("ownerAria.attach.totalTooLarge", "Your attached files exceed {mb} MB in total. Remove a file.", { mb }),
|
|
268
276
|
attachTooLarge: (mb) => copyTemplate("ownerAria.attach.tooLarge", "That file is too large to attach (up to {mb} MB).", { mb }),
|
|
269
277
|
attachWaitUploading: () => copyParam("ownerAria.attach.waitUploading", "Wait for your file to finish uploading."),
|
|
270
278
|
attachWaitFailed: () => copyParam("ownerAria.attach.waitFailed", "Retry or remove the file that didn't upload."),
|
|
@@ -311,25 +319,68 @@ const core = createAriaChat({
|
|
|
311
319
|
deliver: (r) => { void svc.dictated(r); },
|
|
312
320
|
available: async () => (await availability()).recognition !== "unavailable",
|
|
313
321
|
},
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
// `camera` for the picker (it returns the image already re-encoded as JPEG and
|
|
317
|
-
// scaled to 2048 px on its longest side, which is also what keeps a phone
|
|
318
|
-
// photo inside the inline upload bound) and `presigned_upload` for the bytes,
|
|
319
|
-
// which go to a self-authorizing url with no Wix credential attached.
|
|
322
|
+
// Pick into the account file substrate, normalize raster images on the host,
|
|
323
|
+
// and upload handles without copying full file bytes into worker state.
|
|
320
324
|
attachments: {
|
|
321
|
-
|
|
325
|
+
persistDraft: state => serializeForAgent({ [SERVICE]: state }),
|
|
326
|
+
discard: file => removeFile(file),
|
|
327
|
+
pick: async (source, budget) => {
|
|
328
|
+
let files: FileHandle[];
|
|
329
|
+
const picked: AriaPickedAttachment[] = [];
|
|
322
330
|
if (source === "document") {
|
|
323
|
-
const
|
|
324
|
-
|
|
331
|
+
const result = await pickFiles({ types: [...ARIA_IMAGE_TYPES, ...ARIA_DOCUMENT_PICKER_TYPES], multiple: true,
|
|
332
|
+
maxBytes: ARIA_MAX_DOCUMENT_BYTES, maxImageBytes: 32 * 1024 * 1024,
|
|
333
|
+
maxItems: budget?.maxItems ?? ARIA_MAX_ATTACHMENTS, maxTotalBytes: budget?.maxTotalBytes ?? ARIA_MAX_STAGED_BYTES });
|
|
334
|
+
files = result.items;
|
|
335
|
+
picked.push(...result.rejected.map(file => ({ name: file.name, mimeType: file.mimeType, refusal: file.reason })));
|
|
325
336
|
}
|
|
326
|
-
|
|
327
|
-
|
|
337
|
+
else if (source === "library") {
|
|
338
|
+
const result = await pickPhotos({ limit: budget?.maxItems ?? ARIA_MAX_ATTACHMENTS, type: "images",
|
|
339
|
+
maxBytes: 32 * 1024 * 1024, maxTotalBytes: budget?.maxTotalBytes ?? ARIA_MAX_STAGED_BYTES });
|
|
340
|
+
files = result.items;
|
|
341
|
+
picked.push(...result.rejected.map(file => ({ name: file.name, mimeType: file.mimeType, refusal: file.reason })));
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
const asset = await capturePhoto({ source: "camera", maxEdgePixels: 2048 });
|
|
345
|
+
if (!asset) return [];
|
|
346
|
+
const parsed = parseDataUri(asset.uri);
|
|
347
|
+
if (!parsed) throw new Error("Camera returned an unsupported image");
|
|
348
|
+
const path = `captures/${Date.now()}-${Math.random().toString(36).slice(2)}-photo.jpg`;
|
|
349
|
+
files = [await writeFile(path, { base64: parsed.base64 }, { mimeType: asset.mime })];
|
|
350
|
+
}
|
|
351
|
+
// A camera capture is stored under a unique internal path; it has no name of its own to
|
|
352
|
+
// show or send, so `name: ""` asks the chat core for its `photo-N.jpg` fallback instead.
|
|
353
|
+
const shownName = source === "camera" ? { name: "" } : {};
|
|
354
|
+
for (const original of files) {
|
|
355
|
+
const mimeType = attachmentMime(original.mimeType, original.name);
|
|
356
|
+
const file = { ...original, mimeType };
|
|
357
|
+
if (ARIA_IMAGE_TYPES.includes(mimeType)) {
|
|
358
|
+
try {
|
|
359
|
+
const prepared = await prepareImage(file);
|
|
360
|
+
picked.push({ file: prepared.file, previewUri: prepared.previewUri, ...shownName });
|
|
361
|
+
// The codec may write a replacement. Keep the prepared upload/retry
|
|
362
|
+
// handle, and release only the now-unused original import.
|
|
363
|
+
if (prepared.file.uri !== file.uri) {
|
|
364
|
+
try { await removeFile(file); } catch { /* best-effort import cleanup */ }
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
// A rejected codec/oversize image still reaches policy as an
|
|
368
|
+
// unsupported entry, so the owner gets an actionable refusal.
|
|
369
|
+
try { await removeFile(file); } catch { /* keep the policy refusal on cleanup failure */ }
|
|
370
|
+
picked.push({ name: file.name, mimeType, refusal: /size limit|oversized/i.test(String(error)) ? "size" : "type" });
|
|
371
|
+
}
|
|
372
|
+
} else picked.push({ file, ...shownName });
|
|
373
|
+
}
|
|
374
|
+
return picked;
|
|
328
375
|
},
|
|
329
376
|
put: async (url, base64, contentType) => {
|
|
330
377
|
const r = await put(url, { base64 }, { contentType, method: "PUT" });
|
|
331
378
|
return { ok: r.ok, status: r.status };
|
|
332
379
|
},
|
|
380
|
+
putFile: async (url, file, contentType) => {
|
|
381
|
+
const r = await putFile(url, file, { contentType, method: "PUT" });
|
|
382
|
+
return { ok: r.ok, status: r.status };
|
|
383
|
+
},
|
|
333
384
|
// A NEW action on the service, not a return value: the upload outlives the
|
|
334
385
|
// `attach` that started it. `svc` is read at call time, long after it exists.
|
|
335
386
|
settled: (outcome) => { void svc.attachmentSettled(outcome); },
|
|
@@ -37,7 +37,7 @@ export default defineApp({
|
|
|
37
37
|
actions: "http://localhost:8190/v1/actions",
|
|
38
38
|
sync: "http://localhost:8190/v1/sync",
|
|
39
39
|
},
|
|
40
|
-
capabilities: ["live_activity","push","share","secure_storage","open_url","local_database","clipboard","ui","speech","camera","presigned_upload","app_info","biometrics","wix_platform"],
|
|
40
|
+
capabilities: ["live_activity","push","share","secure_storage","open_url","local_database","clipboard","ui","speech","camera","photos","file_picker","files","presigned_upload","app_info","biometrics","wix_platform"],
|
|
41
41
|
designTokensPath: "./tokens.json",
|
|
42
42
|
navigation: {
|
|
43
43
|
"navKind": "responsive",
|
|
@@ -60987,6 +60987,9 @@ const SCREENS = runtimeSetting({
|
|
|
60987
60987
|
"ui",
|
|
60988
60988
|
"speech",
|
|
60989
60989
|
"camera",
|
|
60990
|
+
"photos",
|
|
60991
|
+
"file_picker",
|
|
60992
|
+
"files",
|
|
60990
60993
|
"presigned_upload",
|
|
60991
60994
|
"stream",
|
|
60992
60995
|
"wix_platform"
|