@burdenoff/microfe-buildmyiq 2026.1029.2 → 2026.1029.3

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.
@@ -1,4 +1,4 @@
1
- import { UploadedBuiFile } from '../hooks';
1
+ import { BuiFileVisibility, UploadedBuiFile } from '../hooks';
2
2
  interface EvidenceUploadProps {
3
3
  id: string;
4
4
  label: string;
@@ -7,7 +7,9 @@ interface EvidenceUploadProps {
7
7
  accept?: string;
8
8
  disabled?: boolean;
9
9
  hint?: string;
10
+ /** File-store visibility of the upload; WORKSPACE unless the file is the uploader's alone. */
11
+ visibility?: BuiFileVisibility;
10
12
  }
11
- export declare function EvidenceUpload({ id, label, value, onChange, accept, disabled, hint, }: EvidenceUploadProps): import("react").JSX.Element;
13
+ export declare function EvidenceUpload({ id, label, value, onChange, accept, disabled, hint, visibility, }: EvidenceUploadProps): import("react").JSX.Element;
12
14
  export {};
13
15
  //# sourceMappingURL=EvidenceUpload.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EvidenceUpload.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/EvidenceUpload.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAElE,UAAU,mBAAmB;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,cAAc,CAAC,EAC7B,EAAE,EACF,KAAK,EACL,KAAK,EACL,QAAQ,EACR,MAA0C,EAC1C,QAAgB,EAChB,IAAI,GACL,EAAE,mBAAmB,+BAkFrB"}
1
+ {"version":3,"file":"EvidenceUpload.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/EvidenceUpload.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAoB,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAE1F,UAAU,mBAAmB;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED,wBAAgB,cAAc,CAAC,EAC7B,EAAE,EACF,KAAK,EACL,KAAK,EACL,QAAQ,EACR,MAA0C,EAC1C,QAAgB,EAChB,IAAI,EACJ,UAAU,GACX,EAAE,mBAAmB,+BAkFrB"}
@@ -5,8 +5,8 @@ import { Paperclip as n, Upload as r, X as i } from "lucide-react";
5
5
  import { jsx as a, jsxs as o } from "react/jsx-runtime";
6
6
  import { Button as s, Label as c, Spinner as l } from "@burdenoff/fe-libs/ui";
7
7
  //#region src/buildmyiq/components/EvidenceUpload.tsx
8
- function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,application/pdf,audio/*", disabled: h = !1, hint: g }) {
9
- let _ = t(null), { upload: v, isUploading: y, progress: b, error: x, reset: S } = e();
8
+ function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,application/pdf,audio/*", disabled: h = !1, hint: g, visibility: _ }) {
9
+ let v = t(null), { upload: y, isUploading: b, progress: x, error: S, reset: C } = e({ visibility: _ });
10
10
  return /* @__PURE__ */ o("div", {
11
11
  className: "grid gap-1.5",
12
12
  children: [
@@ -15,16 +15,16 @@ function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,applic
15
15
  children: d
16
16
  }),
17
17
  /* @__PURE__ */ a("input", {
18
- ref: _,
18
+ ref: v,
19
19
  id: u,
20
20
  type: "file",
21
21
  accept: m,
22
22
  className: "hidden",
23
- disabled: h || y,
23
+ disabled: h || b,
24
24
  onChange: async (e) => {
25
25
  let t = e.target.files?.[0];
26
26
  if (e.target.value = "", t) try {
27
- p(await v(t));
27
+ p(await y(t));
28
28
  } catch {}
29
29
  }
30
30
  }),
@@ -40,7 +40,7 @@ function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,applic
40
40
  /* @__PURE__ */ a("button", {
41
41
  type: "button",
42
42
  onClick: () => {
43
- S(), p(null);
43
+ C(), p(null);
44
44
  },
45
45
  disabled: h,
46
46
  "aria-label": "Remove attachment",
@@ -48,7 +48,7 @@ function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,applic
48
48
  children: /* @__PURE__ */ a(i, { className: "h-4 w-4" })
49
49
  })
50
50
  ]
51
- }) : y ? /* @__PURE__ */ o("div", {
51
+ }) : b ? /* @__PURE__ */ o("div", {
52
52
  className: "flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2",
53
53
  children: [
54
54
  /* @__PURE__ */ a(l, { size: "sm" }),
@@ -56,7 +56,7 @@ function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,applic
56
56
  className: "text-sm text-text-secondary",
57
57
  children: [
58
58
  "Uploading… ",
59
- b,
59
+ x,
60
60
  "%"
61
61
  ]
62
62
  }),
@@ -64,7 +64,7 @@ function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,applic
64
64
  className: "ml-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken",
65
65
  children: /* @__PURE__ */ a("div", {
66
66
  className: "h-full rounded-full bg-action-primary-bg transition-[width] duration-150",
67
- style: { width: `${b}%` }
67
+ style: { width: `${x}%` }
68
68
  })
69
69
  })
70
70
  ]
@@ -72,17 +72,17 @@ function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,applic
72
72
  type: "button",
73
73
  variant: "outline",
74
74
  disabled: h,
75
- onClick: () => _.current?.click(),
75
+ onClick: () => v.current?.click(),
76
76
  className: "justify-start",
77
77
  children: [/* @__PURE__ */ a(r, { className: "mr-1.5 h-4 w-4" }), " Choose file"]
78
78
  }),
79
- g && !x && /* @__PURE__ */ a("p", {
79
+ g && !S && /* @__PURE__ */ a("p", {
80
80
  className: "text-xs text-text-tertiary",
81
81
  children: g
82
82
  }),
83
- x && /* @__PURE__ */ a("p", {
83
+ S && /* @__PURE__ */ a("p", {
84
84
  className: "text-xs text-status-error-text",
85
- children: x
85
+ children: S
86
86
  })
87
87
  ]
88
88
  });
@@ -1 +1 @@
1
- {"version":3,"file":"EvidenceUpload.js","names":[],"sources":["../../../src/buildmyiq/components/EvidenceUpload.tsx"],"sourcesContent":["/**\n * EvidenceUpload — a controlled file-attach control (Integration A).\n * @module buildmyiq/components/EvidenceUpload\n *\n * Wraps `useBuiFileUpload` (the wspace-files 3-step flow). On file select it\n * uploads, shows byte-progress + the filename, and emits `{ fileId, fileName }`\n * via `onChange`. The parent stores `fileId` and passes it to the buildmyiq mutations\n * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).\n */\n\nimport { useRef, type ChangeEvent } from 'react';\nimport { Paperclip, Upload, X } from 'lucide-react';\nimport { Button, Label, Spinner } from '@burdenoff/fe-libs/ui';\nimport { useBuiFileUpload, type UploadedBuiFile } from '../hooks';\n\ninterface EvidenceUploadProps {\n id: string;\n label: string;\n value: UploadedBuiFile | null;\n onChange: (value: UploadedBuiFile | null) => void;\n accept?: string;\n disabled?: boolean;\n hint?: string;\n}\n\nexport function EvidenceUpload({\n id,\n label,\n value,\n onChange,\n accept = 'image/*,application/pdf,audio/*',\n disabled = false,\n hint,\n}: EvidenceUploadProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { upload, isUploading, progress, error, reset } = useBuiFileUpload();\n\n const handleSelect = async (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Clear the native input so the same file can be re-selected after a remove.\n event.target.value = '';\n if (!file) return;\n try {\n const uploaded = await upload(file);\n onChange(uploaded);\n } catch {\n // Error surfaced inline via `error`; keep the form usable.\n }\n };\n\n const handleRemove = () => {\n reset();\n onChange(null);\n };\n\n return (\n <div className=\"grid gap-1.5\">\n <Label htmlFor={id}>{label}</Label>\n\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n disabled={disabled || isUploading}\n onChange={handleSelect}\n />\n\n {value ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Paperclip className=\"h-4 w-4 shrink-0 text-text-tertiary\" />\n <span\n className=\"min-w-0 flex-1 truncate text-sm text-text-primary\"\n title={value.fileName}\n >\n {value.fileName}\n </span>\n <button\n type=\"button\"\n onClick={handleRemove}\n disabled={disabled}\n aria-label=\"Remove attachment\"\n className=\"rounded-md p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n ) : isUploading ? (\n <div className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Spinner size=\"sm\" />\n <span className=\"text-sm text-text-secondary\">Uploading… {progress}%</span>\n <div className=\"ml-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg transition-[width] duration-150\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={disabled}\n onClick={() => inputRef.current?.click()}\n className=\"justify-start\"\n >\n <Upload className=\"mr-1.5 h-4 w-4\" /> Choose file\n </Button>\n )}\n\n {hint && !error && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAyBA,SAAgB,EAAe,EAC7B,OACA,UACA,UACA,aACA,YAAS,mCACT,cAAW,IACX,WACsB;CACtB,IAAM,IAAW,EAAyB,IAAI,GACxC,EAAE,WAAQ,gBAAa,aAAU,UAAO,aAAU,EAAiB;CAoBzE,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IAAO,SAAS;cAAK;GAAa,CAAA;GAElC,kBAAC,SAAD;IACE,KAAK;IACD;IACJ,MAAK;IACG;IACR,WAAU;IACV,UAAU,KAAY;IACtB,UAAU,OA7BY,MAAyC;KACnE,IAAM,IAAO,EAAM,OAAO,QAAQ;KAElC,MAAM,OAAO,QAAQ,IAChB,GACL,IAAI;MAEF,EAAS,MADc,EAAO,CAAI,CACjB;KACnB,QAAQ,CAER;IACF;GAmBK,CAAA;GAEA,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAW,WAAU,sCAAuC,CAAA;KAC5D,kBAAC,QAAD;MACE,WAAU;MACV,OAAO,EAAM;gBAEZ,EAAM;KACH,CAAA;KACN,kBAAC,UAAD;MACE,MAAK;MACL,eA9BiB;OAEzB,AADA,EAAM,GACN,EAAS,IAAI;MACf;MA4BoB;MACV,cAAW;MACX,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;KAClB,CAAA;IACL;QACH,IACF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA;KACpB,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAA8C;OAAY;OAAS;MAAO;;KAC1E,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAS,GAAG;MAChC,CAAA;KACE,CAAA;IACF;QAEL,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACE;IACV,eAAe,EAAS,SAAS,MAAM;IACvC,WAAU;cALZ,CAOE,kBAAC,GAAD,EAAQ,WAAU,iBAAkB,CAAA,GAAC,cAC/B;;GAGT,KAAQ,CAAC,KAAS,kBAAC,KAAD;IAAG,WAAU;cAA8B;GAAQ,CAAA;GACrE,KAAS,kBAAC,KAAD;IAAG,WAAU;cAAkC;GAAS,CAAA;EAC/D;;AAET"}
1
+ {"version":3,"file":"EvidenceUpload.js","names":[],"sources":["../../../src/buildmyiq/components/EvidenceUpload.tsx"],"sourcesContent":["/**\n * EvidenceUpload — a controlled file-attach control (Integration A).\n * @module buildmyiq/components/EvidenceUpload\n *\n * Wraps `useBuiFileUpload` (the wspace-files 3-step flow). On file select it\n * uploads, shows byte-progress + the filename, and emits `{ fileId, fileName }`\n * via `onChange`. The parent stores `fileId` and passes it to the buildmyiq mutations\n * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).\n */\n\nimport { useRef, type ChangeEvent } from 'react';\nimport { Paperclip, Upload, X } from 'lucide-react';\nimport { Button, Label, Spinner } from '@burdenoff/fe-libs/ui';\nimport { useBuiFileUpload, type BuiFileVisibility, type UploadedBuiFile } from '../hooks';\n\ninterface EvidenceUploadProps {\n id: string;\n label: string;\n value: UploadedBuiFile | null;\n onChange: (value: UploadedBuiFile | null) => void;\n accept?: string;\n disabled?: boolean;\n hint?: string;\n /** File-store visibility of the upload; WORKSPACE unless the file is the uploader's alone. */\n visibility?: BuiFileVisibility;\n}\n\nexport function EvidenceUpload({\n id,\n label,\n value,\n onChange,\n accept = 'image/*,application/pdf,audio/*',\n disabled = false,\n hint,\n visibility,\n}: EvidenceUploadProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { upload, isUploading, progress, error, reset } = useBuiFileUpload({ visibility });\n\n const handleSelect = async (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Clear the native input so the same file can be re-selected after a remove.\n event.target.value = '';\n if (!file) return;\n try {\n const uploaded = await upload(file);\n onChange(uploaded);\n } catch {\n // Error surfaced inline via `error`; keep the form usable.\n }\n };\n\n const handleRemove = () => {\n reset();\n onChange(null);\n };\n\n return (\n <div className=\"grid gap-1.5\">\n <Label htmlFor={id}>{label}</Label>\n\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n disabled={disabled || isUploading}\n onChange={handleSelect}\n />\n\n {value ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Paperclip className=\"h-4 w-4 shrink-0 text-text-tertiary\" />\n <span\n className=\"min-w-0 flex-1 truncate text-sm text-text-primary\"\n title={value.fileName}\n >\n {value.fileName}\n </span>\n <button\n type=\"button\"\n onClick={handleRemove}\n disabled={disabled}\n aria-label=\"Remove attachment\"\n className=\"rounded-md p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n ) : isUploading ? (\n <div className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Spinner size=\"sm\" />\n <span className=\"text-sm text-text-secondary\">Uploading… {progress}%</span>\n <div className=\"ml-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg transition-[width] duration-150\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={disabled}\n onClick={() => inputRef.current?.click()}\n className=\"justify-start\"\n >\n <Upload className=\"mr-1.5 h-4 w-4\" /> Choose file\n </Button>\n )}\n\n {hint && !error && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n"],"mappings":";;;;;;;AA2BA,SAAgB,EAAe,EAC7B,OACA,UACA,UACA,aACA,YAAS,mCACT,cAAW,IACX,SACA,iBACsB;CACtB,IAAM,IAAW,EAAyB,IAAI,GACxC,EAAE,WAAQ,gBAAa,aAAU,UAAO,aAAU,EAAiB,EAAE,cAAW,CAAC;CAoBvF,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IAAO,SAAS;cAAK;GAAa,CAAA;GAElC,kBAAC,SAAD;IACE,KAAK;IACD;IACJ,MAAK;IACG;IACR,WAAU;IACV,UAAU,KAAY;IACtB,UAAU,OA7BY,MAAyC;KACnE,IAAM,IAAO,EAAM,OAAO,QAAQ;KAElC,MAAM,OAAO,QAAQ,IAChB,GACL,IAAI;MAEF,EAAS,MADc,EAAO,CAAI,CACjB;KACnB,QAAQ,CAER;IACF;GAmBK,CAAA;GAEA,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAW,WAAU,sCAAuC,CAAA;KAC5D,kBAAC,QAAD;MACE,WAAU;MACV,OAAO,EAAM;gBAEZ,EAAM;KACH,CAAA;KACN,kBAAC,UAAD;MACE,MAAK;MACL,eA9BiB;OAEzB,AADA,EAAM,GACN,EAAS,IAAI;MACf;MA4BoB;MACV,cAAW;MACX,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;KAClB,CAAA;IACL;QACH,IACF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA;KACpB,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAA8C;OAAY;OAAS;MAAO;;KAC1E,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAS,GAAG;MAChC,CAAA;KACE,CAAA;IACF;QAEL,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACE;IACV,eAAe,EAAS,SAAS,MAAM;IACvC,WAAU;cALZ,CAOE,kBAAC,GAAD,EAAQ,WAAU,iBAAkB,CAAA,GAAC,cAC/B;;GAGT,KAAQ,CAAC,KAAS,kBAAC,KAAD;IAAG,WAAU;cAA8B;GAAQ,CAAA;GACrE,KAAS,kBAAC,KAAD;IAAG,WAAU;cAAkC;GAAS,CAAA;EAC/D;;AAET"}
@@ -22,6 +22,15 @@ export interface UploadedBuiFile {
22
22
  fileId: string;
23
23
  fileName: string;
24
24
  }
25
+ /** Who can see an uploaded file in the platform file store. */
26
+ export type BuiFileVisibility = 'PRIVATE' | 'WORKSPACE';
27
+ export interface UseBuiFileUploadOptions {
28
+ /**
29
+ * Defaults to WORKSPACE (course media, evidence). Use PRIVATE for anything only
30
+ * the uploader may see, such as a proctoring photo ID (BOFF-9691).
31
+ */
32
+ visibility?: BuiFileVisibility;
33
+ }
25
34
  export interface UseBuiFileUploadResult {
26
35
  upload: (file: File) => Promise<UploadedBuiFile>;
27
36
  isUploading: boolean;
@@ -35,7 +44,7 @@ export interface UseBuiFileUploadResult {
35
44
  * resolves to `{ fileId, fileName }`. `fileId` is what the buildmyiq mutations accept
36
45
  * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).
37
46
  */
38
- export declare const useBuiFileUpload: () => UseBuiFileUploadResult;
47
+ export declare const useBuiFileUpload: (options?: UseBuiFileUploadOptions) => UseBuiFileUploadResult;
39
48
  export interface BuiFileMeta {
40
49
  id: string;
41
50
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiFiles.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiFiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA8BH,8EAA8E;AAC9E,eAAO,MAAM,WAAW,GAAU,KAAK,EACrC,OAAO,MAAM,EACb,WAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,UAAU,MAAM,KACf,OAAO,CAAC,KAAK,CAmBf,CAAC;AAMF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AA+BD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACjD,WAAW,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAO,sBAqFnC,CAAC;AAMF,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,UAAU,GAAI,IAAI,MAAM,GAAG,IAAI,GAAG,SAAS,uFAsBvD,CAAC"}
1
+ {"version":3,"file":"useBuiFiles.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiFiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA8BH,8EAA8E;AAC9E,eAAO,MAAM,WAAW,GAAU,KAAK,EACrC,OAAO,MAAM,EACb,WAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,UAAU,MAAM,KACf,OAAO,CAAC,KAAK,CAmBf,CAAC;AAMF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AA+BD,+DAA+D;AAC/D,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,WAAW,CAAC;AAExD,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACjD,WAAW,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,UAAS,uBAA4B,KAAG,sBAsFxE,CAAC;AAMF,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,UAAU,GAAI,IAAI,MAAM,GAAG,IAAI,GAAG,SAAS,uFAsBvD,CAAC"}
@@ -29,42 +29,42 @@ function s(e, t, n, r) {
29
29
  }, o.onerror = () => a(/* @__PURE__ */ Error("Network error while uploading the file.")), o.onabort = () => a(/* @__PURE__ */ Error("The upload was cancelled.")), o.send(t);
30
30
  });
31
31
  }
32
- var c = () => {
33
- let { apiGatewayUrl: r } = e(), [i, a] = n(!1), [c, l] = n(0), [u, d] = n(null), f = t(() => {
34
- a(!1), l(0), d(null);
32
+ var c = (r = {}) => {
33
+ let { apiGatewayUrl: i } = e(), a = r.visibility ?? "WORKSPACE", [c, l] = n(!1), [u, d] = n(0), [f, p] = n(null), m = t(() => {
34
+ l(!1), d(0), p(null);
35
35
  }, []);
36
36
  return {
37
37
  upload: t(async (e) => {
38
- a(!0), l(0), d(null);
38
+ l(!0), d(0), p(null);
39
39
  try {
40
- let t = e.name.lastIndexOf("."), n = t > -1 ? e.name.slice(t + 1) : "", i = e.type || "application/octet-stream", a = await o("mutation BuiInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }", { input: {
40
+ let t = e.name.lastIndexOf("."), n = t > -1 ? e.name.slice(t + 1) : "", r = e.type || "application/octet-stream", c = await o("mutation BuiInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }", { input: {
41
41
  fileName: e.name,
42
42
  extension: n,
43
- mimeType: i,
43
+ mimeType: r,
44
44
  fileSize: String(e.size),
45
- visibility: "WORKSPACE"
46
- } }, r), { uploadUrl: c } = a.initiateUpload, u = a.initiateUpload.session.id;
47
- if (!c) throw Error("The file service did not return an upload URL.");
48
- await s(c, e, i, l);
49
- let d = await o("mutation BuiCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file { id name }\n }\n }", { input: {
45
+ visibility: a
46
+ } }, i), { uploadUrl: l } = c.initiateUpload, u = c.initiateUpload.session.id;
47
+ if (!l) throw Error("The file service did not return an upload URL.");
48
+ await s(l, e, r, d);
49
+ let f = await o("mutation BuiCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file { id name }\n }\n }", { input: {
50
50
  sessionId: u,
51
51
  fileSize: String(e.size)
52
- } }, r);
53
- return l(100), {
54
- fileId: d.completeUpload.fileId,
55
- fileName: d.completeUpload.file?.name ?? e.name
52
+ } }, i);
53
+ return d(100), {
54
+ fileId: f.completeUpload.fileId,
55
+ fileName: f.completeUpload.file?.name ?? e.name
56
56
  };
57
57
  } catch (e) {
58
58
  let t = e instanceof Error ? e.message : "The file could not be uploaded.";
59
- throw d(t), e instanceof Error ? e : Error(t);
59
+ throw p(t), e instanceof Error ? e : Error(t);
60
60
  } finally {
61
- a(!1);
61
+ l(!1);
62
62
  }
63
- }, [r]),
64
- isUploading: i,
65
- progress: c,
66
- error: u,
67
- reset: f
63
+ }, [i, a]),
64
+ isUploading: c,
65
+ progress: u,
66
+ error: f,
67
+ reset: m
68
68
  };
69
69
  }, l = (t) => {
70
70
  let { apiGatewayUrl: n } = e();
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiFiles.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiFiles.ts"],"sourcesContent":["/**\n * Buildmyiq — wspace-files-svc REUSE hooks (Integration A).\n * @module buildmyiq/hooks/useBuiFiles\n *\n * Reuses the platform file store (`wspace-files-svc`) WITHOUT any buildmyiq-svc\n * change: the buildmyiq schema already exposes `BuiLesson.mediaFileId` /\n * `BuiCertificate.fileId`; this module lets the UI actually produce a `fileId` via\n * the canonical 3-step upload flow composed into the WORKSPACE supergraph:\n * 1. `initiateUpload(input)` → `{ uploadUrl, session { id } }`\n * 2. HTTP `PUT` the bytes to the returned signed `uploadUrl`\n * 3. `completeUpload(input: { sessionId, fileSize })` → `{ fileId }`\n * plus `getFile(id)` to resolve an existing evidence/report file's name + URL.\n *\n * Transport mirrors `useBuildmyiqApi` exactly — `graphqlFetch({ gateway:\n * 'workspace', workspaceToken: true })` — so no ApolloProvider is required.\n * These hooks live in a SEPARATE file by design; `useBuildmyiqApi.ts` is\n * left untouched.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useBuildmyiq } from '../context';\n\n// ============================================================================\n// Shared workspace-gateway transport (reused by the comments + tags hooks too)\n// ============================================================================\n\ntype GraphqlReadCacheGlobals = {\n __graphqlFetchResponseCache?: { clear: () => void };\n __graphqlFetchPendingResponses?: { clear: () => void };\n __boffGraphQLResponseCache?: { clear: () => void };\n};\n\n/**\n * fe-libs `graphqlFetch` coalesces `query` responses in a short-lived in-memory\n * cache. After a write we drop those caches so the invalidation refetch that\n * follows reads fresh data (same remedy `useBuildmyiqApi` applies).\n */\nconst clearGraphqlReadCaches = (): void => {\n const g = globalThis as GraphqlReadCacheGlobals;\n g.__graphqlFetchResponseCache?.clear();\n g.__graphqlFetchPendingResponses?.clear();\n g.__boffGraphQLResponseCache?.clear();\n};\n\n/** Execute a workspace-gateway GraphQL operation with the workspace token. */\nexport const wspaceFetch = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlReadCaches();\n }\n return response.data;\n};\n\n// ============================================================================\n// File upload (initiate → PUT → complete)\n// ============================================================================\n\nexport interface UploadedBuiFile {\n fileId: string;\n fileName: string;\n}\n\n/** `PUT` the blob to the signed URL, reporting byte-progress via `onProgress`. */\nfunction putWithProgress(\n url: string,\n file: File,\n contentType: string,\n onProgress: (percent: number) => void\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('PUT', url);\n xhr.setRequestHeader('Content-Type', contentType);\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) {\n onProgress(Math.round((event.loaded / event.total) * 100));\n }\n };\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n reject(new Error(`File storage rejected the upload (HTTP ${xhr.status}).`));\n }\n };\n xhr.onerror = () => reject(new Error('Network error while uploading the file.'));\n xhr.onabort = () => reject(new Error('The upload was cancelled.'));\n xhr.send(file);\n });\n}\n\nexport interface UseBuiFileUploadResult {\n upload: (file: File) => Promise<UploadedBuiFile>;\n isUploading: boolean;\n /** 0–100 byte-progress of the signed-URL PUT. */\n progress: number;\n error: string | null;\n reset: () => void;\n}\n\n/**\n * Imperative upload hook: `await upload(file)` runs the platform 3-step flow and\n * resolves to `{ fileId, fileName }`. `fileId` is what the buildmyiq mutations accept\n * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).\n */\nexport const useBuiFileUpload = (): UseBuiFileUploadResult => {\n const { apiGatewayUrl } = useBuildmyiq();\n const [isUploading, setIsUploading] = useState(false);\n const [progress, setProgress] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const reset = useCallback(() => {\n setIsUploading(false);\n setProgress(0);\n setError(null);\n }, []);\n\n const upload = useCallback(\n async (file: File): Promise<UploadedBuiFile> => {\n setIsUploading(true);\n setProgress(0);\n setError(null);\n try {\n const dotIndex = file.name.lastIndexOf('.');\n const extension = dotIndex > -1 ? file.name.slice(dotIndex + 1) : '';\n const mimeType = file.type || 'application/octet-stream';\n\n // 1. initiate\n const initiated = await wspaceFetch<{\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n }>(\n `mutation BuiInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }`,\n {\n input: {\n fileName: file.name,\n extension,\n mimeType,\n fileSize: String(file.size),\n visibility: 'WORKSPACE',\n },\n },\n apiGatewayUrl\n );\n\n const { uploadUrl } = initiated.initiateUpload;\n const sessionId = initiated.initiateUpload.session.id;\n if (!uploadUrl) {\n throw new Error('The file service did not return an upload URL.');\n }\n\n // 2. PUT the bytes\n await putWithProgress(uploadUrl, file, mimeType, setProgress);\n\n // 3. complete — `CompleteUploadInput.fileSize` is a REQUIRED BigInt! in\n // the composed schema (quota tracking; must match initiateUpload's fileSize).\n const completed = await wspaceFetch<{\n completeUpload: { fileId: string; file: { id: string; name: string } | null };\n }>(\n `mutation BuiCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file { id name }\n }\n }`,\n { input: { sessionId, fileSize: String(file.size) } },\n apiGatewayUrl\n );\n\n setProgress(100);\n return {\n fileId: completed.completeUpload.fileId,\n fileName: completed.completeUpload.file?.name ?? file.name,\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : 'The file could not be uploaded.';\n setError(message);\n throw err instanceof Error ? err : new Error(message);\n } finally {\n setIsUploading(false);\n }\n },\n [apiGatewayUrl]\n );\n\n return { upload, isUploading, progress, error, reset };\n};\n\n// ============================================================================\n// Resolve an existing file (name + download URL) for rendering evidence links\n// ============================================================================\n\nexport interface BuiFileMeta {\n id: string;\n name: string;\n downloadUrl: string | null;\n mimeType: string;\n size: string;\n}\n\nexport const useEcoFile = (id: string | null | undefined) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: ['buildmyiq', 'file', id ?? ''],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await wspaceFetch<{ getFile: BuiFileMeta | null }>(\n `query BuiGetFile($id: ID!) {\n getFile(id: $id) {\n id\n name\n downloadUrl\n mimeType\n size\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.getFile;\n },\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useBuiFiles.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiFiles.ts"],"sourcesContent":["/**\n * Buildmyiq — wspace-files-svc REUSE hooks (Integration A).\n * @module buildmyiq/hooks/useBuiFiles\n *\n * Reuses the platform file store (`wspace-files-svc`) WITHOUT any buildmyiq-svc\n * change: the buildmyiq schema already exposes `BuiLesson.mediaFileId` /\n * `BuiCertificate.fileId`; this module lets the UI actually produce a `fileId` via\n * the canonical 3-step upload flow composed into the WORKSPACE supergraph:\n * 1. `initiateUpload(input)` → `{ uploadUrl, session { id } }`\n * 2. HTTP `PUT` the bytes to the returned signed `uploadUrl`\n * 3. `completeUpload(input: { sessionId, fileSize })` → `{ fileId }`\n * plus `getFile(id)` to resolve an existing evidence/report file's name + URL.\n *\n * Transport mirrors `useBuildmyiqApi` exactly — `graphqlFetch({ gateway:\n * 'workspace', workspaceToken: true })` — so no ApolloProvider is required.\n * These hooks live in a SEPARATE file by design; `useBuildmyiqApi.ts` is\n * left untouched.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useBuildmyiq } from '../context';\n\n// ============================================================================\n// Shared workspace-gateway transport (reused by the comments + tags hooks too)\n// ============================================================================\n\ntype GraphqlReadCacheGlobals = {\n __graphqlFetchResponseCache?: { clear: () => void };\n __graphqlFetchPendingResponses?: { clear: () => void };\n __boffGraphQLResponseCache?: { clear: () => void };\n};\n\n/**\n * fe-libs `graphqlFetch` coalesces `query` responses in a short-lived in-memory\n * cache. After a write we drop those caches so the invalidation refetch that\n * follows reads fresh data (same remedy `useBuildmyiqApi` applies).\n */\nconst clearGraphqlReadCaches = (): void => {\n const g = globalThis as GraphqlReadCacheGlobals;\n g.__graphqlFetchResponseCache?.clear();\n g.__graphqlFetchPendingResponses?.clear();\n g.__boffGraphQLResponseCache?.clear();\n};\n\n/** Execute a workspace-gateway GraphQL operation with the workspace token. */\nexport const wspaceFetch = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlReadCaches();\n }\n return response.data;\n};\n\n// ============================================================================\n// File upload (initiate → PUT → complete)\n// ============================================================================\n\nexport interface UploadedBuiFile {\n fileId: string;\n fileName: string;\n}\n\n/** `PUT` the blob to the signed URL, reporting byte-progress via `onProgress`. */\nfunction putWithProgress(\n url: string,\n file: File,\n contentType: string,\n onProgress: (percent: number) => void\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('PUT', url);\n xhr.setRequestHeader('Content-Type', contentType);\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) {\n onProgress(Math.round((event.loaded / event.total) * 100));\n }\n };\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n reject(new Error(`File storage rejected the upload (HTTP ${xhr.status}).`));\n }\n };\n xhr.onerror = () => reject(new Error('Network error while uploading the file.'));\n xhr.onabort = () => reject(new Error('The upload was cancelled.'));\n xhr.send(file);\n });\n}\n\n/** Who can see an uploaded file in the platform file store. */\nexport type BuiFileVisibility = 'PRIVATE' | 'WORKSPACE';\n\nexport interface UseBuiFileUploadOptions {\n /**\n * Defaults to WORKSPACE (course media, evidence). Use PRIVATE for anything only\n * the uploader may see, such as a proctoring photo ID (BOFF-9691).\n */\n visibility?: BuiFileVisibility;\n}\n\nexport interface UseBuiFileUploadResult {\n upload: (file: File) => Promise<UploadedBuiFile>;\n isUploading: boolean;\n /** 0–100 byte-progress of the signed-URL PUT. */\n progress: number;\n error: string | null;\n reset: () => void;\n}\n\n/**\n * Imperative upload hook: `await upload(file)` runs the platform 3-step flow and\n * resolves to `{ fileId, fileName }`. `fileId` is what the buildmyiq mutations accept\n * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).\n */\nexport const useBuiFileUpload = (options: UseBuiFileUploadOptions = {}): UseBuiFileUploadResult => {\n const { apiGatewayUrl } = useBuildmyiq();\n const visibility: BuiFileVisibility = options.visibility ?? 'WORKSPACE';\n const [isUploading, setIsUploading] = useState(false);\n const [progress, setProgress] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const reset = useCallback(() => {\n setIsUploading(false);\n setProgress(0);\n setError(null);\n }, []);\n\n const upload = useCallback(\n async (file: File): Promise<UploadedBuiFile> => {\n setIsUploading(true);\n setProgress(0);\n setError(null);\n try {\n const dotIndex = file.name.lastIndexOf('.');\n const extension = dotIndex > -1 ? file.name.slice(dotIndex + 1) : '';\n const mimeType = file.type || 'application/octet-stream';\n\n // 1. initiate\n const initiated = await wspaceFetch<{\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n }>(\n `mutation BuiInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }`,\n {\n input: {\n fileName: file.name,\n extension,\n mimeType,\n fileSize: String(file.size),\n visibility,\n },\n },\n apiGatewayUrl\n );\n\n const { uploadUrl } = initiated.initiateUpload;\n const sessionId = initiated.initiateUpload.session.id;\n if (!uploadUrl) {\n throw new Error('The file service did not return an upload URL.');\n }\n\n // 2. PUT the bytes\n await putWithProgress(uploadUrl, file, mimeType, setProgress);\n\n // 3. complete — `CompleteUploadInput.fileSize` is a REQUIRED BigInt! in\n // the composed schema (quota tracking; must match initiateUpload's fileSize).\n const completed = await wspaceFetch<{\n completeUpload: { fileId: string; file: { id: string; name: string } | null };\n }>(\n `mutation BuiCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file { id name }\n }\n }`,\n { input: { sessionId, fileSize: String(file.size) } },\n apiGatewayUrl\n );\n\n setProgress(100);\n return {\n fileId: completed.completeUpload.fileId,\n fileName: completed.completeUpload.file?.name ?? file.name,\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : 'The file could not be uploaded.';\n setError(message);\n throw err instanceof Error ? err : new Error(message);\n } finally {\n setIsUploading(false);\n }\n },\n [apiGatewayUrl, visibility]\n );\n\n return { upload, isUploading, progress, error, reset };\n};\n\n// ============================================================================\n// Resolve an existing file (name + download URL) for rendering evidence links\n// ============================================================================\n\nexport interface BuiFileMeta {\n id: string;\n name: string;\n downloadUrl: string | null;\n mimeType: string;\n size: string;\n}\n\nexport const useEcoFile = (id: string | null | undefined) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: ['buildmyiq', 'file', id ?? ''],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await wspaceFetch<{ getFile: BuiFileMeta | null }>(\n `query BuiGetFile($id: ID!) {\n getFile(id: $id) {\n id\n name\n downloadUrl\n mimeType\n size\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.getFile;\n },\n });\n};\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { BuiConnection, BuiProctoringSession, BuiProctoringStatus, BuiReviewProctoringSessionInput, BuiSubmitProctoringIdentityInput } from '../types';
1
+ import { BuiConnection, BuiProctoringIdentityFile, BuiProctoringSession, BuiProctoringStatus, BuiReviewProctoringSessionInput, BuiSubmitProctoringIdentityInput } from '../types';
2
2
  /**
3
3
  * The calling learner's most recent proctoring session for an assessment
4
4
  * (null when they have never started one). Polls while a reviewer decision
@@ -18,6 +18,11 @@ export declare const useBuiProctoringSessions: (args: {
18
18
  assessmentId?: string;
19
19
  limit?: number;
20
20
  }, enabled?: boolean) => import('@tanstack/react-query').UseQueryResult<NoInfer<BuiConnection<BuiProctoringSession>>, Error>;
21
+ /**
22
+ * Reviewer-only: the learner's identity photo/ID for one session, with a
23
+ * short-lived download URL (BOFF-9691). Null when the session has none.
24
+ */
25
+ export declare const useBuiProctoringIdentityFile: (sessionId: string | undefined, enabled?: boolean) => import('@tanstack/react-query').UseQueryResult<NoInfer<BuiProctoringIdentityFile | null>, Error>;
21
26
  export declare const useBuiReviewProctoringSession: () => import('@tanstack/react-query').UseMutationResult<BuiProctoringSession, Error, {
22
27
  id: string;
23
28
  input: BuiReviewProctoringSessionInput;
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiProctoring.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiProctoring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EACV,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACnB,+BAA+B,EAC/B,gCAAgC,EACjC,MAAM,UAAU,CAAC;AAoBlB;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,MAAM,GAAG,SAAS,EAAE,iBAAc,gGAwBzF,CAAC;AAEF,eAAO,MAAM,4BAA4B,uGAkBxC,CAAC;AAEF,eAAO,MAAM,8BAA8B;QAIR,MAAM;WAAS,gCAAgC;WAejF,CAAC;AAEF,6FAA6F;AAC7F,eAAO,MAAM,+BAA+B,uGAkB3C,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,wBAAwB,GACnC,MAAM;IAAE,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7E,iBAAc,wGAuBf,CAAC;AAEF,eAAO,MAAM,6BAA6B;QAIP,MAAM;WAAS,+BAA+B;WAYhF,CAAC"}
1
+ {"version":3,"file":"useBuiProctoring.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiProctoring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EACV,aAAa,EACb,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,+BAA+B,EAC/B,gCAAgC,EACjC,MAAM,UAAU,CAAC;AA6BlB;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,MAAM,GAAG,SAAS,EAAE,iBAAc,gGAwBzF,CAAC;AAEF,eAAO,MAAM,4BAA4B,uGAkBxC,CAAC;AAEF,eAAO,MAAM,8BAA8B;QAIR,MAAM;WAAS,gCAAgC;WAejF,CAAC;AAEF,6FAA6F;AAC7F,eAAO,MAAM,+BAA+B,uGAkB3C,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,wBAAwB,GACnC,MAAM;IAAE,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7E,iBAAc,wGAuBf,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GAAI,WAAW,MAAM,GAAG,SAAS,EAAE,iBAAc,qGAuBzF,CAAC;AAEF,eAAO,MAAM,6BAA6B;QAIP,MAAM;WAAS,+BAA+B;WAYhF,CAAC"}
@@ -14,9 +14,15 @@ var a = "\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt
14
14
  "buildmyiq",
15
15
  "proctoring",
16
16
  "queue"
17
+ ],
18
+ identityFile: (e) => [
19
+ "buildmyiq",
20
+ "proctoring",
21
+ "identity-file",
22
+ e
17
23
  ]
18
- }, s = 15e3, c = (n, i = !0) => {
19
- let { apiGatewayUrl: c } = e();
24
+ }, s = 4 * 6e4, c = 15e3, l = (n, i = !0) => {
25
+ let { apiGatewayUrl: s } = e();
20
26
  return r({
21
27
  queryKey: n ? o.mine(n) : [
22
28
  "buildmyiq",
@@ -27,13 +33,13 @@ var a = "\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt
27
33
  enabled: !!n && i,
28
34
  queryFn: async () => (await t(`query BuiMyProctoringSession($assessmentId: ID!) {
29
35
  buiMyProctoringSession(assessmentId: $assessmentId) { ${a} }
30
- }`, { assessmentId: n }, c)).buiMyProctoringSession,
36
+ }`, { assessmentId: n }, s)).buiMyProctoringSession,
31
37
  refetchInterval: (e) => {
32
38
  let t = e.state.data?.status;
33
- return t === "UNDER_REVIEW" || t === "IDENTITY_SUBMITTED" ? s : !1;
39
+ return t === "UNDER_REVIEW" || t === "IDENTITY_SUBMITTED" ? c : !1;
34
40
  }
35
41
  });
36
- }, l = () => {
42
+ }, u = () => {
37
43
  let { apiGatewayUrl: r } = e(), s = i();
38
44
  return n({
39
45
  mutationFn: async (e) => (await t(`mutation BuiStartProctoringSession($assessmentId: ID!) {
@@ -43,7 +49,7 @@ var a = "\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt
43
49
  s.setQueryData(o.mine(e.assessmentId), e);
44
50
  }
45
51
  });
46
- }, u = () => {
52
+ }, d = () => {
47
53
  let { apiGatewayUrl: r } = e(), s = i();
48
54
  return n({
49
55
  mutationFn: async (e) => (await t(`mutation BuiSubmitProctoringIdentity($id: ID!, $input: BuiSubmitProctoringIdentityInput!) {
@@ -53,7 +59,7 @@ var a = "\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt
53
59
  s.setQueryData(o.mine(e.assessmentId), e), s.invalidateQueries({ queryKey: o.queue });
54
60
  }
55
61
  });
56
- }, d = () => {
62
+ }, f = () => {
57
63
  let { apiGatewayUrl: r } = e(), s = i();
58
64
  return n({
59
65
  mutationFn: async (e) => (await t(`mutation BuiReportProctoringViolation($id: ID!) {
@@ -63,7 +69,7 @@ var a = "\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt
63
69
  s.setQueryData(o.mine(e.assessmentId), e);
64
70
  }
65
71
  });
66
- }, f = (n, i = !0) => {
72
+ }, p = (n, i = !0) => {
67
73
  let { apiGatewayUrl: s } = e(), { status: c, assessmentId: l, limit: u = 50 } = n;
68
74
  return r({
69
75
  queryKey: [
@@ -84,7 +90,17 @@ var a = "\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt
84
90
  pagination: { limit: u }
85
91
  }, s)).buiListProctoringSessions
86
92
  });
87
- }, p = () => {
93
+ }, m = (n, i = !0) => {
94
+ let { apiGatewayUrl: a } = e();
95
+ return r({
96
+ queryKey: o.identityFile(n ?? "nil"),
97
+ enabled: !!n && i,
98
+ staleTime: s,
99
+ refetchInterval: s,
100
+ retry: !1,
101
+ queryFn: async () => (await t("query BuiProctoringIdentityFile($sessionId: ID!) {\n buiProctoringIdentityFile(sessionId: $sessionId) {\n fileId name mimeType size downloadUrl expiresAt\n }\n }", { sessionId: n }, a)).buiProctoringIdentityFile
102
+ });
103
+ }, h = () => {
88
104
  let { apiGatewayUrl: r } = e(), s = i();
89
105
  return n({
90
106
  mutationFn: async (e) => (await t(`mutation BuiReviewProctoringSession($id: ID!, $input: BuiReviewProctoringSessionInput!) {
@@ -94,6 +110,6 @@ var a = "\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt
94
110
  });
95
111
  };
96
112
  //#endregion
97
- export { c as useBuiMyProctoringSession, f as useBuiProctoringSessions, d as useBuiReportProctoringViolation, p as useBuiReviewProctoringSession, l as useBuiStartProctoringSession, u as useBuiSubmitProctoringIdentity };
113
+ export { l as useBuiMyProctoringSession, m as useBuiProctoringIdentityFile, p as useBuiProctoringSessions, f as useBuiReportProctoringViolation, h as useBuiReviewProctoringSession, u as useBuiStartProctoringSession, d as useBuiSubmitProctoringIdentity };
98
114
 
99
115
  //# sourceMappingURL=useBuiProctoring.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiProctoring.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiProctoring.ts"],"sourcesContent":["/**\n * Buildmyiq — high-stakes exam proctoring hooks (BOFF-4431).\n * @module buildmyiq/hooks/useBuiProctoring\n *\n * Wired to the `wspace-buildmyiq-svc` proctoring contract:\n * - learner (self-scoped): `buiMyProctoringSession`, `buiStartProctoringSession`,\n * `buiSubmitProctoringIdentity`, `buiReportProctoringViolation`\n * - reviewer (workspace-scoped): `buiListProctoringSessions`,\n * `buiReviewProctoringSession`\n *\n * The backend is the gate: `buiSubmitAssessment` refuses a `requiresProctoring`\n * assessment unless the learner's latest session is APPROVED and unexpired.\n * These hooks only drive the UI through that lifecycle.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useBuildmyiq } from '../context';\nimport { wspaceFetch } from './useBuiFiles';\nimport type {\n BuiConnection,\n BuiProctoringSession,\n BuiProctoringStatus,\n BuiReviewProctoringSessionInput,\n BuiSubmitProctoringIdentityInput,\n} from '../types';\n\n// `identityFile` selects ONLY `id`: wspace-files-svc's File entity has no\n// __resolveReference, so any other field on a federated File stub fails the\n// whole operation. The reviewer page resolves the file through the files\n// graph's root `getFile` (FileLink) instead.\nconst PROCTORING_SESSION_FIELDS = `\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt violationCount\n reviewedById reviewedAt reviewNotes expiresAt createdAt updatedAt\n identityFile { id }\n`;\n\nconst PROCTORING_KEY = {\n mine: (assessmentId: string) => ['buildmyiq', 'proctoring', 'mine', assessmentId] as const,\n queue: ['buildmyiq', 'proctoring', 'queue'] as const,\n};\n\n/** How often the learner's waiting screen re-checks for a reviewer decision. */\nconst AWAITING_REVIEW_POLL_MS = 15_000;\n\n/**\n * The calling learner's most recent proctoring session for an assessment\n * (null when they have never started one). Polls while a reviewer decision\n * is pending so the exam unlocks without a manual refresh.\n */\nexport const useBuiMyProctoringSession = (assessmentId: string | undefined, enabled = true) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: assessmentId\n ? PROCTORING_KEY.mine(assessmentId)\n : ['buildmyiq', 'proctoring', 'mine', 'nil'],\n enabled: Boolean(assessmentId) && enabled,\n queryFn: async () => {\n const data = await wspaceFetch<{ buiMyProctoringSession: BuiProctoringSession | null }>(\n `query BuiMyProctoringSession($assessmentId: ID!) {\n buiMyProctoringSession(assessmentId: $assessmentId) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { assessmentId },\n apiGatewayUrl\n );\n return data.buiMyProctoringSession;\n },\n refetchInterval: (query) => {\n const status = query.state.data?.status;\n return status === 'UNDER_REVIEW' || status === 'IDENTITY_SUBMITTED'\n ? AWAITING_REVIEW_POLL_MS\n : false;\n },\n });\n};\n\nexport const useBuiStartProctoringSession = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (assessmentId: string) => {\n const data = await wspaceFetch<{ buiStartProctoringSession: BuiProctoringSession }>(\n `mutation BuiStartProctoringSession($assessmentId: ID!) {\n buiStartProctoringSession(assessmentId: $assessmentId) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { assessmentId },\n apiGatewayUrl\n );\n return data.buiStartProctoringSession;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n },\n });\n};\n\nexport const useBuiSubmitProctoringIdentity = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiSubmitProctoringIdentityInput }) => {\n const data = await wspaceFetch<{ buiSubmitProctoringIdentity: BuiProctoringSession }>(\n `mutation BuiSubmitProctoringIdentity($id: ID!, $input: BuiSubmitProctoringIdentityInput!) {\n buiSubmitProctoringIdentity(id: $id, input: $input) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n args,\n apiGatewayUrl\n );\n return data.buiSubmitProctoringIdentity;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n void qc.invalidateQueries({ queryKey: PROCTORING_KEY.queue });\n },\n });\n};\n\n/** Records one lockdown violation (tab switch, window blur, fullscreen exit, copy/paste). */\nexport const useBuiReportProctoringViolation = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await wspaceFetch<{ buiReportProctoringViolation: BuiProctoringSession }>(\n `mutation BuiReportProctoringViolation($id: ID!) {\n buiReportProctoringViolation(id: $id) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.buiReportProctoringViolation;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n },\n });\n};\n\n/** Reviewer queue: proctoring sessions in the workspace, optionally filtered by status. */\nexport const useBuiProctoringSessions = (\n args: { status?: BuiProctoringStatus; assessmentId?: string; limit?: number },\n enabled = true\n) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const { status, assessmentId, limit = 50 } = args;\n return useQuery({\n queryKey: [...PROCTORING_KEY.queue, status ?? 'ALL', assessmentId ?? 'ALL', limit],\n enabled,\n queryFn: async () => {\n const data = await wspaceFetch<{\n buiListProctoringSessions: BuiConnection<BuiProctoringSession>;\n }>(\n `query BuiListProctoringSessions($assessmentId: ID, $status: BuiProctoringStatus, $pagination: PaginationInput) {\n buiListProctoringSessions(assessmentId: $assessmentId, status: $status, pagination: $pagination) {\n items { ${PROCTORING_SESSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { assessmentId, status, pagination: { limit } },\n apiGatewayUrl\n );\n return data.buiListProctoringSessions;\n },\n });\n};\n\nexport const useBuiReviewProctoringSession = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiReviewProctoringSessionInput }) => {\n const data = await wspaceFetch<{ buiReviewProctoringSession: BuiProctoringSession }>(\n `mutation BuiReviewProctoringSession($id: ID!, $input: BuiReviewProctoringSessionInput!) {\n buiReviewProctoringSession(id: $id, input: $input) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n args,\n apiGatewayUrl\n );\n return data.buiReviewProctoringSession;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: PROCTORING_KEY.queue }),\n });\n};\n"],"mappings":";;;;;AA+BA,IAAM,IAA4B,wLAM5B,IAAiB;CACrB,OAAO,MAAyB;EAAC;EAAa;EAAc;EAAQ;CAAY;CAChF,OAAO;EAAC;EAAa;EAAc;CAAO;AAC5C,GAGM,IAA0B,MAOnB,KAA6B,GAAkC,IAAU,OAAS;CAC7F,IAAM,EAAE,qBAAkB,EAAa;CACvC,OAAO,EAAS;EACd,UAAU,IACN,EAAe,KAAK,CAAY,IAChC;GAAC;GAAa;GAAc;GAAQ;EAAK;EAC7C,SAAS,EAAQ,KAAiB;EAClC,SAAS,aAQA,MAPY,EACjB;kEAC0D,EAA0B;YAEpF,EAAE,gBAAa,GACf,CACF,GACY;EAEd,kBAAkB,MAAU;GAC1B,IAAM,IAAS,EAAM,MAAM,MAAM;GACjC,OAAO,MAAW,kBAAkB,MAAW,uBAC3C,IACA;EACN;CACF,CAAC;AACH,GAEa,UAAqC;CAChD,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB;qEAC6D,EAA0B;YAEvF,EAAE,gBAAa,GACf,CACF,GACY;EAEd,YAAY,MAAY;GACtB,EAAG,aAAa,EAAe,KAAK,EAAQ,YAAY,GAAG,CAAO;EACpE;CACF,CAAC;AACH,GAEa,UAAuC;CAClD,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB;kEAC0D,EAA0B;YAEpF,GACA,CACF,GACY;EAEd,YAAY,MAAY;GAEtB,AADA,EAAG,aAAa,EAAe,KAAK,EAAQ,YAAY,GAAG,CAAO,GAClE,EAAQ,kBAAkB,EAAE,UAAU,EAAe,MAAM,CAAC;EAC9D;CACF,CAAC;AACH,GAGa,UAAwC;CACnD,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB;oDAC4C,EAA0B;YAEtE,EAAE,MAAG,GACL,CACF,GACY;EAEd,YAAY,MAAY;GACtB,EAAG,aAAa,EAAe,KAAK,EAAQ,YAAY,GAAG,CAAO;EACpE;CACF,CAAC;AACH,GAGa,KACX,GACA,IAAU,OACP;CACH,IAAM,EAAE,qBAAkB,EAAa,GACjC,EAAE,WAAQ,iBAAc,WAAQ,OAAO;CAC7C,OAAO,EAAS;EACd,UAAU;GAAC,GAAG,EAAe;GAAO,KAAU;GAAO,KAAgB;GAAO;EAAK;EACjF;EACA,SAAS,aAaA,MAZY,EAGjB;;sBAEc,EAA0B;;;YAIxC;GAAE;GAAc;GAAQ,YAAY,EAAE,SAAM;EAAE,GAC9C,CACF,GACY;CAEhB,CAAC;AACH,GAEa,UAAsC;CACjD,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB;iEACyD,EAA0B;YAEnF,GACA,CACF,GACY;EAEd,iBAAiB,EAAG,kBAAkB,EAAE,UAAU,EAAe,MAAM,CAAC;CAC1E,CAAC;AACH"}
1
+ {"version":3,"file":"useBuiProctoring.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiProctoring.ts"],"sourcesContent":["/**\n * Buildmyiq — high-stakes exam proctoring hooks (BOFF-4431).\n * @module buildmyiq/hooks/useBuiProctoring\n *\n * Wired to the `wspace-buildmyiq-svc` proctoring contract:\n * - learner (self-scoped): `buiMyProctoringSession`, `buiStartProctoringSession`,\n * `buiSubmitProctoringIdentity`, `buiReportProctoringViolation`\n * - reviewer (workspace-scoped): `buiListProctoringSessions`,\n * `buiReviewProctoringSession`, `buiProctoringIdentityFile`\n *\n * The backend is the gate: `buiSubmitAssessment` refuses a `requiresProctoring`\n * assessment unless the learner's latest session is APPROVED and unexpired.\n * These hooks only drive the UI through that lifecycle.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useBuildmyiq } from '../context';\nimport { wspaceFetch } from './useBuiFiles';\nimport type {\n BuiConnection,\n BuiProctoringIdentityFile,\n BuiProctoringSession,\n BuiProctoringStatus,\n BuiReviewProctoringSessionInput,\n BuiSubmitProctoringIdentityInput,\n} from '../types';\n\n// `identityFile` selects ONLY `id`: wspace-files-svc's File entity has no\n// __resolveReference, so any other field on a federated File stub fails the\n// whole operation. The learner's photo ID is PRIVATE in the file store\n// (BOFF-9691), so the reviewer page opens it through `buiProctoringIdentityFile`\n// (useBuiProctoringIdentityFile), never through the files graph.\nconst PROCTORING_SESSION_FIELDS = `\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt violationCount\n reviewedById reviewedAt reviewNotes expiresAt createdAt updatedAt\n identityFile { id }\n`;\n\nconst PROCTORING_KEY = {\n mine: (assessmentId: string) => ['buildmyiq', 'proctoring', 'mine', assessmentId] as const,\n queue: ['buildmyiq', 'proctoring', 'queue'] as const,\n identityFile: (sessionId: string) =>\n ['buildmyiq', 'proctoring', 'identity-file', sessionId] as const,\n};\n\n/**\n * The backend presigns the identity URL for about five minutes; re-fetch well\n * before that so an open review page never holds a dead link.\n */\nconst IDENTITY_URL_REFRESH_MS = 4 * 60_000;\n\n/** How often the learner's waiting screen re-checks for a reviewer decision. */\nconst AWAITING_REVIEW_POLL_MS = 15_000;\n\n/**\n * The calling learner's most recent proctoring session for an assessment\n * (null when they have never started one). Polls while a reviewer decision\n * is pending so the exam unlocks without a manual refresh.\n */\nexport const useBuiMyProctoringSession = (assessmentId: string | undefined, enabled = true) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: assessmentId\n ? PROCTORING_KEY.mine(assessmentId)\n : ['buildmyiq', 'proctoring', 'mine', 'nil'],\n enabled: Boolean(assessmentId) && enabled,\n queryFn: async () => {\n const data = await wspaceFetch<{ buiMyProctoringSession: BuiProctoringSession | null }>(\n `query BuiMyProctoringSession($assessmentId: ID!) {\n buiMyProctoringSession(assessmentId: $assessmentId) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { assessmentId },\n apiGatewayUrl\n );\n return data.buiMyProctoringSession;\n },\n refetchInterval: (query) => {\n const status = query.state.data?.status;\n return status === 'UNDER_REVIEW' || status === 'IDENTITY_SUBMITTED'\n ? AWAITING_REVIEW_POLL_MS\n : false;\n },\n });\n};\n\nexport const useBuiStartProctoringSession = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (assessmentId: string) => {\n const data = await wspaceFetch<{ buiStartProctoringSession: BuiProctoringSession }>(\n `mutation BuiStartProctoringSession($assessmentId: ID!) {\n buiStartProctoringSession(assessmentId: $assessmentId) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { assessmentId },\n apiGatewayUrl\n );\n return data.buiStartProctoringSession;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n },\n });\n};\n\nexport const useBuiSubmitProctoringIdentity = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiSubmitProctoringIdentityInput }) => {\n const data = await wspaceFetch<{ buiSubmitProctoringIdentity: BuiProctoringSession }>(\n `mutation BuiSubmitProctoringIdentity($id: ID!, $input: BuiSubmitProctoringIdentityInput!) {\n buiSubmitProctoringIdentity(id: $id, input: $input) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n args,\n apiGatewayUrl\n );\n return data.buiSubmitProctoringIdentity;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n void qc.invalidateQueries({ queryKey: PROCTORING_KEY.queue });\n },\n });\n};\n\n/** Records one lockdown violation (tab switch, window blur, fullscreen exit, copy/paste). */\nexport const useBuiReportProctoringViolation = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await wspaceFetch<{ buiReportProctoringViolation: BuiProctoringSession }>(\n `mutation BuiReportProctoringViolation($id: ID!) {\n buiReportProctoringViolation(id: $id) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.buiReportProctoringViolation;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n },\n });\n};\n\n/** Reviewer queue: proctoring sessions in the workspace, optionally filtered by status. */\nexport const useBuiProctoringSessions = (\n args: { status?: BuiProctoringStatus; assessmentId?: string; limit?: number },\n enabled = true\n) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const { status, assessmentId, limit = 50 } = args;\n return useQuery({\n queryKey: [...PROCTORING_KEY.queue, status ?? 'ALL', assessmentId ?? 'ALL', limit],\n enabled,\n queryFn: async () => {\n const data = await wspaceFetch<{\n buiListProctoringSessions: BuiConnection<BuiProctoringSession>;\n }>(\n `query BuiListProctoringSessions($assessmentId: ID, $status: BuiProctoringStatus, $pagination: PaginationInput) {\n buiListProctoringSessions(assessmentId: $assessmentId, status: $status, pagination: $pagination) {\n items { ${PROCTORING_SESSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { assessmentId, status, pagination: { limit } },\n apiGatewayUrl\n );\n return data.buiListProctoringSessions;\n },\n });\n};\n\n/**\n * Reviewer-only: the learner's identity photo/ID for one session, with a\n * short-lived download URL (BOFF-9691). Null when the session has none.\n */\nexport const useBuiProctoringIdentityFile = (sessionId: string | undefined, enabled = true) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: PROCTORING_KEY.identityFile(sessionId ?? 'nil'),\n enabled: Boolean(sessionId) && enabled,\n staleTime: IDENTITY_URL_REFRESH_MS,\n refetchInterval: IDENTITY_URL_REFRESH_MS,\n retry: false,\n queryFn: async () => {\n const data = await wspaceFetch<{\n buiProctoringIdentityFile: BuiProctoringIdentityFile | null;\n }>(\n `query BuiProctoringIdentityFile($sessionId: ID!) {\n buiProctoringIdentityFile(sessionId: $sessionId) {\n fileId name mimeType size downloadUrl expiresAt\n }\n }`,\n { sessionId },\n apiGatewayUrl\n );\n return data.buiProctoringIdentityFile;\n },\n });\n};\n\nexport const useBuiReviewProctoringSession = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiReviewProctoringSessionInput }) => {\n const data = await wspaceFetch<{ buiReviewProctoringSession: BuiProctoringSession }>(\n `mutation BuiReviewProctoringSession($id: ID!, $input: BuiReviewProctoringSessionInput!) {\n buiReviewProctoringSession(id: $id, input: $input) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n args,\n apiGatewayUrl\n );\n return data.buiReviewProctoringSession;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: PROCTORING_KEY.queue }),\n });\n};\n"],"mappings":";;;;;AAiCA,IAAM,IAA4B,wLAM5B,IAAiB;CACrB,OAAO,MAAyB;EAAC;EAAa;EAAc;EAAQ;CAAY;CAChF,OAAO;EAAC;EAAa;EAAc;CAAO;CAC1C,eAAe,MACb;EAAC;EAAa;EAAc;EAAiB;CAAS;AAC1D,GAMM,IAA0B,IAAI,KAG9B,IAA0B,MAOnB,KAA6B,GAAkC,IAAU,OAAS;CAC7F,IAAM,EAAE,qBAAkB,EAAa;CACvC,OAAO,EAAS;EACd,UAAU,IACN,EAAe,KAAK,CAAY,IAChC;GAAC;GAAa;GAAc;GAAQ;EAAK;EAC7C,SAAS,EAAQ,KAAiB;EAClC,SAAS,aAQA,MAPY,EACjB;kEAC0D,EAA0B;YAEpF,EAAE,gBAAa,GACf,CACF,GACY;EAEd,kBAAkB,MAAU;GAC1B,IAAM,IAAS,EAAM,MAAM,MAAM;GACjC,OAAO,MAAW,kBAAkB,MAAW,uBAC3C,IACA;EACN;CACF,CAAC;AACH,GAEa,UAAqC;CAChD,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB;qEAC6D,EAA0B;YAEvF,EAAE,gBAAa,GACf,CACF,GACY;EAEd,YAAY,MAAY;GACtB,EAAG,aAAa,EAAe,KAAK,EAAQ,YAAY,GAAG,CAAO;EACpE;CACF,CAAC;AACH,GAEa,UAAuC;CAClD,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB;kEAC0D,EAA0B;YAEpF,GACA,CACF,GACY;EAEd,YAAY,MAAY;GAEtB,AADA,EAAG,aAAa,EAAe,KAAK,EAAQ,YAAY,GAAG,CAAO,GAClE,EAAQ,kBAAkB,EAAE,UAAU,EAAe,MAAM,CAAC;EAC9D;CACF,CAAC;AACH,GAGa,UAAwC;CACnD,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB;oDAC4C,EAA0B;YAEtE,EAAE,MAAG,GACL,CACF,GACY;EAEd,YAAY,MAAY;GACtB,EAAG,aAAa,EAAe,KAAK,EAAQ,YAAY,GAAG,CAAO;EACpE;CACF,CAAC;AACH,GAGa,KACX,GACA,IAAU,OACP;CACH,IAAM,EAAE,qBAAkB,EAAa,GACjC,EAAE,WAAQ,iBAAc,WAAQ,OAAO;CAC7C,OAAO,EAAS;EACd,UAAU;GAAC,GAAG,EAAe;GAAO,KAAU;GAAO,KAAgB;GAAO;EAAK;EACjF;EACA,SAAS,aAaA,MAZY,EAGjB;;sBAEc,EAA0B;;;YAIxC;GAAE;GAAc;GAAQ,YAAY,EAAE,SAAM;EAAE,GAC9C,CACF,GACY;CAEhB,CAAC;AACH,GAMa,KAAgC,GAA+B,IAAU,OAAS;CAC7F,IAAM,EAAE,qBAAkB,EAAa;CACvC,OAAO,EAAS;EACd,UAAU,EAAe,aAAa,KAAa,KAAK;EACxD,SAAS,EAAQ,KAAc;EAC/B,WAAW;EACX,iBAAiB;EACjB,OAAO;EACP,SAAS,aAYA,MAXY,EAGjB,yMAKA,EAAE,aAAU,GACZ,CACF,GACY;CAEhB,CAAC;AACH,GAEa,UAAsC;CACjD,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB;iEACyD,EAA0B;YAEnF,GACA,CACF,GACY;EAEd,iBAAiB,EAAG,kBAAkB,EAAE,UAAU,EAAe,MAAM,CAAC;CAC1E,CAAC;AACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProctoringGate.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/ProctoringGate.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAwB1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,UAAU,mBAAmB;IAC3B,UAAU,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,OAAO,CAAC,CAAC;IAChD,6EAA6E;IAC7E,QAAQ,EAAE,SAAS,CAAC;CACrB;AAoBD,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CAuPlD,CAAC"}
1
+ {"version":3,"file":"ProctoringGate.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/ProctoringGate.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAwB1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,UAAU,mBAAmB;IAC3B,UAAU,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,OAAO,CAAC,CAAC;IAChD,6EAA6E;IAC7E,QAAQ,EAAE,SAAS,CAAC;CACrB;AAoBD,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CAwPlD,CAAC"}
@@ -86,7 +86,8 @@ var O = ({ icon: e, title: t, children: n }) => /* @__PURE__ */ S("div", {
86
86
  value: F,
87
87
  onChange: I,
88
88
  accept: "image/*,application/pdf",
89
- hint: A("buildmyiq.proctoring.idHint", "A government or institution ID showing your name and photo. Only reviewers in this workspace see it.")
89
+ visibility: "PRIVATE",
90
+ hint: A("buildmyiq.proctoring.idHintPrivate", "A government or institution ID showing your name and photo. It is stored privately: only you and the proctoring reviewers in this workspace can open it.")
90
91
  }),
91
92
  /* @__PURE__ */ C("div", {
92
93
  className: "rounded-lg border border-border-seam p-3",
@@ -1 +1 @@
1
- {"version":3,"file":"ProctoringGate.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/ProctoringGate.tsx"],"sourcesContent":["/**\n * ProctoringGate — the learner's path into a high-stakes, proctored exam (BOFF-4431).\n * @module buildmyiq/pages/assessments\n *\n * Wraps the exam for an assessment with `requiresProctoring`. The learner:\n * 1. starts a proctoring session (`buiStartProctoringSession`),\n * 2. uploads a photo ID and accepts the lockdown terms\n * (`buiSubmitProctoringIdentity`), which puts the session UNDER_REVIEW,\n * 3. waits for an instructor to approve it (this screen polls),\n * 4. sits the exam with the lockdown monitor running; tab switches, focus\n * loss, fullscreen exits and clipboard use are reported\n * (`buiReportProctoringViolation`) for the reviewer.\n *\n * The backend is the real gate — `buiSubmitAssessment` refuses a proctored\n * assessment without an APPROVED, unexpired session — so this component only\n * guides the learner through it.\n */\n\nimport { useState, type FC, type ReactNode } from 'react';\nimport {\n AlertTriangle,\n Clock,\n Eye,\n Fingerprint,\n Maximize,\n RefreshCw,\n ShieldAlert,\n ShieldCheck,\n} from 'lucide-react';\nimport { Button, Checkbox, GlassCard, Label } from '@burdenoff/fe-libs/ui';\n\nimport { EvidenceUpload, ErrorView, LoadingView, StatusBadge } from '../../components';\nimport {\n proctoringStep,\n useBuiMyProctoringSession,\n useBuiReportProctoringViolation,\n useBuiStartProctoringSession,\n useBuiSubmitProctoringIdentity,\n type UploadedBuiFile,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { formatDate } from '../../utils/formatters';\nimport type { BuiAssessment } from '../../types';\nimport { useLockdownMonitor } from './useLockdownMonitor';\n\ninterface ProctoringGateProps {\n assessment: Pick<BuiAssessment, 'id' | 'title'>;\n /** The exam itself; rendered only once the learner's session is approved. */\n children: ReactNode;\n}\n\nconst Shell: FC<{ icon: ReactNode; title: string; children: ReactNode }> = ({\n icon,\n title,\n children,\n}) => (\n <div className=\"mx-auto max-w-xl\" data-testid=\"proctoring-gate\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-4 p-6\">\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n </div>\n {children}\n </GlassCard>\n </div>\n);\n\nexport const ProctoringGate: FC<ProctoringGateProps> = ({ assessment, children }) => {\n const tr = useTr();\n const sessionQuery = useBuiMyProctoringSession(assessment.id);\n const start = useBuiStartProctoringSession();\n const submitIdentity = useBuiSubmitProctoringIdentity();\n const reportViolation = useBuiReportProctoringViolation();\n\n const [identityFile, setIdentityFile] = useState<UploadedBuiFile | null>(null);\n const [acknowledged, setAcknowledged] = useState(false);\n\n const session = sessionQuery.data ?? null;\n const step = proctoringStep(session);\n\n const monitor = useLockdownMonitor(step === 'exam', () => {\n if (session) reportViolation.mutate(session.id);\n });\n\n if (sessionQuery.isLoading) {\n return (\n <LoadingView label={tr('buildmyiq.proctoring.loading', 'Checking your proctoring status…')} />\n );\n }\n\n if (sessionQuery.isError) {\n return (\n <ErrorView\n title={tr('buildmyiq.proctoring.loadError', \"Couldn't check your proctoring status\")}\n message={sessionQuery.error instanceof Error ? sessionQuery.error.message : undefined}\n onRetry={() => sessionQuery.refetch()}\n />\n );\n }\n\n if (step === 'start') {\n const previous = session?.status === 'REJECTED' ? session : null;\n const lapsed = session != null && session.status !== 'REJECTED';\n return (\n <Shell\n icon={<ShieldCheck className=\"h-5 w-5\" />}\n title={tr('buildmyiq.proctoring.startTitle', 'This is a proctored exam')}\n >\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'buildmyiq.proctoring.startBody',\n 'Before you can answer, an instructor has to confirm who you are. You will upload a photo ID and agree to the exam rules. Once a reviewer approves your check, you have four hours to complete “{{title}}”.',\n { title: assessment.title }\n )}\n </p>\n {previous && (\n <div className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n <p className=\"font-medium\">\n {tr('buildmyiq.proctoring.rejected', 'Your last identity check was not approved.')}\n </p>\n {previous.reviewNotes && <p className=\"mt-1\">{previous.reviewNotes}</p>}\n </div>\n )}\n {lapsed && (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'buildmyiq.proctoring.lapsed',\n 'Your previous proctoring session has expired. Start a new one to continue.'\n )}\n </p>\n )}\n {start.isError && (\n <p className=\"text-sm text-status-error-text\">\n {start.error instanceof Error ? start.error.message : 'Could not start the session.'}\n </p>\n )}\n <div>\n <Button onClick={() => start.mutate(assessment.id)} disabled={start.isPending}>\n <Fingerprint className=\"mr-1.5 h-4 w-4\" />\n {start.isPending\n ? tr('buildmyiq.proctoring.starting', 'Starting…')\n : tr('buildmyiq.proctoring.start', 'Start identity check')}\n </Button>\n </div>\n </Shell>\n );\n }\n\n if (step === 'identity' && session) {\n const handleSubmit = (): void => {\n submitIdentity.mutate({\n id: session.id,\n input: { identityFileId: identityFile?.fileId, lockdownAcknowledged: acknowledged },\n });\n };\n return (\n <Shell\n icon={<Fingerprint className=\"h-5 w-5\" />}\n title={tr('buildmyiq.proctoring.identityTitle', 'Confirm your identity')}\n >\n <EvidenceUpload\n id=\"proctoring-identity\"\n label={tr('buildmyiq.proctoring.idLabel', 'Photo ID')}\n value={identityFile}\n onChange={setIdentityFile}\n accept=\"image/*,application/pdf\"\n hint={tr(\n 'buildmyiq.proctoring.idHint',\n 'A government or institution ID showing your name and photo. Only reviewers in this workspace see it.'\n )}\n />\n <div className=\"rounded-lg border border-border-seam p-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('buildmyiq.proctoring.rulesTitle', 'Exam rules')}\n </p>\n <ul className=\"mt-2 list-disc space-y-1 pl-5 text-sm text-text-secondary\">\n <li>\n {tr(\n 'buildmyiq.proctoring.ruleFullscreen',\n 'Stay in fullscreen on this tab for the whole exam.'\n )}\n </li>\n <li>\n {tr(\n 'buildmyiq.proctoring.ruleTabs',\n 'Switching tabs or apps, leaving fullscreen, and copying or pasting are recorded and shown to your reviewer.'\n )}\n </li>\n <li>\n {tr(\n 'buildmyiq.proctoring.ruleOwnWork',\n 'Answer on your own, without notes or help unless your instructor allowed them.'\n )}\n </li>\n </ul>\n <div className=\"mt-3 flex items-start gap-2\">\n <Checkbox\n id=\"proctoring-ack\"\n checked={acknowledged}\n onCheckedChange={(checked) => setAcknowledged(checked === true)}\n />\n <Label htmlFor=\"proctoring-ack\" className=\"cursor-pointer text-sm leading-snug\">\n {tr('buildmyiq.proctoring.ack', 'I have read the exam rules and agree to them.')}\n </Label>\n </div>\n </div>\n {submitIdentity.isError && (\n <p className=\"text-sm text-status-error-text\">\n {submitIdentity.error instanceof Error\n ? submitIdentity.error.message\n : 'Could not submit your identity check.'}\n </p>\n )}\n <div>\n <Button\n onClick={handleSubmit}\n disabled={!identityFile || !acknowledged || submitIdentity.isPending}\n >\n {submitIdentity.isPending\n ? tr('buildmyiq.proctoring.submitting', 'Submitting…')\n : tr('buildmyiq.proctoring.submit', 'Submit for review')}\n </Button>\n </div>\n </Shell>\n );\n }\n\n if (step === 'review' && session) {\n return (\n <Shell\n icon={<Clock className=\"h-5 w-5\" />}\n title={tr('buildmyiq.proctoring.reviewTitle', 'Waiting for a reviewer')}\n >\n <div className=\"flex items-center gap-2\">\n <StatusBadge status={session.status} />\n <span className=\"text-xs text-text-secondary\">\n {tr('buildmyiq.proctoring.expires', 'Session expires {{date}}', {\n date: formatDate(session.expiresAt),\n })}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'buildmyiq.proctoring.reviewBody',\n 'An instructor is checking your ID. This page updates on its own as soon as they decide; you can also check now.'\n )}\n </p>\n <div>\n <Button\n variant=\"secondary\"\n onClick={() => sessionQuery.refetch()}\n disabled={sessionQuery.isFetching}\n >\n <RefreshCw className=\"mr-1.5 h-4 w-4\" />\n {tr('buildmyiq.proctoring.checkNow', 'Check now')}\n </Button>\n </div>\n </Shell>\n );\n }\n\n // step === 'exam'\n const violations = Math.max(session?.violationCount ?? 0, monitor.localViolations);\n return (\n <>\n <div\n className=\"mx-auto mb-4 flex max-w-3xl flex-col gap-3 rounded-2xl border border-border-seam bg-bg-surface p-4 sm:flex-row sm:items-center\"\n data-testid=\"proctoring-banner\"\n >\n <div className=\"flex items-center gap-2\">\n <Eye className=\"h-4 w-4 text-[var(--color-accent-teal)]\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('buildmyiq.proctoring.examBanner', 'Proctored attempt in progress')}\n </span>\n </div>\n <div className=\"flex items-center gap-2 text-sm\">\n {violations > 0 ? (\n <span className=\"inline-flex items-center gap-1 text-status-warning-text\">\n <AlertTriangle className=\"h-4 w-4\" />\n {tr('buildmyiq.proctoring.violations', '{{count}} recorded event(s)', {\n count: violations,\n })}\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 text-status-success-text\">\n <ShieldCheck className=\"h-4 w-4\" />\n {tr('buildmyiq.proctoring.noViolations', 'No events recorded')}\n </span>\n )}\n </div>\n {!monitor.isFullscreen && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"sm:ml-auto\"\n onClick={() => void monitor.enterFullscreen()}\n >\n <Maximize className=\"mr-1.5 h-4 w-4\" />\n {tr('buildmyiq.proctoring.fullscreen', 'Enter fullscreen')}\n </Button>\n )}\n </div>\n {reportViolation.isError && (\n <p className=\"mx-auto mb-4 flex max-w-3xl items-center gap-2 text-sm text-status-error-text\">\n <ShieldAlert className=\"h-4 w-4\" />\n {tr(\n 'buildmyiq.proctoring.reportError',\n 'An exam event could not be recorded. Stay on this tab and check your connection.'\n )}\n </p>\n )}\n {children}\n </>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProctoringGate.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/ProctoringGate.tsx"],"sourcesContent":["/**\n * ProctoringGate — the learner's path into a high-stakes, proctored exam (BOFF-4431).\n * @module buildmyiq/pages/assessments\n *\n * Wraps the exam for an assessment with `requiresProctoring`. The learner:\n * 1. starts a proctoring session (`buiStartProctoringSession`),\n * 2. uploads a photo ID and accepts the lockdown terms\n * (`buiSubmitProctoringIdentity`), which puts the session UNDER_REVIEW,\n * 3. waits for an instructor to approve it (this screen polls),\n * 4. sits the exam with the lockdown monitor running; tab switches, focus\n * loss, fullscreen exits and clipboard use are reported\n * (`buiReportProctoringViolation`) for the reviewer.\n *\n * The backend is the real gate — `buiSubmitAssessment` refuses a proctored\n * assessment without an APPROVED, unexpired session — so this component only\n * guides the learner through it.\n */\n\nimport { useState, type FC, type ReactNode } from 'react';\nimport {\n AlertTriangle,\n Clock,\n Eye,\n Fingerprint,\n Maximize,\n RefreshCw,\n ShieldAlert,\n ShieldCheck,\n} from 'lucide-react';\nimport { Button, Checkbox, GlassCard, Label } from '@burdenoff/fe-libs/ui';\n\nimport { EvidenceUpload, ErrorView, LoadingView, StatusBadge } from '../../components';\nimport {\n proctoringStep,\n useBuiMyProctoringSession,\n useBuiReportProctoringViolation,\n useBuiStartProctoringSession,\n useBuiSubmitProctoringIdentity,\n type UploadedBuiFile,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { formatDate } from '../../utils/formatters';\nimport type { BuiAssessment } from '../../types';\nimport { useLockdownMonitor } from './useLockdownMonitor';\n\ninterface ProctoringGateProps {\n assessment: Pick<BuiAssessment, 'id' | 'title'>;\n /** The exam itself; rendered only once the learner's session is approved. */\n children: ReactNode;\n}\n\nconst Shell: FC<{ icon: ReactNode; title: string; children: ReactNode }> = ({\n icon,\n title,\n children,\n}) => (\n <div className=\"mx-auto max-w-xl\" data-testid=\"proctoring-gate\">\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-4 p-6\">\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n </div>\n {children}\n </GlassCard>\n </div>\n);\n\nexport const ProctoringGate: FC<ProctoringGateProps> = ({ assessment, children }) => {\n const tr = useTr();\n const sessionQuery = useBuiMyProctoringSession(assessment.id);\n const start = useBuiStartProctoringSession();\n const submitIdentity = useBuiSubmitProctoringIdentity();\n const reportViolation = useBuiReportProctoringViolation();\n\n const [identityFile, setIdentityFile] = useState<UploadedBuiFile | null>(null);\n const [acknowledged, setAcknowledged] = useState(false);\n\n const session = sessionQuery.data ?? null;\n const step = proctoringStep(session);\n\n const monitor = useLockdownMonitor(step === 'exam', () => {\n if (session) reportViolation.mutate(session.id);\n });\n\n if (sessionQuery.isLoading) {\n return (\n <LoadingView label={tr('buildmyiq.proctoring.loading', 'Checking your proctoring status…')} />\n );\n }\n\n if (sessionQuery.isError) {\n return (\n <ErrorView\n title={tr('buildmyiq.proctoring.loadError', \"Couldn't check your proctoring status\")}\n message={sessionQuery.error instanceof Error ? sessionQuery.error.message : undefined}\n onRetry={() => sessionQuery.refetch()}\n />\n );\n }\n\n if (step === 'start') {\n const previous = session?.status === 'REJECTED' ? session : null;\n const lapsed = session != null && session.status !== 'REJECTED';\n return (\n <Shell\n icon={<ShieldCheck className=\"h-5 w-5\" />}\n title={tr('buildmyiq.proctoring.startTitle', 'This is a proctored exam')}\n >\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'buildmyiq.proctoring.startBody',\n 'Before you can answer, an instructor has to confirm who you are. You will upload a photo ID and agree to the exam rules. Once a reviewer approves your check, you have four hours to complete “{{title}}”.',\n { title: assessment.title }\n )}\n </p>\n {previous && (\n <div className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n <p className=\"font-medium\">\n {tr('buildmyiq.proctoring.rejected', 'Your last identity check was not approved.')}\n </p>\n {previous.reviewNotes && <p className=\"mt-1\">{previous.reviewNotes}</p>}\n </div>\n )}\n {lapsed && (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'buildmyiq.proctoring.lapsed',\n 'Your previous proctoring session has expired. Start a new one to continue.'\n )}\n </p>\n )}\n {start.isError && (\n <p className=\"text-sm text-status-error-text\">\n {start.error instanceof Error ? start.error.message : 'Could not start the session.'}\n </p>\n )}\n <div>\n <Button onClick={() => start.mutate(assessment.id)} disabled={start.isPending}>\n <Fingerprint className=\"mr-1.5 h-4 w-4\" />\n {start.isPending\n ? tr('buildmyiq.proctoring.starting', 'Starting…')\n : tr('buildmyiq.proctoring.start', 'Start identity check')}\n </Button>\n </div>\n </Shell>\n );\n }\n\n if (step === 'identity' && session) {\n const handleSubmit = (): void => {\n submitIdentity.mutate({\n id: session.id,\n input: { identityFileId: identityFile?.fileId, lockdownAcknowledged: acknowledged },\n });\n };\n return (\n <Shell\n icon={<Fingerprint className=\"h-5 w-5\" />}\n title={tr('buildmyiq.proctoring.identityTitle', 'Confirm your identity')}\n >\n <EvidenceUpload\n id=\"proctoring-identity\"\n label={tr('buildmyiq.proctoring.idLabel', 'Photo ID')}\n value={identityFile}\n onChange={setIdentityFile}\n accept=\"image/*,application/pdf\"\n visibility=\"PRIVATE\"\n hint={tr(\n 'buildmyiq.proctoring.idHintPrivate',\n 'A government or institution ID showing your name and photo. It is stored privately: only you and the proctoring reviewers in this workspace can open it.'\n )}\n />\n <div className=\"rounded-lg border border-border-seam p-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('buildmyiq.proctoring.rulesTitle', 'Exam rules')}\n </p>\n <ul className=\"mt-2 list-disc space-y-1 pl-5 text-sm text-text-secondary\">\n <li>\n {tr(\n 'buildmyiq.proctoring.ruleFullscreen',\n 'Stay in fullscreen on this tab for the whole exam.'\n )}\n </li>\n <li>\n {tr(\n 'buildmyiq.proctoring.ruleTabs',\n 'Switching tabs or apps, leaving fullscreen, and copying or pasting are recorded and shown to your reviewer.'\n )}\n </li>\n <li>\n {tr(\n 'buildmyiq.proctoring.ruleOwnWork',\n 'Answer on your own, without notes or help unless your instructor allowed them.'\n )}\n </li>\n </ul>\n <div className=\"mt-3 flex items-start gap-2\">\n <Checkbox\n id=\"proctoring-ack\"\n checked={acknowledged}\n onCheckedChange={(checked) => setAcknowledged(checked === true)}\n />\n <Label htmlFor=\"proctoring-ack\" className=\"cursor-pointer text-sm leading-snug\">\n {tr('buildmyiq.proctoring.ack', 'I have read the exam rules and agree to them.')}\n </Label>\n </div>\n </div>\n {submitIdentity.isError && (\n <p className=\"text-sm text-status-error-text\">\n {submitIdentity.error instanceof Error\n ? submitIdentity.error.message\n : 'Could not submit your identity check.'}\n </p>\n )}\n <div>\n <Button\n onClick={handleSubmit}\n disabled={!identityFile || !acknowledged || submitIdentity.isPending}\n >\n {submitIdentity.isPending\n ? tr('buildmyiq.proctoring.submitting', 'Submitting…')\n : tr('buildmyiq.proctoring.submit', 'Submit for review')}\n </Button>\n </div>\n </Shell>\n );\n }\n\n if (step === 'review' && session) {\n return (\n <Shell\n icon={<Clock className=\"h-5 w-5\" />}\n title={tr('buildmyiq.proctoring.reviewTitle', 'Waiting for a reviewer')}\n >\n <div className=\"flex items-center gap-2\">\n <StatusBadge status={session.status} />\n <span className=\"text-xs text-text-secondary\">\n {tr('buildmyiq.proctoring.expires', 'Session expires {{date}}', {\n date: formatDate(session.expiresAt),\n })}\n </span>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'buildmyiq.proctoring.reviewBody',\n 'An instructor is checking your ID. This page updates on its own as soon as they decide; you can also check now.'\n )}\n </p>\n <div>\n <Button\n variant=\"secondary\"\n onClick={() => sessionQuery.refetch()}\n disabled={sessionQuery.isFetching}\n >\n <RefreshCw className=\"mr-1.5 h-4 w-4\" />\n {tr('buildmyiq.proctoring.checkNow', 'Check now')}\n </Button>\n </div>\n </Shell>\n );\n }\n\n // step === 'exam'\n const violations = Math.max(session?.violationCount ?? 0, monitor.localViolations);\n return (\n <>\n <div\n className=\"mx-auto mb-4 flex max-w-3xl flex-col gap-3 rounded-2xl border border-border-seam bg-bg-surface p-4 sm:flex-row sm:items-center\"\n data-testid=\"proctoring-banner\"\n >\n <div className=\"flex items-center gap-2\">\n <Eye className=\"h-4 w-4 text-[var(--color-accent-teal)]\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('buildmyiq.proctoring.examBanner', 'Proctored attempt in progress')}\n </span>\n </div>\n <div className=\"flex items-center gap-2 text-sm\">\n {violations > 0 ? (\n <span className=\"inline-flex items-center gap-1 text-status-warning-text\">\n <AlertTriangle className=\"h-4 w-4\" />\n {tr('buildmyiq.proctoring.violations', '{{count}} recorded event(s)', {\n count: violations,\n })}\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 text-status-success-text\">\n <ShieldCheck className=\"h-4 w-4\" />\n {tr('buildmyiq.proctoring.noViolations', 'No events recorded')}\n </span>\n )}\n </div>\n {!monitor.isFullscreen && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"sm:ml-auto\"\n onClick={() => void monitor.enterFullscreen()}\n >\n <Maximize className=\"mr-1.5 h-4 w-4\" />\n {tr('buildmyiq.proctoring.fullscreen', 'Enter fullscreen')}\n </Button>\n )}\n </div>\n {reportViolation.isError && (\n <p className=\"mx-auto mb-4 flex max-w-3xl items-center gap-2 text-sm text-status-error-text\">\n <ShieldAlert className=\"h-4 w-4\" />\n {tr(\n 'buildmyiq.proctoring.reportError',\n 'An exam event could not be recorded. Stay on this tab and check your connection.'\n )}\n </p>\n )}\n {children}\n </>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProctoringReviewPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/proctoring/ProctoringReviewPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAuI1C,eAAO,MAAM,oBAAoB,EAAE,EAyLlC,CAAC"}
1
+ {"version":3,"file":"ProctoringReviewPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/proctoring/ProctoringReviewPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AA+K1C,eAAO,MAAM,oBAAoB,EAAE,EAyLlC,CAAC"}