@burdenoff/fe-libs 2026.904.20 → 2026.904.22

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.
Files changed (37) hide show
  1. package/dist/node_modules/fflate/esm/browser.js +324 -0
  2. package/dist/node_modules/pdfjs-dist/build/pdf.js +15775 -0
  3. package/dist/node_modules/pdfjs-dist/build/pdf.worker.min.js +50952 -0
  4. package/dist/shared/assistant/aiCredits.d.ts +21 -0
  5. package/dist/shared/assistant/aiCredits.d.ts.map +1 -0
  6. package/dist/shared/assistant/aiCredits.js +15 -0
  7. package/dist/shared/assistant/createSandboxAssistantTransport.js +2 -1
  8. package/dist/shared/assistant/documentExtract.d.ts +22 -0
  9. package/dist/shared/assistant/documentExtract.d.ts.map +1 -0
  10. package/dist/shared/assistant/documentExtract.js +133 -0
  11. package/dist/shared/assistant/gatewayClient.d.ts +15 -1
  12. package/dist/shared/assistant/gatewayClient.d.ts.map +1 -1
  13. package/dist/shared/assistant/gatewayClient.js +6 -11
  14. package/dist/shared/assistant/index.d.ts +6 -0
  15. package/dist/shared/assistant/index.d.ts.map +1 -1
  16. package/dist/shared/assistant/quota.d.ts +65 -0
  17. package/dist/shared/assistant/quota.d.ts.map +1 -0
  18. package/dist/shared/assistant/quota.js +45 -0
  19. package/dist/shared/assistant/ui/composer/AssistantComposer.d.ts +50 -0
  20. package/dist/shared/assistant/ui/composer/AssistantComposer.d.ts.map +1 -0
  21. package/dist/shared/assistant/ui/composer/AssistantComposer.js +185 -0
  22. package/dist/shared/assistant/useAssistantAttachments.d.ts +26 -0
  23. package/dist/shared/assistant/useAssistantAttachments.d.ts.map +1 -0
  24. package/dist/shared/assistant/useAssistantAttachments.js +244 -0
  25. package/dist/shared/assistant/useWorkspaceWriteAccess.d.ts +29 -0
  26. package/dist/shared/assistant/useWorkspaceWriteAccess.d.ts.map +1 -0
  27. package/dist/shared/assistant/useWorkspaceWriteAccess.js +41 -0
  28. package/dist/shared/utils/imageDownscale.d.ts +91 -0
  29. package/dist/shared/utils/imageDownscale.d.ts.map +1 -0
  30. package/dist/shared/utils/imageDownscale.js +102 -0
  31. package/dist/shared/utils/index.d.ts +4 -3
  32. package/dist/shared/utils/index.d.ts.map +1 -1
  33. package/dist/shared-assistant.js +84 -78
  34. package/dist/shared-utils.js +6 -5
  35. package/dist/shared.js +94 -93
  36. package/dist/utils/index.js +1 -0
  37. package/package.json +13 -3
@@ -0,0 +1,185 @@
1
+ import { useI18n as e } from "../../../providers/shell/I18nProvider.js";
2
+ import { useVoiceInput as t } from "../../voice/useVoiceInput.js";
3
+ import { languageDisplayName as n } from "../../voice/speechVoices.js";
4
+ import { resolveAssistantLanguage as ee } from "../../voice/languages.js";
5
+ import { useAssistantTr as r } from "../../i18n.js";
6
+ import { getSendableAttachments as i } from "../../attachments.js";
7
+ import { useAssistantStore as a } from "../../store.js";
8
+ import { AssistantAttachmentButton as o } from "./AssistantAttachmentButton.js";
9
+ import { AssistantAttachmentChips as s } from "./AssistantAttachmentChips.js";
10
+ import { AssistantLanguageSelector as c } from "./AssistantLanguageSelector.js";
11
+ import { AssistantModeSelector as te } from "./AssistantModeSelector.js";
12
+ import { AssistantVoiceButton as ne } from "./AssistantVoiceButton.js";
13
+ import { AssistantVoiceSessionButton as re } from "../voice/AssistantVoiceSessionButton.js";
14
+ import { useAssistantAttachments as ie } from "../../useAssistantAttachments.js";
15
+ import { useCallback as l, useLayoutEffect as ae, useMemo as oe, useRef as u, useState as d } from "react";
16
+ import { ArrowUp as f, Loader2 as p } from "lucide-react";
17
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
18
+ //#region src/shared/assistant/ui/composer/AssistantComposer.tsx
19
+ var g = 200, _ = ({ writeAccessDenied: _ = !1, onSend: v, disabled: y = !1, placeholder: b, mode: se, availableModes: x, onModeChange: ce, onStartVoiceSession: S }) => {
20
+ let C = r(), { locale: w } = e(), T = u(null), [E, D] = d(""), [O, k] = d(!1), A = u(0), { attachments: j, isBusy: M, preparingCount: N, sendBlocker: P, rejection: F, addFiles: I, remove: L, retry: R, clear: z, clearRejection: B, canAddMore: V } = ie(), H = l((e) => {
21
+ D((t) => t ? `${t.replace(/\s+$/, "")} ${e}` : e);
22
+ }, []), U = a((e) => e.assistantLanguage), W = a((e) => e.setAssistantLanguage), G = oe(() => ee(U, w), [U, w]), K = t({
23
+ onFinalize: H,
24
+ language: G.tag
25
+ }), le = b ?? C("composer.placeholder", "Ask HealthyBowl anything…"), q = E.trim(), J = i(j), Y = j.filter((e) => e.status === "error"), X = (q.length > 0 || J.length > 0) && !y && P === null;
26
+ ae(() => {
27
+ let e = T.current;
28
+ e && (e.style.height = "auto", e.style.height = `${Math.min(e.scrollHeight, g)}px`);
29
+ }, [E]);
30
+ let Z = l(() => {
31
+ X && (K.isActive && K.stop(), v(E.trim(), J), D(""), z());
32
+ }, [
33
+ X,
34
+ v,
35
+ E,
36
+ J,
37
+ z,
38
+ K
39
+ ]), ue = l((e) => {
40
+ e.key === "Enter" && !e.shiftKey && (e.preventDefault(), Z());
41
+ }, [Z]), Q = l((e) => {
42
+ let t = Array.from(e.clipboardData?.files ?? []);
43
+ t.length !== 0 && (e.preventDefault(), I(t));
44
+ }, [I]), de = l((e) => {
45
+ Array.from(e.dataTransfer?.types ?? []).includes("Files") && (A.current += 1, k(!0));
46
+ }, []), fe = l(() => {
47
+ A.current = Math.max(0, A.current - 1), A.current === 0 && k(!1);
48
+ }, []), pe = l((e) => {
49
+ Array.from(e.dataTransfer?.types ?? []).includes("Files") && e.preventDefault();
50
+ }, []), me = l((e) => {
51
+ e.preventDefault(), A.current = 0, k(!1), I(e.dataTransfer?.files ?? null);
52
+ }, [I]), he = (e) => {
53
+ switch (e) {
54
+ case "too-many": return C("attachments.tooMany", "You can attach up to {{max}} files per message.", { max: 5 });
55
+ case "too-large": return C("attachments.tooLarge", "That file is larger than 10 MB.");
56
+ case "empty": return C("attachments.empty", "That file is empty.");
57
+ case "unreadable": return C("attachments.unreadable", "That file could not be read.");
58
+ case "unsupported-image": return C("attachments.unsupportedImage", "This device can't read that image format, so it can't be resized to send. Save it as JPEG and try again.");
59
+ }
60
+ }, $ = (() => {
61
+ switch (K.error) {
62
+ case "not-allowed": return C("voice.denied", "Microphone access is blocked. Allow it in your browser settings to dictate.");
63
+ case "audio-capture": return C("voice.noMic", "No microphone was found.");
64
+ case "network": return C("voice.network", "Speech recognition is offline right now.");
65
+ case "language-not-supported": return C("voice.languageNotSupported", "This browser cannot recognise spoken {{language}}. Try a different browser, or type instead.", { language: n(K.language, w) });
66
+ case "unknown": return C("voice.failed", "Dictation stopped unexpectedly.");
67
+ default: return null;
68
+ }
69
+ })();
70
+ return /* @__PURE__ */ h("div", {
71
+ className: "flex-shrink-0 px-3 pb-3 pt-1.5",
72
+ children: [
73
+ /* @__PURE__ */ h("div", {
74
+ onDragEnter: de,
75
+ onDragLeave: fe,
76
+ onDragOver: pe,
77
+ onDrop: me,
78
+ className: `rounded-2xl border p-2 shadow-sm transition-colors focus-within:border-action-primary-bg focus-within:ring-2 focus-within:ring-action-primary-bg/15 ${O ? "border-action-primary-bg border-dashed bg-action-primary-bg/5" : "border-border-default bg-bg-sunken"}`,
79
+ "data-testid": "assistant-composer",
80
+ children: [
81
+ /* @__PURE__ */ m(s, {
82
+ attachments: j,
83
+ onRemove: L,
84
+ onRetry: R,
85
+ disabled: y,
86
+ preparingCount: N
87
+ }),
88
+ /* @__PURE__ */ h("div", {
89
+ className: "flex items-end gap-2",
90
+ children: [
91
+ /* @__PURE__ */ m(o, {
92
+ onFiles: I,
93
+ canAddMore: V,
94
+ disabled: y
95
+ }),
96
+ /* @__PURE__ */ m("textarea", {
97
+ ref: T,
98
+ value: E,
99
+ onChange: (e) => D(e.target.value),
100
+ onKeyDown: ue,
101
+ onPaste: Q,
102
+ disabled: y,
103
+ placeholder: O ? C("composer.dropHere", "Drop files to attach") : le,
104
+ rows: 1,
105
+ className: "min-h-[36px] flex-1 resize-none self-center bg-transparent px-1 py-2 text-[15px] leading-relaxed text-text-primary placeholder:text-text-muted focus:outline-none disabled:opacity-50",
106
+ "aria-label": C("chatInput.ariaLabel", "Message input"),
107
+ "data-testid": "assistant-input"
108
+ }),
109
+ /* @__PURE__ */ m(c, {
110
+ current: G,
111
+ selected: U,
112
+ onChange: W,
113
+ disabled: y || K.isActive
114
+ }),
115
+ K.isSupported && /* @__PURE__ */ m(ne, {
116
+ isListening: K.isListening,
117
+ isStarting: K.isStarting,
118
+ onToggle: K.toggle,
119
+ disabled: y
120
+ }),
121
+ S && q.length === 0 && j.length === 0 && !K.isActive ? /* @__PURE__ */ m(re, {
122
+ onStart: S,
123
+ disabled: y
124
+ }) : /* @__PURE__ */ m("button", {
125
+ type: "button",
126
+ onClick: Z,
127
+ disabled: !X,
128
+ className: `flex size-9 flex-shrink-0 items-center justify-center rounded-full shadow-sm transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong ${X ? "bg-action-primary-bg text-action-primary-text hover:scale-105 hover:bg-action-primary-bg-hover" : "bg-bg-elevated text-text-muted"}`,
129
+ "aria-label": C("chatInput.send", "Send message"),
130
+ "data-testid": "assistant-send",
131
+ children: y || M ? /* @__PURE__ */ m(p, { className: "size-4 animate-spin" }) : /* @__PURE__ */ m(f, { className: "size-4" })
132
+ })
133
+ ]
134
+ }),
135
+ x.length > 1 && /* @__PURE__ */ m("div", {
136
+ className: "mt-2 px-0.5",
137
+ children: /* @__PURE__ */ m(te, {
138
+ mode: se,
139
+ availableModes: x,
140
+ onModeChange: ce,
141
+ disabled: y
142
+ })
143
+ })
144
+ ]
145
+ }),
146
+ _ ? /* @__PURE__ */ m("p", {
147
+ className: "mt-1.5 px-1 text-center text-[11px] text-status-warning",
148
+ "data-testid": "assistant-write-access-warning",
149
+ children: C("composer.writeAccessDenied", "Changes may not save — your role in this workspace does not currently allow edits. You can still plan and review an action.")
150
+ }) : null,
151
+ /* @__PURE__ */ m("div", {
152
+ className: "mt-1.5 px-1 text-center text-[11px]",
153
+ "aria-live": "polite",
154
+ children: K.isStarting && !K.isListening ? /* @__PURE__ */ m("p", {
155
+ className: "text-text-muted",
156
+ "data-testid": "assistant-voice-starting",
157
+ children: C("voice.starting", "Starting the microphone…")
158
+ }) : K.isListening ? /* @__PURE__ */ h("p", {
159
+ className: "text-status-error-text",
160
+ "data-testid": "assistant-voice-status",
161
+ children: [/* @__PURE__ */ m("span", { className: "mr-1 inline-block size-1.5 rounded-full bg-status-error-text align-middle motion-safe:animate-pulse" }), K.interim || C("voice.listening", "Listening… speak now, tap the square to stop")]
162
+ }) : $ ? /* @__PURE__ */ m("p", {
163
+ className: "text-status-error-text",
164
+ "data-testid": "assistant-voice-error",
165
+ children: $
166
+ }) : F ? /* @__PURE__ */ m("button", {
167
+ type: "button",
168
+ onClick: B,
169
+ className: "text-status-error-text underline-offset-2 hover:underline",
170
+ "data-testid": "assistant-attachment-rejection",
171
+ children: he(F)
172
+ }) : P === "failed" ? /* @__PURE__ */ m("p", {
173
+ className: "text-status-error-text",
174
+ "data-testid": "assistant-attachment-blocked",
175
+ children: Y.length === 1 ? C("attachments.blockedOne", "{{name}} didn't upload. Retry or remove it before sending.", { name: Y[0].filename }) : C("attachments.blockedMany", "{{count}} attachments didn't upload. Retry or remove them before sending.", { count: Y.length })
176
+ }) : /* @__PURE__ */ m("p", {
177
+ className: "text-text-muted",
178
+ children: C("composer.hint", "Enter to send · Shift+Enter for newline · drag or paste files to attach")
179
+ })
180
+ })
181
+ ]
182
+ });
183
+ };
184
+ //#endregion
185
+ export { _ as AssistantComposer };
@@ -0,0 +1,26 @@
1
+ import { AssistantAttachment, AttachmentRejection, AttachmentSendBlocker } from './attachments';
2
+ export interface UseAssistantAttachmentsResult {
3
+ /** Files being read/downscaled — no chip of their own yet. See below. */
4
+ preparingCount: number;
5
+ attachments: AssistantAttachment[];
6
+ /** True while any attachment is still being prepared or uploaded. */
7
+ isBusy: boolean;
8
+ /**
9
+ * Why the current attachments block sending, or `null` when they do not.
10
+ * Strictly wider than `isBusy`: it also reports the terminal 'failed' state,
11
+ * which is not busy but must still stop a send. See
12
+ * `getAttachmentSendBlocker`.
13
+ */
14
+ sendBlocker: AttachmentSendBlocker | null;
15
+ /** Most recent rejection, for a transient inline message. */
16
+ rejection: AttachmentRejection | null;
17
+ addFiles: (files: FileList | File[] | null | undefined) => void;
18
+ remove: (id: string) => void;
19
+ /** Re-upload a single failed attachment, reusing the bytes already prepared. */
20
+ retry: (id: string) => void;
21
+ clear: () => void;
22
+ clearRejection: () => void;
23
+ canAddMore: boolean;
24
+ }
25
+ export declare function useAssistantAttachments(): UseAssistantAttachmentsResult;
26
+ //# sourceMappingURL=useAssistantAttachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAssistantAttachments.d.ts","sourceRoot":"","sources":["../../../src/shared/assistant/useAssistantAttachments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAOH,OAAO,EAYL,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC3B,MAAM,eAAe,CAAC;AAmJvB,MAAM,WAAW,6BAA6B;IAC5C,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,qEAAqE;IACrE,MAAM,EAAE,OAAO,CAAC;IAChB;;;;;OAKG;IACH,WAAW,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC1C,6DAA6D;IAC7D,SAAS,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,GAAG,IAAI,GAAG,SAAS,KAAK,IAAI,CAAC;IAChE,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,gFAAgF;IAChF,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,uBAAuB,IAAI,6BAA6B,CAoRvE"}
@@ -0,0 +1,244 @@
1
+ import { graphqlFetch as e } from "../graphql/fetch.js";
2
+ import "../../shared-graphql.js";
3
+ import { downscaleImage as t } from "../utils/imageDownscale.js";
4
+ import { useAssistantTr as n } from "./i18n.js";
5
+ import { AttachmentRejectedError as r, INLINE_IMAGE_QUALITY as i, MAX_INLINE_CHARS_PER_FILE as a, classifyAttachment as o, detectDocumentFormat as s, getAttachmentSendBlocker as c, truncateForInline as l, validateFile as u } from "./attachments.js";
6
+ import { getWorkspaceId as d } from "./context.js";
7
+ import { useSaveToFiles as f } from "../../storage/useSaveToFiles.js";
8
+ import "../../storage/index.js";
9
+ import { extractDocumentText as p } from "./documentExtract.js";
10
+ import { useCallback as m, useEffect as h, useRef as g, useState as _ } from "react";
11
+ //#region src/shared/assistant/useAssistantAttachments.ts
12
+ var v = "/HealthyBowl/AI Assistant/Attachments", y = "\n query HealthyBowlAssistantAttachmentUrl($id: ID!) {\n getFileDownloadUrl(id: $id)\n }\n";
13
+ async function b(t, n) {
14
+ if (!t || !n) return null;
15
+ try {
16
+ return (await e({
17
+ gateway: "workspace",
18
+ query: y,
19
+ variables: { id: t },
20
+ workspaceToken: !0,
21
+ workspaceId: n,
22
+ suppressGlobalErrorEvent: !0
23
+ })).data?.getFileDownloadUrl ?? null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ var x = 0;
29
+ function S() {
30
+ return x += 1, `att-${Date.now()}-${x}`;
31
+ }
32
+ async function C(e) {
33
+ let n = o(e.type, e.name), c = {
34
+ id: S(),
35
+ status: "pending"
36
+ };
37
+ if (n === "image") {
38
+ let a = await t(e, {
39
+ encodeFallback: !1,
40
+ inline: {
41
+ maxDim: 768,
42
+ quality: i
43
+ }
44
+ });
45
+ if (!a.downscaled && a.blob.size > 10485760) throw new r("unsupported-image");
46
+ return {
47
+ attachment: {
48
+ ...c,
49
+ filename: a.filename,
50
+ mimeType: a.mimeType,
51
+ size: a.blob.size,
52
+ kind: n,
53
+ previewUrl: a.dataUrl || void 0,
54
+ inlineDataUrl: a.inlineDataUrl
55
+ },
56
+ blob: a.blob
57
+ };
58
+ }
59
+ if (n === "text") {
60
+ let { text: t, truncated: r } = l(await e.text(), a);
61
+ return {
62
+ attachment: {
63
+ ...c,
64
+ filename: e.name,
65
+ mimeType: e.type || "text/plain",
66
+ size: e.size,
67
+ kind: n,
68
+ textContent: t,
69
+ textTruncated: r
70
+ },
71
+ blob: e
72
+ };
73
+ }
74
+ let u = s(e.type, e.name);
75
+ if (u) {
76
+ let t = await p(e, u);
77
+ return {
78
+ attachment: {
79
+ ...c,
80
+ filename: e.name,
81
+ mimeType: e.type || "application/octet-stream",
82
+ size: e.size,
83
+ kind: n,
84
+ extraction: t.extraction,
85
+ textContent: t.textContent,
86
+ textTruncated: t.textTruncated,
87
+ documentPages: t.documentPages
88
+ },
89
+ blob: e
90
+ };
91
+ }
92
+ return {
93
+ attachment: {
94
+ ...c,
95
+ filename: e.name,
96
+ mimeType: e.type || "application/octet-stream",
97
+ size: e.size,
98
+ kind: n
99
+ },
100
+ blob: e
101
+ };
102
+ }
103
+ function w() {
104
+ let e = n(), { save: t } = f(), [i, a] = _([]), [o, s] = _(null), l = g(Promise.resolve()), [p, y] = _(0), x = g(0), S = g(/* @__PURE__ */ new Map()), w = g(/* @__PURE__ */ new Set()), T = g(0), E = g(!0);
105
+ h(() => () => {
106
+ E.current = !1;
107
+ }, []);
108
+ let D = g([]);
109
+ h(() => {
110
+ D.current = i;
111
+ }, [i]);
112
+ let O = m((e, t) => {
113
+ E.current && a((n) => n.map((n) => n.id === e ? {
114
+ ...n,
115
+ ...t
116
+ } : n));
117
+ }, []), k = m(async ({ attachment: n, blob: r }) => {
118
+ if (!w.current.has(n.id)) {
119
+ O(n.id, {
120
+ status: "uploading",
121
+ progressNote: void 0
122
+ });
123
+ try {
124
+ let i = await t({
125
+ source: {
126
+ kind: "blob",
127
+ filename: n.filename,
128
+ mimeType: n.mimeType,
129
+ getBlob: async () => ({
130
+ blob: r,
131
+ filename: n.filename,
132
+ mimeType: n.mimeType
133
+ })
134
+ },
135
+ folderPath: v,
136
+ onRetry: (t) => {
137
+ w.current.has(n.id) || O(n.id, { progressNote: t.kind === "cold-start" ? e("attachments.waking", "File service is starting up — retry {{attempt}} of {{max}}", {
138
+ attempt: String(t.attempt),
139
+ max: String(t.maxAttempts)
140
+ }) : e("attachments.retrying", "Retrying {{attempt}} of {{max}}", {
141
+ attempt: String(t.attempt),
142
+ max: String(t.maxAttempts)
143
+ }) });
144
+ }
145
+ });
146
+ if (w.current.has(n.id)) return;
147
+ let a = i?.id ? await b(i.id, d()) : null;
148
+ if (w.current.has(n.id)) return;
149
+ O(n.id, {
150
+ status: "ready",
151
+ fileId: i?.id,
152
+ downloadUrl: a ?? void 0,
153
+ progressNote: void 0
154
+ });
155
+ } catch (e) {
156
+ if (w.current.has(n.id)) return;
157
+ if (n.kind === "text" || n.textContent) {
158
+ O(n.id, {
159
+ status: "ready",
160
+ progressNote: void 0
161
+ });
162
+ return;
163
+ }
164
+ O(n.id, {
165
+ status: "error",
166
+ error: e instanceof Error ? e.message : "Upload failed",
167
+ progressNote: void 0
168
+ });
169
+ }
170
+ }
171
+ }, [
172
+ O,
173
+ t,
174
+ e
175
+ ]), A = m((e) => {
176
+ if (!e) return;
177
+ let t = Array.from(e);
178
+ if (t.length === 0) return;
179
+ s(null);
180
+ let n = [];
181
+ for (let e of t) {
182
+ let t = u(e, T.current);
183
+ if (t) {
184
+ s(t);
185
+ continue;
186
+ }
187
+ n.push(e), T.current += 1;
188
+ }
189
+ if (n.length !== 0) {
190
+ y((e) => e + n.length);
191
+ for (let e of n) {
192
+ let t = x.current;
193
+ l.current = l.current.then(async () => {
194
+ let n = null;
195
+ try {
196
+ n = await C(e);
197
+ } catch (e) {
198
+ T.current = Math.max(0, T.current - 1), E.current && s(e instanceof r ? e.reason : "unreadable");
199
+ }
200
+ if (!E.current || (y((e) => Math.max(0, e - 1)), !n)) return;
201
+ if (t !== x.current) {
202
+ T.current = Math.max(0, T.current - 1);
203
+ return;
204
+ }
205
+ let i = n;
206
+ S.current.set(i.attachment.id, i.blob), a((e) => [...e, i.attachment]), await k(i);
207
+ });
208
+ }
209
+ }
210
+ }, [k]), j = m((e) => {
211
+ w.current.add(e), S.current.delete(e), T.current = Math.max(0, T.current - 1), a((t) => t.filter((t) => t.id !== e));
212
+ }, []), M = m((e) => {
213
+ let t = D.current.find((t) => t.id === e), n = S.current.get(e);
214
+ !t || t.status !== "error" || !n || (w.current.delete(e), O(e, {
215
+ status: "pending",
216
+ error: void 0,
217
+ progressNote: void 0
218
+ }), l.current = l.current.then(() => k({
219
+ attachment: {
220
+ ...t,
221
+ status: "pending",
222
+ error: void 0
223
+ },
224
+ blob: n
225
+ })));
226
+ }, [O, k]), N = m(() => {
227
+ x.current += 1, T.current = 0, D.current.forEach((e) => w.current.add(e.id)), S.current.clear(), a([]);
228
+ }, []), P = c(p, i);
229
+ return {
230
+ attachments: i,
231
+ isBusy: P === "in-flight",
232
+ preparingCount: p,
233
+ sendBlocker: P,
234
+ rejection: o,
235
+ addFiles: A,
236
+ remove: j,
237
+ retry: M,
238
+ clear: N,
239
+ clearRejection: m(() => s(null), []),
240
+ canAddMore: T.current < 5
241
+ };
242
+ }
243
+ //#endregion
244
+ export { w as useAssistantAttachments };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Can this user's writes actually LAND in this workspace?
3
+ *
4
+ * ★ Separate from `useAssistantApiAccess`, and deliberately not a replacement
5
+ * for it. That hook asks `myRoles` at ORG scope, which is the right question for
6
+ * "may this person use a mode that mints an OAuth app under their identity".
7
+ * Execution, though, happens at WORKSPACE scope — so an org admin could enter
8
+ * Take-action mode, have the model plan a change, confirm it, and only then hit
9
+ * a permission error. The two questions have different answers and both are
10
+ * legitimate; this hook asks the second one.
11
+ *
12
+ * ★ Measured 2026-09-02 against prod: `checkPermissions` answers cleanly
13
+ * (HTTP 200, no errors) with `allowed: false`, `decisionPath: "default:deny"`,
14
+ * and a null role — for an org OWNER. Workspace/project scopes have no roles
15
+ * provisioned at all, so the honest answer today is "no, for everyone".
16
+ *
17
+ * ★ Which is exactly why this hook only WARNS. Using it to gate the mode would
18
+ * remove Take-action from the product entirely until an ops task lands, and
19
+ * dressing that up as a permissions fix would be worse than the confusion it
20
+ * replaces. The moment roles are provisioned this starts returning `granted`
21
+ * and the warning disappears on its own, with no code change.
22
+ *
23
+ * Fails OPEN on error or while loading: a network blip must not accuse someone
24
+ * of lacking a permission they may well have.
25
+ */
26
+ /** `unknown` while in flight or unanswerable — never treated as a refusal. */
27
+ export type WorkspaceWriteAccess = "granted" | "denied" | "unknown";
28
+ export declare function useWorkspaceWriteAccess(actorId: string | null, workspaceId: string | null, probeResources: readonly string[]): WorkspaceWriteAccess;
29
+ //# sourceMappingURL=useWorkspaceWriteAccess.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useWorkspaceWriteAccess.d.ts","sourceRoot":"","sources":["../../../src/shared/assistant/useWorkspaceWriteAccess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAwBH,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEpE,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,cAAc,EAAE,SAAS,MAAM,EAAE,GAChC,oBAAoB,CAkDtB"}
@@ -0,0 +1,41 @@
1
+ import { graphqlFetch as e } from "../graphql/fetch.js";
2
+ import "../../shared-graphql.js";
3
+ import { useEffect as t, useState as n } from "react";
4
+ //#region src/shared/assistant/useWorkspaceWriteAccess.ts
5
+ var r = "query AssistantWorkspaceWriteAccess($inputs: [PermissionCheckInput!]!) {\n checkPermissions(inputs: $inputs) {\n allowed\n }\n}";
6
+ function i(i, a, o) {
7
+ let [s, c] = n("unknown");
8
+ return t(() => {
9
+ if (!i || !a) {
10
+ c("unknown");
11
+ return;
12
+ }
13
+ let t = !1;
14
+ return c("unknown"), e({
15
+ gateway: "workspace",
16
+ query: r,
17
+ variables: { inputs: o.map((e) => ({
18
+ actorId: i,
19
+ actorType: "USER",
20
+ scopeId: a,
21
+ resourceType: e,
22
+ action: "create"
23
+ })) },
24
+ suppressGlobalErrorEvent: !0
25
+ }).then((e) => {
26
+ if (t) return;
27
+ let n = e.data?.checkPermissions;
28
+ if (!Array.isArray(n) || n.length === 0) {
29
+ c("unknown");
30
+ return;
31
+ }
32
+ c(n.some((e) => e.allowed) ? "granted" : "denied");
33
+ }).catch(() => {
34
+ t || c("unknown");
35
+ }), () => {
36
+ t = !0;
37
+ };
38
+ }, [i, a]), s;
39
+ }
40
+ //#endregion
41
+ export { i as useWorkspaceWriteAccess };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Client-side image downscaling for the AI Pest & Disease scan flow (BOFF-2887)
3
+ * and the AI Assistant composer.
4
+ *
5
+ * A phone photo can be several megabytes; we downscale it to a sane max dimension
6
+ * and re-encode as JPEG before (a) sending the base64 to the diagnosis model and
7
+ * (b) uploading it to wspace-files-svc. This keeps the GraphQL payload small, the
8
+ * upload fast, and the model call cheap — without changing what the farmer sees.
9
+ *
10
+ * Falls back to the original file bytes if the browser can't rasterise the image
11
+ * (e.g. an exotic format), so a scan is never blocked by the optimisation. That
12
+ * fallback is not free — see `encodeFallback`.
13
+ */
14
+ /**
15
+ * The transport encode for the diagnosis mutation: it is sized against the
16
+ * gateway's body cap rather than against the upload. The stored copy stays at
17
+ * the primary 1280px encode — the file library should not be degraded to the
18
+ * transport's budget — while `inline` produces a second, smaller encode from
19
+ * the SAME decode for the wire (768px/q0.6 ≈ 92 KB of base64 fits the 256 KB
20
+ * body cap; the 1280px primary measures ~360 KB and does not).
21
+ */
22
+ export declare const INLINE_ENCODE: {
23
+ readonly maxDim: 768;
24
+ readonly quality: 0.6;
25
+ };
26
+ /**
27
+ * The payload the mutation should carry: the transport encode when we have one,
28
+ * otherwise the primary. Returns '' when neither is usable.
29
+ */
30
+ export declare function selectInlineBase64(image: DownscaledImage): string;
31
+ export interface DownscaledImage {
32
+ /** Re-encoded image blob, for upload to files-svc. */
33
+ blob: Blob;
34
+ /** Bare base64 (no `data:` prefix), for the diagnosis mutation. '' when skipped. */
35
+ base64: string;
36
+ /** `data:<mime>;base64,...` URL, for an <img> preview. '' when skipped. */
37
+ dataUrl: string;
38
+ /**
39
+ * A SECOND, smaller `data:` URL for callers that must fit the image inside a
40
+ * request body (see `DownscaleOptions.inline`). Absent unless asked for, and
41
+ * absent when it would not actually be smaller than `dataUrl`.
42
+ */
43
+ inlineDataUrl?: string;
44
+ mimeType: string;
45
+ filename: string;
46
+ /**
47
+ * Did the browser actually rasterise and re-encode the image?
48
+ *
49
+ * False means `blob` is the ORIGINAL file: the format could not be decoded
50
+ * here (HEIC from an iPhone is the real-world case — neither Chrome nor
51
+ * Firefox decodes it), so nothing was resized, `mimeType` is whatever the file
52
+ * claimed, and any preview built from it cannot render either.
53
+ *
54
+ * NOTE this is "was it rasterised", NOT "did the dimensions shrink". An image
55
+ * already smaller than `maxDim` takes `scale === 1` yet still goes through the
56
+ * canvas and comes back as a perfectly good JPEG — that is a success.
57
+ */
58
+ downscaled: boolean;
59
+ }
60
+ export interface DownscaleOptions {
61
+ /** Longest-edge ceiling for the primary encode. */
62
+ maxDim?: number;
63
+ /** JPEG quality for the primary encode. */
64
+ quality?: number;
65
+ /**
66
+ * Encode the fallback (un-rasterisable) bytes to base64 as well?
67
+ *
68
+ * Default true, for callers that need `base64` no matter what. Pass false when
69
+ * only `blob` is used: base64 costs ~1.37x the file size in a string that is
70
+ * then held in memory, and on the fallback path the file has NOT been
71
+ * downscaled — so a 12 MB HEIC becomes ~16 MB of strings backing a preview the
72
+ * browser cannot render anyway.
73
+ */
74
+ encodeFallback?: boolean;
75
+ /**
76
+ * Also produce `inlineDataUrl` at these settings, reusing the bitmap that has
77
+ * already been decoded (one decode, two encodes — the second is cheap).
78
+ *
79
+ * This exists because "small enough to upload" and "small enough to fit in a
80
+ * request body" are different budgets: the upload can be a 1280px photo, but
81
+ * the turn that carries the pixels to the model has to fit through a gateway
82
+ * that caps bodies at 256 KB. Keeping them separate means the file the farmer
83
+ * later opens from their file library is NOT degraded to the transport's size.
84
+ */
85
+ inline?: {
86
+ maxDim: number;
87
+ quality: number;
88
+ };
89
+ }
90
+ export declare function downscaleImage(file: File, options?: DownscaleOptions): Promise<DownscaledImage>;
91
+ //# sourceMappingURL=imageDownscale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageDownscale.d.ts","sourceRoot":"","sources":["../../../src/shared/utils/imageDownscale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;CAAyC,CAAC;AAQpE;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAIjE;AAED,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,IAAI,EAAE,IAAI,CAAC;IACX,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;OAWG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AA0FD,wBAAsB,cAAc,CAClC,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAiF1B"}