@burdenoff/microfe-buildmyiq 2026.1029.2 → 2026.1029.4

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 (56) hide show
  1. package/dist/buildmyiq/components/EvidenceUpload.d.ts +4 -2
  2. package/dist/buildmyiq/components/EvidenceUpload.d.ts.map +1 -1
  3. package/dist/buildmyiq/components/EvidenceUpload.js +13 -13
  4. package/dist/buildmyiq/components/EvidenceUpload.js.map +1 -1
  5. package/dist/buildmyiq/hooks/aiProctoringSignals.d.ts +84 -0
  6. package/dist/buildmyiq/hooks/aiProctoringSignals.d.ts.map +1 -0
  7. package/dist/buildmyiq/hooks/aiProctoringSignals.js +133 -0
  8. package/dist/buildmyiq/hooks/aiProctoringSignals.js.map +1 -0
  9. package/dist/buildmyiq/hooks/index.d.ts +1 -0
  10. package/dist/buildmyiq/hooks/index.d.ts.map +1 -1
  11. package/dist/buildmyiq/hooks/index.js +1 -0
  12. package/dist/buildmyiq/hooks/useBuiFiles.d.ts +10 -1
  13. package/dist/buildmyiq/hooks/useBuiFiles.d.ts.map +1 -1
  14. package/dist/buildmyiq/hooks/useBuiFiles.js +22 -22
  15. package/dist/buildmyiq/hooks/useBuiFiles.js.map +1 -1
  16. package/dist/buildmyiq/hooks/useBuiProctoring.d.ts +17 -1
  17. package/dist/buildmyiq/hooks/useBuiProctoring.d.ts.map +1 -1
  18. package/dist/buildmyiq/hooks/useBuiProctoring.js +91 -43
  19. package/dist/buildmyiq/hooks/useBuiProctoring.js.map +1 -1
  20. package/dist/buildmyiq/hooks/useBuildmyiqApi.d.ts +1 -0
  21. package/dist/buildmyiq/hooks/useBuildmyiqApi.d.ts.map +1 -1
  22. package/dist/buildmyiq/hooks/useBuildmyiqApi.js +1 -1
  23. package/dist/buildmyiq/hooks/useBuildmyiqApi.js.map +1 -1
  24. package/dist/buildmyiq/pages/assessments/AiMonitoringConsent.d.ts +10 -0
  25. package/dist/buildmyiq/pages/assessments/AiMonitoringConsent.d.ts.map +1 -0
  26. package/dist/buildmyiq/pages/assessments/AiMonitoringConsent.js +111 -0
  27. package/dist/buildmyiq/pages/assessments/AiMonitoringConsent.js.map +1 -0
  28. package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.d.ts.map +1 -1
  29. package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js +236 -232
  30. package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js.map +1 -1
  31. package/dist/buildmyiq/pages/assessments/AssessmentsPage.d.ts.map +1 -1
  32. package/dist/buildmyiq/pages/assessments/AssessmentsPage.js +27 -3
  33. package/dist/buildmyiq/pages/assessments/AssessmentsPage.js.map +1 -1
  34. package/dist/buildmyiq/pages/assessments/ProctoringGate.d.ts +1 -1
  35. package/dist/buildmyiq/pages/assessments/ProctoringGate.d.ts.map +1 -1
  36. package/dist/buildmyiq/pages/assessments/ProctoringGate.js +176 -129
  37. package/dist/buildmyiq/pages/assessments/ProctoringGate.js.map +1 -1
  38. package/dist/buildmyiq/pages/assessments/faceDetection.d.ts +17 -0
  39. package/dist/buildmyiq/pages/assessments/faceDetection.d.ts.map +1 -0
  40. package/dist/buildmyiq/pages/assessments/faceDetection.js +35 -0
  41. package/dist/buildmyiq/pages/assessments/faceDetection.js.map +1 -0
  42. package/dist/buildmyiq/pages/assessments/useAiProctoringMonitor.d.ts +25 -0
  43. package/dist/buildmyiq/pages/assessments/useAiProctoringMonitor.d.ts.map +1 -0
  44. package/dist/buildmyiq/pages/assessments/useAiProctoringMonitor.js +145 -0
  45. package/dist/buildmyiq/pages/assessments/useAiProctoringMonitor.js.map +1 -0
  46. package/dist/buildmyiq/pages/assessments/useProctoringSignalQueue.d.ts +8 -0
  47. package/dist/buildmyiq/pages/assessments/useProctoringSignalQueue.d.ts.map +1 -0
  48. package/dist/buildmyiq/pages/assessments/useProctoringSignalQueue.js +50 -0
  49. package/dist/buildmyiq/pages/assessments/useProctoringSignalQueue.js.map +1 -0
  50. package/dist/buildmyiq/pages/proctoring/ProctoringReviewPage.d.ts.map +1 -1
  51. package/dist/buildmyiq/pages/proctoring/ProctoringReviewPage.js +188 -63
  52. package/dist/buildmyiq/pages/proctoring/ProctoringReviewPage.js.map +1 -1
  53. package/dist/buildmyiq/types/index.d.ts +55 -0
  54. package/dist/buildmyiq/types/index.d.ts.map +1 -1
  55. package/dist/buildmyiq/types/index.js.map +1 -1
  56. package/package.json +2 -1
@@ -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"}
@@ -0,0 +1,84 @@
1
+ import { BuiProctoringSignalInput, BuiProctoringSignalType } from '../types';
2
+ /** Lockdown monitor signal names (see `useLockdownMonitor`) mapped to backend types. */
3
+ export declare const LOCKDOWN_SIGNAL_TYPE: {
4
+ readonly 'tab-hidden': "TAB_HIDDEN";
5
+ readonly 'window-blur': "WINDOW_BLUR";
6
+ readonly 'fullscreen-exit': "FULLSCREEN_EXIT";
7
+ readonly clipboard: "CLIPBOARD";
8
+ };
9
+ /** The conditions the AI monitor tracks as episodes. */
10
+ export type AiCondition = 'FACE_ABSENT' | 'MULTIPLE_FACES' | 'GAZE_AWAY' | 'VOICE_DETECTED';
11
+ /** How long a condition must last before it counts as an episode. */
12
+ export declare const EPISODE_MIN_MS: Record<AiCondition, number>;
13
+ /** A condition must stay clear this long before its episode closes (absorbs flicker). */
14
+ export declare const EPISODE_RELEASE_MS = 1500;
15
+ /** |yaw ratio| above this means the head is turned well away from the screen. */
16
+ export declare const GAZE_AWAY_YAW_RATIO = 0.5;
17
+ /** Speech must be this many dB above the calibrated noise floor... */
18
+ export declare const SPEECH_MARGIN_DB = 12;
19
+ /** ...and never quieter than this absolute level. */
20
+ export declare const SPEECH_MIN_LEVEL_DB = -65;
21
+ export interface EpisodeSample {
22
+ confidence?: number | null;
23
+ faceCount?: number | null;
24
+ levelDb?: number | null;
25
+ }
26
+ /**
27
+ * Turns per-sample condition flags into closed episodes. Call `update` on
28
+ * every sample; it returns any episodes that closed on that sample. Call
29
+ * `flush` when monitoring stops to close whatever is still open.
30
+ */
31
+ export declare class EpisodeTracker {
32
+ private open;
33
+ update(condition: AiCondition, active: boolean, now: number, sample?: EpisodeSample): BuiProctoringSignalInput[];
34
+ /** True while the condition has an episode in progress that already meets its minimum. */
35
+ isOngoing(condition: AiCondition, now: number): boolean;
36
+ /** Close every open episode (monitoring stopped). */
37
+ flush(): BuiProctoringSignalInput[];
38
+ }
39
+ export interface Keypoint {
40
+ x: number;
41
+ y: number;
42
+ }
43
+ /**
44
+ * Head-turn estimate from BlazeFace keypoints (0 right eye, 1 left eye,
45
+ * 2 nose tip): how far the nose sits from the midpoint of the eyes, in units
46
+ * of the distance between the eyes. About 0 when facing the screen; beyond
47
+ * ±0.5 the head is turned roughly 40° or more. Null when the keypoints are
48
+ * missing or degenerate.
49
+ */
50
+ export declare function estimateYawRatio(keypoints: readonly Keypoint[] | undefined): number | null;
51
+ export declare function isLookingAway(yawRatio: number | null): boolean;
52
+ /**
53
+ * Average level (dBFS) of the speech band (300–3400 Hz) from an
54
+ * AnalyserNode's `getFloatFrequencyData` output. Averages in the power
55
+ * domain, so a loud voice is not drowned out by quiet bins.
56
+ */
57
+ export declare function speechBandLevelDb(frequencyDb: ArrayLike<number>, sampleRate: number, fftSize: number, lowHz?: number, highHz?: number): number;
58
+ /**
59
+ * Tracks the room's noise floor: the 20th-percentile of recent levels. Needs
60
+ * a few seconds of samples before it will call anything speech.
61
+ */
62
+ export declare class NoiseFloor {
63
+ private readonly windowSize;
64
+ private readonly minSamples;
65
+ private samples;
66
+ constructor(windowSize?: number, minSamples?: number);
67
+ add(levelDb: number): void;
68
+ /** Current floor, or null while still calibrating. */
69
+ value(): number | null;
70
+ isSpeech(levelDb: number): boolean;
71
+ }
72
+ /** Human summary of a signal type for reviewers and the learner banner. */
73
+ export declare const SIGNAL_LABELS: Record<BuiProctoringSignalType, string>;
74
+ /** AI-derived signal types (shown with an "AI" marker to reviewers). */
75
+ export declare const AI_SIGNAL_TYPES: readonly BuiProctoringSignalType[];
76
+ /** Published retention period for proctoring signals (matches the backend). */
77
+ export declare const PROCTORING_SIGNAL_RETENTION_DAYS = 60;
78
+ /** The backend accepts at most this many signals per call. */
79
+ export declare const MAX_SIGNALS_PER_BATCH = 50;
80
+ /** Split a queue into backend-sized batches. */
81
+ export declare function toBatches<T>(items: readonly T[], size?: number): T[][];
82
+ /** "1 min 05 s" style duration for reviewer timelines. */
83
+ export declare function formatEpisodeDuration(ms: number): string;
84
+ //# sourceMappingURL=aiProctoringSignals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiProctoringSignals.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/aiProctoringSignals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAElF,wFAAwF;AACxF,eAAO,MAAM,oBAAoB;;;;;CAK2B,CAAC;AAE7D,wDAAwD;AACxD,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,gBAAgB,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAE5F,qEAAqE;AACrE,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAKtD,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AAExC,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,qDAAqD;AACrD,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAeD;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAuC;IAEnD,MAAM,CACJ,SAAS,EAAE,WAAW,EACtB,MAAM,EAAE,OAAO,EACf,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,aAAkB,GACzB,wBAAwB,EAAE;IAyB7B,0FAA0F;IAC1F,SAAS,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAKvD,qDAAqD;IACrD,KAAK,IAAI,wBAAwB,EAAE;CASpC;AAuBD,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAO1F;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,SAAS,CAAC,MAAM,CAAC,EAC9B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,KAAK,SAAM,EACX,MAAM,SAAO,GACZ,MAAM,CAcR;AAED;;;GAGG;AACH,qBAAa,UAAU;IAInB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAJ7B,OAAO,CAAC,OAAO,CAAgB;gBAGZ,UAAU,SAAK,EACf,UAAU,SAAI;IAGjC,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAM1B,sDAAsD;IACtD,KAAK,IAAI,MAAM,GAAG,IAAI;IAMtB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;CAKnC;AAED,2EAA2E;AAC3E,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,uBAAuB,EAAE,MAAM,CAWjE,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,eAAe,EAAE,SAAS,uBAAuB,EAO7D,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,gCAAgC,KAAK,CAAC;AAEnD,8DAA8D;AAC9D,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,SAAwB,GAAG,CAAC,EAAE,EAAE,CAIrF;AAED,0DAA0D;AAC1D,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAOxD"}
@@ -0,0 +1,133 @@
1
+ //#region src/buildmyiq/hooks/aiProctoringSignals.ts
2
+ var e = {
3
+ "tab-hidden": "TAB_HIDDEN",
4
+ "window-blur": "WINDOW_BLUR",
5
+ "fullscreen-exit": "FULLSCREEN_EXIT",
6
+ clipboard: "CLIPBOARD"
7
+ }, t = {
8
+ FACE_ABSENT: 3e3,
9
+ MULTIPLE_FACES: 2e3,
10
+ GAZE_AWAY: 4e3,
11
+ VOICE_DETECTED: 3e3
12
+ }, n = 1500, r = .5;
13
+ function i(e, t) {
14
+ return t == null || !Number.isFinite(t) ? e : e == null ? t : Math.max(e, t);
15
+ }
16
+ var a = class {
17
+ open = /* @__PURE__ */ new Map();
18
+ update(e, t, n, r = {}) {
19
+ let a = this.open.get(e);
20
+ if (t) {
21
+ let t = a ?? {
22
+ startedAt: n,
23
+ lastActiveAt: n,
24
+ confidence: null,
25
+ faceCount: null,
26
+ peakLevelDb: null
27
+ };
28
+ return t.lastActiveAt = n, t.confidence = i(t.confidence, r.confidence), t.faceCount = i(t.faceCount, r.faceCount), t.peakLevelDb = i(t.peakLevelDb, r.levelDb), this.open.set(e, t), [];
29
+ }
30
+ if (a && n - a.lastActiveAt >= 1500) {
31
+ this.open.delete(e);
32
+ let t = o(e, a);
33
+ return t ? [t] : [];
34
+ }
35
+ return [];
36
+ }
37
+ isOngoing(e, n) {
38
+ let r = this.open.get(e);
39
+ return r != null && n - r.startedAt >= t[e];
40
+ }
41
+ flush() {
42
+ let e = [];
43
+ for (let [t, n] of this.open) {
44
+ let r = o(t, n);
45
+ r && e.push(r);
46
+ }
47
+ return this.open.clear(), e;
48
+ }
49
+ };
50
+ function o(e, n) {
51
+ let r = Math.round(n.lastActiveAt - n.startedAt);
52
+ if (r < t[e]) return null;
53
+ let i = {
54
+ type: e,
55
+ startedAt: new Date(n.startedAt).toISOString(),
56
+ durationMs: r
57
+ };
58
+ return n.confidence != null && (i.confidence = Math.min(1, Math.max(0, s(n.confidence, 3)))), n.faceCount != null && (i.faceCount = Math.min(20, Math.round(n.faceCount))), n.peakLevelDb != null && (i.peakLevelDb = Math.min(0, Math.max(-160, s(n.peakLevelDb, 1)))), i;
59
+ }
60
+ function s(e, t) {
61
+ let n = 10 ** t;
62
+ return Math.round(e * n) / n;
63
+ }
64
+ function c(e) {
65
+ if (!e || e.length < 3) return null;
66
+ let [t, n, r] = e, i = Math.abs(n.x - t.x);
67
+ if (!Number.isFinite(i) || i < .001) return null;
68
+ let a = (n.x + t.x) / 2;
69
+ return (r.x - a) / i;
70
+ }
71
+ function l(e) {
72
+ return e != null && Math.abs(e) > .5;
73
+ }
74
+ function u(e, t, n, r = 300, i = 3400) {
75
+ let a = t / n, o = Math.max(0, Math.floor(r / a)), s = Math.min(e.length - 1, Math.ceil(i / a)), c = 0, l = 0;
76
+ for (let t = o; t <= s; t++) {
77
+ let n = e[t];
78
+ n == null || !Number.isFinite(n) || (c += 10 ** (n / 10), l += 1);
79
+ }
80
+ return l === 0 || c <= 0 ? -160 : 10 * Math.log10(c / l);
81
+ }
82
+ var d = class {
83
+ windowSize;
84
+ minSamples;
85
+ samples = [];
86
+ constructor(e = 60, t = 5) {
87
+ this.windowSize = e, this.minSamples = t;
88
+ }
89
+ add(e) {
90
+ Number.isFinite(e) && (this.samples.push(e), this.samples.length > this.windowSize && this.samples.shift());
91
+ }
92
+ value() {
93
+ if (this.samples.length < this.minSamples) return null;
94
+ let e = [...this.samples].sort((e, t) => e - t);
95
+ return e[Math.floor(e.length * .2)] ?? null;
96
+ }
97
+ isSpeech(e) {
98
+ let t = this.value();
99
+ return t == null ? !1 : e >= -65 && e - t >= 12;
100
+ }
101
+ }, f = {
102
+ TAB_HIDDEN: "Switched tab or app",
103
+ WINDOW_BLUR: "Exam window lost focus",
104
+ FULLSCREEN_EXIT: "Left fullscreen",
105
+ CLIPBOARD: "Copy, cut or paste",
106
+ FACE_ABSENT: "No face in camera view",
107
+ MULTIPLE_FACES: "More than one face",
108
+ GAZE_AWAY: "Looking away from the screen",
109
+ VOICE_DETECTED: "Sustained speech or voices",
110
+ CAMERA_UNAVAILABLE: "Camera stopped",
111
+ MICROPHONE_UNAVAILABLE: "Microphone stopped"
112
+ }, p = [
113
+ "FACE_ABSENT",
114
+ "MULTIPLE_FACES",
115
+ "GAZE_AWAY",
116
+ "VOICE_DETECTED",
117
+ "CAMERA_UNAVAILABLE",
118
+ "MICROPHONE_UNAVAILABLE"
119
+ ];
120
+ function m(e, t = 50) {
121
+ let n = [];
122
+ for (let r = 0; r < e.length; r += t) n.push(e.slice(r, r + t));
123
+ return n;
124
+ }
125
+ function h(e) {
126
+ if (e <= 0) return "";
127
+ let t = Math.round(e / 1e3), n = Math.floor(t / 60), r = t % 60;
128
+ return n === 0 ? `${r} s` : `${n} min ${String(r).padStart(2, "0")} s`;
129
+ }
130
+ //#endregion
131
+ export { p as AI_SIGNAL_TYPES, t as EPISODE_MIN_MS, n as EPISODE_RELEASE_MS, a as EpisodeTracker, r as GAZE_AWAY_YAW_RATIO, e as LOCKDOWN_SIGNAL_TYPE, d as NoiseFloor, f as SIGNAL_LABELS, c as estimateYawRatio, h as formatEpisodeDuration, l as isLookingAway, u as speechBandLevelDb, m as toBatches };
132
+
133
+ //# sourceMappingURL=aiProctoringSignals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiProctoringSignals.js","names":[],"sources":["../../../src/buildmyiq/hooks/aiProctoringSignals.ts"],"sourcesContent":["/**\n * Pure rules for in-browser AI proctoring signals (BOFF-9690).\n * @module buildmyiq/hooks/aiProctoringSignals\n *\n * The AI monitor samples the learner's camera and microphone on their own\n * device and turns sustained conditions into signal episodes:\n *\n * - FACE_ABSENT no face in frame for at least 3 s\n * - MULTIPLE_FACES more than one face for at least 2 s\n * - GAZE_AWAY head turned well away from the screen for at least 4 s\n * - VOICE_DETECTED speech-band audio well above the room's noise floor for at least 3 s\n *\n * Nothing here touches images or audio buffers beyond a single number per\n * sample (face count, a head-turn ratio, a level in dBFS); the episodes are\n * the only thing sent to the backend. Everything is pure so it can be unit\n * tested without a camera.\n */\n\nimport type { BuiProctoringSignalInput, BuiProctoringSignalType } from '../types';\n\n/** Lockdown monitor signal names (see `useLockdownMonitor`) mapped to backend types. */\nexport const LOCKDOWN_SIGNAL_TYPE = {\n 'tab-hidden': 'TAB_HIDDEN',\n 'window-blur': 'WINDOW_BLUR',\n 'fullscreen-exit': 'FULLSCREEN_EXIT',\n clipboard: 'CLIPBOARD',\n} as const satisfies Record<string, BuiProctoringSignalType>;\n\n/** The conditions the AI monitor tracks as episodes. */\nexport type AiCondition = 'FACE_ABSENT' | 'MULTIPLE_FACES' | 'GAZE_AWAY' | 'VOICE_DETECTED';\n\n/** How long a condition must last before it counts as an episode. */\nexport const EPISODE_MIN_MS: Record<AiCondition, number> = {\n FACE_ABSENT: 3_000,\n MULTIPLE_FACES: 2_000,\n GAZE_AWAY: 4_000,\n VOICE_DETECTED: 3_000,\n};\n\n/** A condition must stay clear this long before its episode closes (absorbs flicker). */\nexport const EPISODE_RELEASE_MS = 1_500;\n\n/** |yaw ratio| above this means the head is turned well away from the screen. */\nexport const GAZE_AWAY_YAW_RATIO = 0.5;\n\n/** Speech must be this many dB above the calibrated noise floor... */\nexport const SPEECH_MARGIN_DB = 12;\n/** ...and never quieter than this absolute level. */\nexport const SPEECH_MIN_LEVEL_DB = -65;\n\nexport interface EpisodeSample {\n confidence?: number | null;\n faceCount?: number | null;\n levelDb?: number | null;\n}\n\ninterface OpenEpisode {\n startedAt: number;\n lastActiveAt: number;\n confidence: number | null;\n faceCount: number | null;\n peakLevelDb: number | null;\n}\n\nfunction maxOrNull(a: number | null, b: number | null | undefined): number | null {\n if (b == null || !Number.isFinite(b)) return a;\n return a == null ? b : Math.max(a, b);\n}\n\n/**\n * Turns per-sample condition flags into closed episodes. Call `update` on\n * every sample; it returns any episodes that closed on that sample. Call\n * `flush` when monitoring stops to close whatever is still open.\n */\nexport class EpisodeTracker {\n private open = new Map<AiCondition, OpenEpisode>();\n\n update(\n condition: AiCondition,\n active: boolean,\n now: number,\n sample: EpisodeSample = {}\n ): BuiProctoringSignalInput[] {\n const current = this.open.get(condition);\n if (active) {\n const episode = current ?? {\n startedAt: now,\n lastActiveAt: now,\n confidence: null,\n faceCount: null,\n peakLevelDb: null,\n };\n episode.lastActiveAt = now;\n episode.confidence = maxOrNull(episode.confidence, sample.confidence);\n episode.faceCount = maxOrNull(episode.faceCount, sample.faceCount);\n episode.peakLevelDb = maxOrNull(episode.peakLevelDb, sample.levelDb);\n this.open.set(condition, episode);\n return [];\n }\n if (current && now - current.lastActiveAt >= EPISODE_RELEASE_MS) {\n this.open.delete(condition);\n const closed = toSignal(condition, current);\n return closed ? [closed] : [];\n }\n return [];\n }\n\n /** True while the condition has an episode in progress that already meets its minimum. */\n isOngoing(condition: AiCondition, now: number): boolean {\n const episode = this.open.get(condition);\n return episode != null && now - episode.startedAt >= EPISODE_MIN_MS[condition];\n }\n\n /** Close every open episode (monitoring stopped). */\n flush(): BuiProctoringSignalInput[] {\n const out: BuiProctoringSignalInput[] = [];\n for (const [condition, episode] of this.open) {\n const closed = toSignal(condition, episode);\n if (closed) out.push(closed);\n }\n this.open.clear();\n return out;\n }\n}\n\nfunction toSignal(condition: AiCondition, episode: OpenEpisode): BuiProctoringSignalInput | null {\n const durationMs = Math.round(episode.lastActiveAt - episode.startedAt);\n if (durationMs < EPISODE_MIN_MS[condition]) return null;\n const signal: BuiProctoringSignalInput = {\n type: condition,\n startedAt: new Date(episode.startedAt).toISOString(),\n durationMs,\n };\n if (episode.confidence != null)\n signal.confidence = Math.min(1, Math.max(0, round(episode.confidence, 3)));\n if (episode.faceCount != null) signal.faceCount = Math.min(20, Math.round(episode.faceCount));\n if (episode.peakLevelDb != null)\n signal.peakLevelDb = Math.min(0, Math.max(-160, round(episode.peakLevelDb, 1)));\n return signal;\n}\n\nfunction round(value: number, digits: number): number {\n const f = 10 ** digits;\n return Math.round(value * f) / f;\n}\n\nexport interface Keypoint {\n x: number;\n y: number;\n}\n\n/**\n * Head-turn estimate from BlazeFace keypoints (0 right eye, 1 left eye,\n * 2 nose tip): how far the nose sits from the midpoint of the eyes, in units\n * of the distance between the eyes. About 0 when facing the screen; beyond\n * ±0.5 the head is turned roughly 40° or more. Null when the keypoints are\n * missing or degenerate.\n */\nexport function estimateYawRatio(keypoints: readonly Keypoint[] | undefined): number | null {\n if (!keypoints || keypoints.length < 3) return null;\n const [rightEye, leftEye, nose] = keypoints as [Keypoint, Keypoint, Keypoint];\n const eyeDistance = Math.abs(leftEye.x - rightEye.x);\n if (!Number.isFinite(eyeDistance) || eyeDistance < 1e-3) return null;\n const mid = (leftEye.x + rightEye.x) / 2;\n return (nose.x - mid) / eyeDistance;\n}\n\nexport function isLookingAway(yawRatio: number | null): boolean {\n return yawRatio != null && Math.abs(yawRatio) > GAZE_AWAY_YAW_RATIO;\n}\n\n/**\n * Average level (dBFS) of the speech band (300–3400 Hz) from an\n * AnalyserNode's `getFloatFrequencyData` output. Averages in the power\n * domain, so a loud voice is not drowned out by quiet bins.\n */\nexport function speechBandLevelDb(\n frequencyDb: ArrayLike<number>,\n sampleRate: number,\n fftSize: number,\n lowHz = 300,\n highHz = 3400\n): number {\n const binHz = sampleRate / fftSize;\n const from = Math.max(0, Math.floor(lowHz / binHz));\n const to = Math.min(frequencyDb.length - 1, Math.ceil(highHz / binHz));\n let power = 0;\n let bins = 0;\n for (let i = from; i <= to; i++) {\n const db = frequencyDb[i];\n if (db == null || !Number.isFinite(db)) continue;\n power += 10 ** (db / 10);\n bins += 1;\n }\n if (bins === 0 || power <= 0) return -160;\n return 10 * Math.log10(power / bins);\n}\n\n/**\n * Tracks the room's noise floor: the 20th-percentile of recent levels. Needs\n * a few seconds of samples before it will call anything speech.\n */\nexport class NoiseFloor {\n private samples: number[] = [];\n\n constructor(\n private readonly windowSize = 60,\n private readonly minSamples = 5\n ) {}\n\n add(levelDb: number): void {\n if (!Number.isFinite(levelDb)) return;\n this.samples.push(levelDb);\n if (this.samples.length > this.windowSize) this.samples.shift();\n }\n\n /** Current floor, or null while still calibrating. */\n value(): number | null {\n if (this.samples.length < this.minSamples) return null;\n const sorted = [...this.samples].sort((a, b) => a - b);\n return sorted[Math.floor(sorted.length * 0.2)] ?? null;\n }\n\n isSpeech(levelDb: number): boolean {\n const floor = this.value();\n if (floor == null) return false;\n return levelDb >= SPEECH_MIN_LEVEL_DB && levelDb - floor >= SPEECH_MARGIN_DB;\n }\n}\n\n/** Human summary of a signal type for reviewers and the learner banner. */\nexport const SIGNAL_LABELS: Record<BuiProctoringSignalType, string> = {\n TAB_HIDDEN: 'Switched tab or app',\n WINDOW_BLUR: 'Exam window lost focus',\n FULLSCREEN_EXIT: 'Left fullscreen',\n CLIPBOARD: 'Copy, cut or paste',\n FACE_ABSENT: 'No face in camera view',\n MULTIPLE_FACES: 'More than one face',\n GAZE_AWAY: 'Looking away from the screen',\n VOICE_DETECTED: 'Sustained speech or voices',\n CAMERA_UNAVAILABLE: 'Camera stopped',\n MICROPHONE_UNAVAILABLE: 'Microphone stopped',\n};\n\n/** AI-derived signal types (shown with an \"AI\" marker to reviewers). */\nexport const AI_SIGNAL_TYPES: readonly BuiProctoringSignalType[] = [\n 'FACE_ABSENT',\n 'MULTIPLE_FACES',\n 'GAZE_AWAY',\n 'VOICE_DETECTED',\n 'CAMERA_UNAVAILABLE',\n 'MICROPHONE_UNAVAILABLE',\n];\n\n/** Published retention period for proctoring signals (matches the backend). */\nexport const PROCTORING_SIGNAL_RETENTION_DAYS = 60;\n\n/** The backend accepts at most this many signals per call. */\nexport const MAX_SIGNALS_PER_BATCH = 50;\n\n/** Split a queue into backend-sized batches. */\nexport function toBatches<T>(items: readonly T[], size = MAX_SIGNALS_PER_BATCH): T[][] {\n const out: T[][] = [];\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));\n return out;\n}\n\n/** \"1 min 05 s\" style duration for reviewer timelines. */\nexport function formatEpisodeDuration(ms: number): string {\n if (ms <= 0) return '';\n const totalSeconds = Math.round(ms / 1000);\n const minutes = Math.floor(totalSeconds / 60);\n const seconds = totalSeconds % 60;\n if (minutes === 0) return `${seconds} s`;\n return `${minutes} min ${String(seconds).padStart(2, '0')} s`;\n}\n"],"mappings":"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"}
@@ -10,6 +10,7 @@ export * from './useBuiTags';
10
10
  export * from './useBuiPermissions';
11
11
  export * from './useBuiProctoring';
12
12
  export * from './proctoringState';
13
+ export * from './aiProctoringSignals';
13
14
  export * from './useBuiCredentials';
14
15
  export * from './useBuiAccessibility';
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC"}
@@ -6,6 +6,7 @@ import "./useBuiTags.js";
6
6
  import "./useBuiPermissions.js";
7
7
  import "./useBuiProctoring.js";
8
8
  import "./proctoringState.js";
9
+ import "./aiProctoringSignals.js";
9
10
  import "./useBuiCredentials.js";
10
11
  import "./accessibilityDraft.js";
11
12
  import "./useBuiAccessibility.js";
@@ -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, BuiProctoringSignal, BuiProctoringSignalInput, 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
@@ -12,12 +12,28 @@ export declare const useBuiSubmitProctoringIdentity: () => import('@tanstack/rea
12
12
  }, unknown>;
13
13
  /** Records one lockdown violation (tab switch, window blur, fullscreen exit, copy/paste). */
14
14
  export declare const useBuiReportProctoringViolation: () => import('@tanstack/react-query').UseMutationResult<BuiProctoringSession, Error, string, unknown>;
15
+ /**
16
+ * BOFF-9690: records a batch (max 50) of signal episodes on the learner's own
17
+ * approved session — lockdown events and, when the assessment opted in and
18
+ * the learner consented, in-browser AI signals.
19
+ */
20
+ export declare const useBuiRecordProctoringSignals: () => import('@tanstack/react-query').UseMutationResult<BuiProctoringSession, Error, {
21
+ id: string;
22
+ signals: BuiProctoringSignalInput[];
23
+ }, unknown>;
24
+ /** BOFF-9690: reviewer timeline of one session's signals, oldest first, 100 per page. */
25
+ export declare const useBuiProctoringSessionSignals: (sessionId: string | undefined, enabled?: boolean) => import('@tanstack/react-query').UseInfiniteQueryResult<import('@tanstack/query-core').InfiniteData<BuiConnection<BuiProctoringSignal>, unknown>, Error>;
15
26
  /** Reviewer queue: proctoring sessions in the workspace, optionally filtered by status. */
16
27
  export declare const useBuiProctoringSessions: (args: {
17
28
  status?: BuiProctoringStatus;
18
29
  assessmentId?: string;
19
30
  limit?: number;
20
31
  }, enabled?: boolean) => import('@tanstack/react-query').UseQueryResult<NoInfer<BuiConnection<BuiProctoringSession>>, Error>;
32
+ /**
33
+ * Reviewer-only: the learner's identity photo/ID for one session, with a
34
+ * short-lived download URL (BOFF-9691). Null when the session has none.
35
+ */
36
+ export declare const useBuiProctoringIdentityFile: (sessionId: string | undefined, enabled?: boolean) => import('@tanstack/react-query').UseQueryResult<NoInfer<BuiProctoringIdentityFile | null>, Error>;
21
37
  export declare const useBuiReviewProctoringSession: () => import('@tanstack/react-query').UseMutationResult<BuiProctoringSession, Error, {
22
38
  id: string;
23
39
  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;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EACV,aAAa,EACb,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,+BAA+B,EAC/B,gCAAgC,EACjC,MAAM,UAAU,CAAC;AAkClB;;;;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;;;;GAIG;AACH,eAAO,MAAM,6BAA6B;QAIP,MAAM;aAAW,wBAAwB,EAAE;WAc7E,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,8BAA8B,GAAI,WAAW,MAAM,GAAG,SAAS,EAAE,iBAAc,4JA2B3F,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"}