@pablo2410/shared-ui 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,4 +61,22 @@ interface OpiAskBarProps {
61
61
  }
62
62
  declare function OpiAskBar({ onAsk, placeholder, skin, }: OpiAskBarProps): React.JSX.Element;
63
63
 
64
- export { type InsightSeverity, OpiAskBar, type OpiAskBarProps, OpiBadgeChip, OpiInsightsShell, type OpiInsightsShellProps, type OpiInsightsSkin, OpiScoreDial, OpiSectionHeading, severityColor };
64
+ interface GuidedQuestionOption {
65
+ /** Opaque to this component — the consuming app's own question-kind enum. */
66
+ kind: string;
67
+ /** Chip label, e.g. "Worst pillar this period". */
68
+ label: string;
69
+ }
70
+ interface GuidedAnswerView {
71
+ headline: string;
72
+ detail?: string;
73
+ }
74
+ interface OpiGuidedQuestionsProps {
75
+ questions: GuidedQuestionOption[];
76
+ /** Resolves with the phrased answer for the clicked question's kind. */
77
+ onAsk: (kind: string) => Promise<GuidedAnswerView>;
78
+ skin?: OpiInsightsSkin;
79
+ }
80
+ declare function OpiGuidedQuestions({ questions, onAsk, skin, }: OpiGuidedQuestionsProps): React.JSX.Element | null;
81
+
82
+ export { type GuidedAnswerView, type GuidedQuestionOption, type InsightSeverity, OpiAskBar, type OpiAskBarProps, OpiBadgeChip, OpiGuidedQuestions, type OpiGuidedQuestionsProps, OpiInsightsShell, type OpiInsightsShellProps, type OpiInsightsSkin, OpiScoreDial, OpiSectionHeading, severityColor };
@@ -345,9 +345,126 @@ function OpiAskBar({
345
345
  }
346
346
  );
347
347
  }
348
+
349
+ // src/insights/OpiGuidedQuestions.tsx
350
+ import { useState as useState3 } from "react";
351
+ import { Sparkles as Sparkles3, Loader2 as Loader22, XCircle } from "lucide-react";
352
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
353
+ var defaultSkin3 = {
354
+ accent: "#14b8a6",
355
+ panelBg: "#0e1624",
356
+ panelBorder: "1px solid #1e2738",
357
+ mutedText: "#596475",
358
+ bodyText: "#c0c6d0",
359
+ softText: "#8890a0"
360
+ };
361
+ function OpiGuidedQuestions({
362
+ questions,
363
+ onAsk,
364
+ skin = defaultSkin3
365
+ }) {
366
+ const [activeKind, setActiveKind] = useState3(null);
367
+ const [pendingKind, setPendingKind] = useState3(null);
368
+ const [answer, setAnswer] = useState3(null);
369
+ const [error, setError] = useState3(null);
370
+ if (questions.length === 0) return null;
371
+ const handleClick = async (kind) => {
372
+ if (pendingKind) return;
373
+ setActiveKind(kind);
374
+ setPendingKind(kind);
375
+ setAnswer(null);
376
+ setError(null);
377
+ try {
378
+ const result = await onAsk(kind);
379
+ setAnswer(result);
380
+ } catch {
381
+ setError("Couldn't reach Opi just then \u2014 give it another go.");
382
+ } finally {
383
+ setPendingKind(null);
384
+ }
385
+ };
386
+ return /* @__PURE__ */ jsxs3("div", { className: "space-y-2 pb-1", children: [
387
+ /* @__PURE__ */ jsx3("div", { className: "flex flex-wrap gap-1.5", children: questions.map((q) => {
388
+ const isActive = activeKind === q.kind;
389
+ const isPending = pendingKind === q.kind;
390
+ return /* @__PURE__ */ jsxs3(
391
+ "button",
392
+ {
393
+ type: "button",
394
+ onClick: () => handleClick(q.kind),
395
+ disabled: !!pendingKind,
396
+ className: "inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-medium transition-opacity disabled:opacity-60",
397
+ style: {
398
+ background: isActive ? `${skin.accent}20` : "rgba(255,255,255,0.04)",
399
+ border: `1px solid ${isActive ? `${skin.accent}55` : "rgba(255,255,255,0.08)"}`,
400
+ color: isActive ? skin.accent : skin.softText
401
+ },
402
+ children: [
403
+ isPending ? /* @__PURE__ */ jsx3(Loader22, { size: 10, className: "animate-spin" }) : /* @__PURE__ */ jsx3(Sparkles3, { size: 10 }),
404
+ q.label
405
+ ]
406
+ },
407
+ q.kind
408
+ );
409
+ }) }),
410
+ activeKind && (pendingKind || answer || error) && /* @__PURE__ */ jsxs3(
411
+ "div",
412
+ {
413
+ className: "rounded-lg px-2.5 py-2",
414
+ style: {
415
+ background: `${skin.accent}0d`,
416
+ border: `1px solid ${skin.accent}26`
417
+ },
418
+ children: [
419
+ pendingKind && /* @__PURE__ */ jsxs3(
420
+ "div",
421
+ {
422
+ className: "flex items-center gap-1.5 text-[10px]",
423
+ style: { color: skin.mutedText },
424
+ children: [
425
+ /* @__PURE__ */ jsx3(Loader22, { size: 11, className: "animate-spin" }),
426
+ "Opi is checking\u2026"
427
+ ]
428
+ }
429
+ ),
430
+ !pendingKind && error && /* @__PURE__ */ jsxs3(
431
+ "div",
432
+ {
433
+ className: "flex items-center gap-1.5 text-[10px]",
434
+ style: { color: "#ef4444" },
435
+ children: [
436
+ /* @__PURE__ */ jsx3(XCircle, { size: 11 }),
437
+ error
438
+ ]
439
+ }
440
+ ),
441
+ !pendingKind && !error && answer && /* @__PURE__ */ jsxs3("div", { className: "space-y-1", children: [
442
+ /* @__PURE__ */ jsx3(
443
+ "p",
444
+ {
445
+ className: "text-[11px] leading-relaxed font-medium",
446
+ style: { color: skin.bodyText },
447
+ children: answer.headline
448
+ }
449
+ ),
450
+ answer.detail && /* @__PURE__ */ jsx3(
451
+ "p",
452
+ {
453
+ className: "text-[10px] leading-relaxed",
454
+ style: { color: skin.softText },
455
+ children: answer.detail
456
+ }
457
+ )
458
+ ] })
459
+ ]
460
+ }
461
+ )
462
+ ] });
463
+ }
348
464
  export {
349
465
  OpiAskBar,
350
466
  OpiBadgeChip,
467
+ OpiGuidedQuestions,
351
468
  OpiInsightsShell,
352
469
  OpiScoreDial,
353
470
  OpiSectionHeading,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/insights/OpiInsightsShell.tsx","../../src/insights/OpiAskBar.tsx"],"sourcesContent":["/**\n * OpiInsightsShell — the standard collapsible \"Opi Insights\" AI coaching\n * sidebar chrome, shared by every Oplytics subdomain service.\n *\n * This is deliberately shell-only: it owns the collapsible rail, header,\n * \"Powered by AI\" badge, and the small set of visual primitives every\n * service's AI panel tends to need (score dial, severity styling, section\n * heading, footer note). Each service supplies its OWN domain analysis and\n * renders its own content as children — same pattern as reportingToolbar\n * and hierarchyNavigator. Policy Deployment is the design source (see\n * OpiInsightsPanel there for the first real consumer) and the reference\n * for what a service-specific content tree looks like inside this shell.\n *\n * Services with their own dark-theme skin tokens (panel background, border,\n * text colors) should pass them via `skin` so the panel matches their other\n * views; omit it to fall back to the shared oplytics-theme teal/dark tokens.\n */\nimport { useState, type ReactNode } from \"react\";\nimport { Sparkles, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { OPLYTICS_COLORS } from \"../theme\";\n\nexport type InsightSeverity = \"good\" | \"watch\" | \"gap\";\n\nexport interface OpiInsightsSkin {\n accent: string;\n panelBg: string;\n panelBorder: string;\n mutedText: string;\n bodyText: string;\n softText: string;\n}\n\nconst defaultSkin: OpiInsightsSkin = {\n accent: OPLYTICS_COLORS.accentTeal,\n panelBg: \"#0e1624\",\n panelBorder: \"1px solid #1e2738\",\n mutedText: \"#596475\",\n bodyText: \"#c0c6d0\",\n softText: \"#8890a0\",\n};\n\nexport function severityColor(severity: InsightSeverity): {\n text: string;\n bg: string;\n border: string;\n} {\n switch (severity) {\n case \"good\":\n return {\n text: \"#10b981\",\n bg: \"rgba(16,185,129,0.1)\",\n border: \"rgba(16,185,129,0.25)\",\n };\n case \"watch\":\n return {\n text: \"#f59e0b\",\n bg: \"rgba(245,158,11,0.1)\",\n border: \"rgba(245,158,11,0.25)\",\n };\n case \"gap\":\n return {\n text: \"#ef4444\",\n bg: \"rgba(239,68,68,0.1)\",\n border: \"rgba(239,68,68,0.25)\",\n };\n }\n}\n\n/** Small \"Powered by AI\" chip — the standard cross-service Opi AI marker. */\nexport function OpiBadgeChip({\n skin = defaultSkin,\n}: {\n skin?: OpiInsightsSkin;\n}) {\n return (\n <span\n className=\"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-medium uppercase tracking-wide\"\n style={{\n background: \"#0b0f1a\",\n color: skin.accent,\n border: `1px solid ${skin.accent}33`,\n }}\n >\n <span\n className=\"w-2.5 h-2.5 rounded-full\"\n style={{\n background: `radial-gradient(circle at 30% 30%, ${skin.accent}, #0d5a63 70%)`,\n }}\n />\n Powered by AI\n </span>\n );\n}\n\n/** Circular 0-100 score dial — green/amber/red by threshold, used for any composite score. */\nexport function OpiScoreDial({\n score,\n size = 56,\n}: {\n score: number;\n size?: number;\n}) {\n const radius = (size - 6) / 2;\n const circumference = 2 * Math.PI * radius;\n const offset = circumference - (score / 100) * circumference;\n const color = score >= 75 ? \"#10b981\" : score >= 45 ? \"#f59e0b\" : \"#ef4444\";\n return (\n <div className=\"relative shrink-0\" style={{ width: size, height: size }}>\n <svg width={size} height={size} className=\"-rotate-90\">\n <circle\n cx={size / 2}\n cy={size / 2}\n r={radius}\n fill=\"none\"\n stroke=\"rgba(255,255,255,0.06)\"\n strokeWidth={4}\n />\n <circle\n cx={size / 2}\n cy={size / 2}\n r={radius}\n fill=\"none\"\n stroke={color}\n strokeWidth={4}\n strokeLinecap=\"round\"\n strokeDasharray={circumference}\n strokeDashoffset={offset}\n style={{ transition: \"stroke-dashoffset 0.6s ease-out\" }}\n />\n </svg>\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"text-sm font-bold text-white\">{score}</span>\n </div>\n </div>\n );\n}\n\n/** Section label with an icon — used to group the panel's content into named sections. */\nexport function OpiSectionHeading({\n icon,\n title,\n skin = defaultSkin,\n}: {\n icon: ReactNode;\n title: string;\n skin?: OpiInsightsSkin;\n}) {\n return (\n <div className=\"flex items-center gap-2 mb-2.5\">\n <span style={{ color: skin.accent }}>{icon}</span>\n <h4\n className=\"text-[11px] font-bold uppercase tracking-wider\"\n style={{ color: skin.softText }}\n >\n {title}\n </h4>\n </div>\n );\n}\n\nexport interface OpiInsightsShellProps {\n /** Panel content — build with OpiSectionHeading/OpiScoreDial/severityColor etc. */\n children: ReactNode;\n /** Optional headline shown above the badge (e.g. a one-line AI summary). */\n headline?: ReactNode;\n /** Footer note — defaults to the standard \"mocked pending real engine\" preview notice. */\n footerNote?: ReactNode;\n /** Service-specific dark-theme tokens; falls back to the shared theme's teal/dark defaults. */\n skin?: OpiInsightsSkin;\n /** Panel title in the header. Defaults to \"Opi Insights\". */\n title?: string;\n /** Controlled open state. Omit to let the shell manage its own collapse state. */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n}\n\n/**\n * The collapsible teal Opi Insights rail. Renders nothing if collapsed\n * except a slim vertical tab; expanded, it's a fixed-width sticky sidebar\n * with a header, scrollable content area, and footer note.\n */\nexport function OpiInsightsShell({\n children,\n headline,\n footerNote = \"Preview build — coaching logic is mocked pending the real Opi engine.\",\n skin = defaultSkin,\n title = \"Opi Insights\",\n open: openProp,\n onOpenChange,\n}: OpiInsightsShellProps) {\n const [openState, setOpenState] = useState(true);\n const open = openProp ?? openState;\n const setOpen = (value: boolean) => {\n setOpenState(value);\n onOpenChange?.(value);\n };\n\n if (!open) {\n return (\n <button\n onClick={() => setOpen(true)}\n className=\"flex flex-col items-center gap-2 py-4 px-1.5 rounded-l-lg shrink-0 self-start sticky top-4 transition-colors hover:brightness-110\"\n style={{\n background: skin.panelBg,\n border: skin.panelBorder,\n borderRight: \"none\",\n }}\n title={`Open ${title}`}\n >\n <Sparkles size={14} style={{ color: skin.accent }} />\n <span\n className=\"text-[10px] font-semibold uppercase tracking-wider\"\n style={{ color: skin.accent, writingMode: \"vertical-rl\" }}\n >\n {title}\n </span>\n <ChevronLeft size={12} style={{ color: skin.mutedText }} />\n </button>\n );\n }\n\n return (\n <aside\n className=\"w-[300px] shrink-0 rounded-lg overflow-hidden self-start sticky top-4 max-h-[calc(100vh-6rem)] flex flex-col\"\n style={{ background: skin.panelBg, border: skin.panelBorder }}\n >\n <div\n className=\"flex items-center justify-between gap-2 px-4 py-3 shrink-0\"\n style={{\n background: \"linear-gradient(135deg, #0d5a63 0%, #0b3a40 100%)\",\n }}\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <div\n className=\"w-6 h-6 rounded-md flex items-center justify-center shrink-0\"\n style={{ background: `${skin.accent}26` }}\n >\n <Sparkles size={13} style={{ color: skin.accent }} />\n </div>\n <span className=\"text-xs font-bold text-white truncate\">{title}</span>\n </div>\n <button\n onClick={() => setOpen(false)}\n className=\"p-1 rounded hover:bg-white/10 transition-colors shrink-0\"\n title=\"Collapse\"\n >\n <ChevronRight size={14} className=\"text-white/70\" />\n </button>\n </div>\n\n <div className=\"overflow-y-auto px-4 py-3.5 space-y-5\">\n {headline && (\n <div>\n <p\n className=\"text-[12px] leading-relaxed mb-2\"\n style={{ color: skin.bodyText }}\n >\n {headline}\n </p>\n <OpiBadgeChip skin={skin} />\n </div>\n )}\n {children}\n </div>\n\n {footerNote && (\n <div\n className=\"px-4 py-2 text-[9px] text-center shrink-0\"\n style={{ color: skin.mutedText, borderTop: skin.panelBorder }}\n >\n {footerNote}\n </div>\n )}\n </aside>\n );\n}\n","/**\n * OpiAskBar — the follow-up chat input for the Opi Insights panel.\n *\n * The shell and every service's analysis content are read-only displays;\n * this is the one interactive surface where a user can ask Opi a question\n * about what's on screen. Transport-agnostic by design: each consuming\n * app has its own way of reaching the portal's `ai.askOpi` endpoint (same-\n * origin tRPC in the portal itself, a cross-origin client elsewhere), so\n * this component takes a plain `onAsk(question)` callback rather than\n * knowing about tRPC. Renders its own short message thread above the\n * input; the shell just places it as the last child.\n */\nimport { useState, type FormEvent } from \"react\";\nimport { Sparkles, Send, Loader2 } from \"lucide-react\";\nimport type { OpiInsightsSkin } from \"./OpiInsightsShell\";\n\ninterface AskMessage {\n role: \"user\" | \"assistant\";\n content: string;\n}\n\nexport interface OpiAskBarProps {\n /** Sends the question (with prior thread already appended by the caller if desired) and resolves with Opi's reply. */\n onAsk: (question: string) => Promise<string>;\n /** Placeholder for the input. Defaults to a generic prompt. */\n placeholder?: string;\n skin?: OpiInsightsSkin;\n}\n\nconst defaultSkin: OpiInsightsSkin = {\n accent: \"#14b8a6\",\n panelBg: \"#0e1624\",\n panelBorder: \"1px solid #1e2738\",\n mutedText: \"#596475\",\n bodyText: \"#c0c6d0\",\n softText: \"#8890a0\",\n};\n\nexport function OpiAskBar({\n onAsk,\n placeholder = \"Ask Opi about this…\",\n skin = defaultSkin,\n}: OpiAskBarProps) {\n const [thread, setThread] = useState<AskMessage[]>([]);\n const [input, setInput] = useState(\"\");\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n const question = input.trim();\n if (!question || pending) return;\n\n setThread(prev => [...prev, { role: \"user\", content: question }]);\n setInput(\"\");\n setPending(true);\n setError(null);\n\n try {\n const reply = await onAsk(question);\n setThread(prev => [...prev, { role: \"assistant\", content: reply }]);\n } catch {\n setError(\"Couldn't reach Opi just then — give it another go.\");\n } finally {\n setPending(false);\n }\n };\n\n return (\n <div\n className=\"pt-1 -mx-4 -mb-3.5 px-4 pb-3.5\"\n style={{ borderTop: skin.panelBorder }}\n >\n {thread.length > 0 && (\n <div className=\"space-y-2 py-3 max-h-64 overflow-y-auto\">\n {thread.map((m, i) => (\n <div\n key={i}\n className=\"text-[11px] leading-relaxed rounded-lg px-2.5 py-2\"\n style={\n m.role === \"user\"\n ? {\n background: \"rgba(255,255,255,0.04)\",\n color: skin.bodyText,\n }\n : {\n background: `${skin.accent}14`,\n color: skin.bodyText,\n border: `1px solid ${skin.accent}26`,\n }\n }\n >\n {m.role === \"assistant\" && (\n <div\n className=\"flex items-center gap-1 mb-1\"\n style={{ color: skin.accent }}\n >\n <Sparkles size={10} />\n <span className=\"text-[9px] font-bold uppercase tracking-wide\">\n Opi\n </span>\n </div>\n )}\n {m.content}\n </div>\n ))}\n {pending && (\n <div\n className=\"flex items-center gap-1.5 text-[10px]\"\n style={{ color: skin.mutedText }}\n >\n <Loader2 size={11} className=\"animate-spin\" />\n Thinking…\n </div>\n )}\n </div>\n )}\n\n {error && (\n <p className=\"text-[10px] pb-2\" style={{ color: \"#ef4444\" }}>\n {error}\n </p>\n )}\n\n <form onSubmit={handleSubmit} className=\"flex items-center gap-1.5 pt-2\">\n <input\n value={input}\n onChange={e => setInput(e.target.value)}\n placeholder={placeholder}\n disabled={pending}\n className=\"flex-1 min-w-0 rounded-md px-2.5 py-1.5 text-[11px] outline-none\"\n style={{\n background: \"#0b0f1a\",\n border: skin.panelBorder,\n color: skin.bodyText,\n }}\n />\n <button\n type=\"submit\"\n disabled={!input.trim() || pending}\n className=\"shrink-0 w-7 h-7 rounded-md flex items-center justify-center transition-opacity disabled:opacity-40\"\n style={{ background: `${skin.accent}26`, color: skin.accent }}\n title=\"Ask Opi\"\n >\n <Send size={12} />\n </button>\n </form>\n </div>\n );\n}\n"],"mappings":"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","names":["useState","Sparkles","jsx","jsxs","defaultSkin"]}
1
+ {"version":3,"sources":["../../src/insights/OpiInsightsShell.tsx","../../src/insights/OpiAskBar.tsx","../../src/insights/OpiGuidedQuestions.tsx"],"sourcesContent":["/**\n * OpiInsightsShell — the standard collapsible \"Opi Insights\" AI coaching\n * sidebar chrome, shared by every Oplytics subdomain service.\n *\n * This is deliberately shell-only: it owns the collapsible rail, header,\n * \"Powered by AI\" badge, and the small set of visual primitives every\n * service's AI panel tends to need (score dial, severity styling, section\n * heading, footer note). Each service supplies its OWN domain analysis and\n * renders its own content as children — same pattern as reportingToolbar\n * and hierarchyNavigator. Policy Deployment is the design source (see\n * OpiInsightsPanel there for the first real consumer) and the reference\n * for what a service-specific content tree looks like inside this shell.\n *\n * Services with their own dark-theme skin tokens (panel background, border,\n * text colors) should pass them via `skin` so the panel matches their other\n * views; omit it to fall back to the shared oplytics-theme teal/dark tokens.\n */\nimport { useState, type ReactNode } from \"react\";\nimport { Sparkles, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { OPLYTICS_COLORS } from \"../theme\";\n\nexport type InsightSeverity = \"good\" | \"watch\" | \"gap\";\n\nexport interface OpiInsightsSkin {\n accent: string;\n panelBg: string;\n panelBorder: string;\n mutedText: string;\n bodyText: string;\n softText: string;\n}\n\nconst defaultSkin: OpiInsightsSkin = {\n accent: OPLYTICS_COLORS.accentTeal,\n panelBg: \"#0e1624\",\n panelBorder: \"1px solid #1e2738\",\n mutedText: \"#596475\",\n bodyText: \"#c0c6d0\",\n softText: \"#8890a0\",\n};\n\nexport function severityColor(severity: InsightSeverity): {\n text: string;\n bg: string;\n border: string;\n} {\n switch (severity) {\n case \"good\":\n return {\n text: \"#10b981\",\n bg: \"rgba(16,185,129,0.1)\",\n border: \"rgba(16,185,129,0.25)\",\n };\n case \"watch\":\n return {\n text: \"#f59e0b\",\n bg: \"rgba(245,158,11,0.1)\",\n border: \"rgba(245,158,11,0.25)\",\n };\n case \"gap\":\n return {\n text: \"#ef4444\",\n bg: \"rgba(239,68,68,0.1)\",\n border: \"rgba(239,68,68,0.25)\",\n };\n }\n}\n\n/** Small \"Powered by AI\" chip — the standard cross-service Opi AI marker. */\nexport function OpiBadgeChip({\n skin = defaultSkin,\n}: {\n skin?: OpiInsightsSkin;\n}) {\n return (\n <span\n className=\"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-medium uppercase tracking-wide\"\n style={{\n background: \"#0b0f1a\",\n color: skin.accent,\n border: `1px solid ${skin.accent}33`,\n }}\n >\n <span\n className=\"w-2.5 h-2.5 rounded-full\"\n style={{\n background: `radial-gradient(circle at 30% 30%, ${skin.accent}, #0d5a63 70%)`,\n }}\n />\n Powered by AI\n </span>\n );\n}\n\n/** Circular 0-100 score dial — green/amber/red by threshold, used for any composite score. */\nexport function OpiScoreDial({\n score,\n size = 56,\n}: {\n score: number;\n size?: number;\n}) {\n const radius = (size - 6) / 2;\n const circumference = 2 * Math.PI * radius;\n const offset = circumference - (score / 100) * circumference;\n const color = score >= 75 ? \"#10b981\" : score >= 45 ? \"#f59e0b\" : \"#ef4444\";\n return (\n <div className=\"relative shrink-0\" style={{ width: size, height: size }}>\n <svg width={size} height={size} className=\"-rotate-90\">\n <circle\n cx={size / 2}\n cy={size / 2}\n r={radius}\n fill=\"none\"\n stroke=\"rgba(255,255,255,0.06)\"\n strokeWidth={4}\n />\n <circle\n cx={size / 2}\n cy={size / 2}\n r={radius}\n fill=\"none\"\n stroke={color}\n strokeWidth={4}\n strokeLinecap=\"round\"\n strokeDasharray={circumference}\n strokeDashoffset={offset}\n style={{ transition: \"stroke-dashoffset 0.6s ease-out\" }}\n />\n </svg>\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"text-sm font-bold text-white\">{score}</span>\n </div>\n </div>\n );\n}\n\n/** Section label with an icon — used to group the panel's content into named sections. */\nexport function OpiSectionHeading({\n icon,\n title,\n skin = defaultSkin,\n}: {\n icon: ReactNode;\n title: string;\n skin?: OpiInsightsSkin;\n}) {\n return (\n <div className=\"flex items-center gap-2 mb-2.5\">\n <span style={{ color: skin.accent }}>{icon}</span>\n <h4\n className=\"text-[11px] font-bold uppercase tracking-wider\"\n style={{ color: skin.softText }}\n >\n {title}\n </h4>\n </div>\n );\n}\n\nexport interface OpiInsightsShellProps {\n /** Panel content — build with OpiSectionHeading/OpiScoreDial/severityColor etc. */\n children: ReactNode;\n /** Optional headline shown above the badge (e.g. a one-line AI summary). */\n headline?: ReactNode;\n /** Footer note — defaults to the standard \"mocked pending real engine\" preview notice. */\n footerNote?: ReactNode;\n /** Service-specific dark-theme tokens; falls back to the shared theme's teal/dark defaults. */\n skin?: OpiInsightsSkin;\n /** Panel title in the header. Defaults to \"Opi Insights\". */\n title?: string;\n /** Controlled open state. Omit to let the shell manage its own collapse state. */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n}\n\n/**\n * The collapsible teal Opi Insights rail. Renders nothing if collapsed\n * except a slim vertical tab; expanded, it's a fixed-width sticky sidebar\n * with a header, scrollable content area, and footer note.\n */\nexport function OpiInsightsShell({\n children,\n headline,\n footerNote = \"Preview build — coaching logic is mocked pending the real Opi engine.\",\n skin = defaultSkin,\n title = \"Opi Insights\",\n open: openProp,\n onOpenChange,\n}: OpiInsightsShellProps) {\n const [openState, setOpenState] = useState(true);\n const open = openProp ?? openState;\n const setOpen = (value: boolean) => {\n setOpenState(value);\n onOpenChange?.(value);\n };\n\n if (!open) {\n return (\n <button\n onClick={() => setOpen(true)}\n className=\"flex flex-col items-center gap-2 py-4 px-1.5 rounded-l-lg shrink-0 self-start sticky top-4 transition-colors hover:brightness-110\"\n style={{\n background: skin.panelBg,\n border: skin.panelBorder,\n borderRight: \"none\",\n }}\n title={`Open ${title}`}\n >\n <Sparkles size={14} style={{ color: skin.accent }} />\n <span\n className=\"text-[10px] font-semibold uppercase tracking-wider\"\n style={{ color: skin.accent, writingMode: \"vertical-rl\" }}\n >\n {title}\n </span>\n <ChevronLeft size={12} style={{ color: skin.mutedText }} />\n </button>\n );\n }\n\n return (\n <aside\n className=\"w-[300px] shrink-0 rounded-lg overflow-hidden self-start sticky top-4 max-h-[calc(100vh-6rem)] flex flex-col\"\n style={{ background: skin.panelBg, border: skin.panelBorder }}\n >\n <div\n className=\"flex items-center justify-between gap-2 px-4 py-3 shrink-0\"\n style={{\n background: \"linear-gradient(135deg, #0d5a63 0%, #0b3a40 100%)\",\n }}\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <div\n className=\"w-6 h-6 rounded-md flex items-center justify-center shrink-0\"\n style={{ background: `${skin.accent}26` }}\n >\n <Sparkles size={13} style={{ color: skin.accent }} />\n </div>\n <span className=\"text-xs font-bold text-white truncate\">{title}</span>\n </div>\n <button\n onClick={() => setOpen(false)}\n className=\"p-1 rounded hover:bg-white/10 transition-colors shrink-0\"\n title=\"Collapse\"\n >\n <ChevronRight size={14} className=\"text-white/70\" />\n </button>\n </div>\n\n <div className=\"overflow-y-auto px-4 py-3.5 space-y-5\">\n {headline && (\n <div>\n <p\n className=\"text-[12px] leading-relaxed mb-2\"\n style={{ color: skin.bodyText }}\n >\n {headline}\n </p>\n <OpiBadgeChip skin={skin} />\n </div>\n )}\n {children}\n </div>\n\n {footerNote && (\n <div\n className=\"px-4 py-2 text-[9px] text-center shrink-0\"\n style={{ color: skin.mutedText, borderTop: skin.panelBorder }}\n >\n {footerNote}\n </div>\n )}\n </aside>\n );\n}\n","/**\n * OpiAskBar — the follow-up chat input for the Opi Insights panel.\n *\n * The shell and every service's analysis content are read-only displays;\n * this is the one interactive surface where a user can ask Opi a question\n * about what's on screen. Transport-agnostic by design: each consuming\n * app has its own way of reaching the portal's `ai.askOpi` endpoint (same-\n * origin tRPC in the portal itself, a cross-origin client elsewhere), so\n * this component takes a plain `onAsk(question)` callback rather than\n * knowing about tRPC. Renders its own short message thread above the\n * input; the shell just places it as the last child.\n */\nimport { useState, type FormEvent } from \"react\";\nimport { Sparkles, Send, Loader2 } from \"lucide-react\";\nimport type { OpiInsightsSkin } from \"./OpiInsightsShell\";\n\ninterface AskMessage {\n role: \"user\" | \"assistant\";\n content: string;\n}\n\nexport interface OpiAskBarProps {\n /** Sends the question (with prior thread already appended by the caller if desired) and resolves with Opi's reply. */\n onAsk: (question: string) => Promise<string>;\n /** Placeholder for the input. Defaults to a generic prompt. */\n placeholder?: string;\n skin?: OpiInsightsSkin;\n}\n\nconst defaultSkin: OpiInsightsSkin = {\n accent: \"#14b8a6\",\n panelBg: \"#0e1624\",\n panelBorder: \"1px solid #1e2738\",\n mutedText: \"#596475\",\n bodyText: \"#c0c6d0\",\n softText: \"#8890a0\",\n};\n\nexport function OpiAskBar({\n onAsk,\n placeholder = \"Ask Opi about this…\",\n skin = defaultSkin,\n}: OpiAskBarProps) {\n const [thread, setThread] = useState<AskMessage[]>([]);\n const [input, setInput] = useState(\"\");\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n const question = input.trim();\n if (!question || pending) return;\n\n setThread(prev => [...prev, { role: \"user\", content: question }]);\n setInput(\"\");\n setPending(true);\n setError(null);\n\n try {\n const reply = await onAsk(question);\n setThread(prev => [...prev, { role: \"assistant\", content: reply }]);\n } catch {\n setError(\"Couldn't reach Opi just then — give it another go.\");\n } finally {\n setPending(false);\n }\n };\n\n return (\n <div\n className=\"pt-1 -mx-4 -mb-3.5 px-4 pb-3.5\"\n style={{ borderTop: skin.panelBorder }}\n >\n {thread.length > 0 && (\n <div className=\"space-y-2 py-3 max-h-64 overflow-y-auto\">\n {thread.map((m, i) => (\n <div\n key={i}\n className=\"text-[11px] leading-relaxed rounded-lg px-2.5 py-2\"\n style={\n m.role === \"user\"\n ? {\n background: \"rgba(255,255,255,0.04)\",\n color: skin.bodyText,\n }\n : {\n background: `${skin.accent}14`,\n color: skin.bodyText,\n border: `1px solid ${skin.accent}26`,\n }\n }\n >\n {m.role === \"assistant\" && (\n <div\n className=\"flex items-center gap-1 mb-1\"\n style={{ color: skin.accent }}\n >\n <Sparkles size={10} />\n <span className=\"text-[9px] font-bold uppercase tracking-wide\">\n Opi\n </span>\n </div>\n )}\n {m.content}\n </div>\n ))}\n {pending && (\n <div\n className=\"flex items-center gap-1.5 text-[10px]\"\n style={{ color: skin.mutedText }}\n >\n <Loader2 size={11} className=\"animate-spin\" />\n Thinking…\n </div>\n )}\n </div>\n )}\n\n {error && (\n <p className=\"text-[10px] pb-2\" style={{ color: \"#ef4444\" }}>\n {error}\n </p>\n )}\n\n <form onSubmit={handleSubmit} className=\"flex items-center gap-1.5 pt-2\">\n <input\n value={input}\n onChange={e => setInput(e.target.value)}\n placeholder={placeholder}\n disabled={pending}\n className=\"flex-1 min-w-0 rounded-md px-2.5 py-1.5 text-[11px] outline-none\"\n style={{\n background: \"#0b0f1a\",\n border: skin.panelBorder,\n color: skin.bodyText,\n }}\n />\n <button\n type=\"submit\"\n disabled={!input.trim() || pending}\n className=\"shrink-0 w-7 h-7 rounded-md flex items-center justify-center transition-opacity disabled:opacity-40\"\n style={{ background: `${skin.accent}26`, color: skin.accent }}\n title=\"Ask Opi\"\n >\n <Send size={12} />\n </button>\n </form>\n </div>\n );\n}\n","/**\n * OpiGuidedQuestions — the preset-question chip row for the Opi Insights\n * panel, sitting between the findings sections and OpiAskBar's free-text\n * follow-up chat. Lower-risk sibling to the open-ended ask bar: each chip\n * maps to a question the consuming app answers from real, deterministic\n * computation (see e.g. sqdcp/server/guidedQuestions.ts) — the LLM, if one\n * is involved at all, only narrates already-computed facts, never invents\n * them. Transport-agnostic like OpiAskBar: takes a plain `onAsk(kind)`\n * callback rather than knowing about tRPC, so each consuming app wires it\n * to its own server calls.\n */\nimport { useState } from \"react\";\nimport { Sparkles, Loader2, XCircle } from \"lucide-react\";\nimport type { OpiInsightsSkin } from \"./OpiInsightsShell\";\n\nexport interface GuidedQuestionOption {\n /** Opaque to this component — the consuming app's own question-kind enum. */\n kind: string;\n /** Chip label, e.g. \"Worst pillar this period\". */\n label: string;\n}\n\nexport interface GuidedAnswerView {\n headline: string;\n detail?: string;\n}\n\nexport interface OpiGuidedQuestionsProps {\n questions: GuidedQuestionOption[];\n /** Resolves with the phrased answer for the clicked question's kind. */\n onAsk: (kind: string) => Promise<GuidedAnswerView>;\n skin?: OpiInsightsSkin;\n}\n\nconst defaultSkin: OpiInsightsSkin = {\n accent: \"#14b8a6\",\n panelBg: \"#0e1624\",\n panelBorder: \"1px solid #1e2738\",\n mutedText: \"#596475\",\n bodyText: \"#c0c6d0\",\n softText: \"#8890a0\",\n};\n\nexport function OpiGuidedQuestions({\n questions,\n onAsk,\n skin = defaultSkin,\n}: OpiGuidedQuestionsProps) {\n const [activeKind, setActiveKind] = useState<string | null>(null);\n const [pendingKind, setPendingKind] = useState<string | null>(null);\n const [answer, setAnswer] = useState<GuidedAnswerView | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n if (questions.length === 0) return null;\n\n const handleClick = async (kind: string) => {\n if (pendingKind) return;\n setActiveKind(kind);\n setPendingKind(kind);\n setAnswer(null);\n setError(null);\n\n try {\n const result = await onAsk(kind);\n setAnswer(result);\n } catch {\n setError(\"Couldn't reach Opi just then — give it another go.\");\n } finally {\n setPendingKind(null);\n }\n };\n\n return (\n <div className=\"space-y-2 pb-1\">\n <div className=\"flex flex-wrap gap-1.5\">\n {questions.map(q => {\n const isActive = activeKind === q.kind;\n const isPending = pendingKind === q.kind;\n return (\n <button\n key={q.kind}\n type=\"button\"\n onClick={() => handleClick(q.kind)}\n disabled={!!pendingKind}\n className=\"inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-medium transition-opacity disabled:opacity-60\"\n style={{\n background: isActive\n ? `${skin.accent}20`\n : \"rgba(255,255,255,0.04)\",\n border: `1px solid ${isActive ? `${skin.accent}55` : \"rgba(255,255,255,0.08)\"}`,\n color: isActive ? skin.accent : skin.softText,\n }}\n >\n {isPending ? (\n <Loader2 size={10} className=\"animate-spin\" />\n ) : (\n <Sparkles size={10} />\n )}\n {q.label}\n </button>\n );\n })}\n </div>\n\n {activeKind && (pendingKind || answer || error) && (\n <div\n className=\"rounded-lg px-2.5 py-2\"\n style={{\n background: `${skin.accent}0d`,\n border: `1px solid ${skin.accent}26`,\n }}\n >\n {pendingKind && (\n <div\n className=\"flex items-center gap-1.5 text-[10px]\"\n style={{ color: skin.mutedText }}\n >\n <Loader2 size={11} className=\"animate-spin\" />\n Opi is checking…\n </div>\n )}\n {!pendingKind && error && (\n <div\n className=\"flex items-center gap-1.5 text-[10px]\"\n style={{ color: \"#ef4444\" }}\n >\n <XCircle size={11} />\n {error}\n </div>\n )}\n {!pendingKind && !error && answer && (\n <div className=\"space-y-1\">\n <p\n className=\"text-[11px] leading-relaxed font-medium\"\n style={{ color: skin.bodyText }}\n >\n {answer.headline}\n </p>\n {answer.detail && (\n <p\n className=\"text-[10px] leading-relaxed\"\n style={{ color: skin.softText }}\n >\n {answer.detail}\n </p>\n )}\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["useState","Sparkles","jsx","jsxs","defaultSkin","useState","Sparkles","Loader2","jsx","jsxs","defaultSkin"]}
@@ -14,9 +14,9 @@ interface ReportingToolbarProps extends Omit<ReportingToolbarConfig, "moduleName
14
14
  /** Show the "attach screenshot" control (default: true). */
15
15
  enableScreenshot?: boolean;
16
16
  /**
17
- * While true (the DEFAULT), the Report items are disabled with a "Coming
18
- * soon" badge and `onReportSubmit` is never called. A later release wires the
19
- * per-type forms and passes `false`.
17
+ * While true, the Report items are disabled with a "Coming soon" badge and
18
+ * `onReportSubmit` is never called. Defaults to `false` — pass `true` only
19
+ * if this host's `onReportSubmit` doesn't actually persist the report yet.
20
20
  */
21
21
  reportsComingSoon?: boolean;
22
22
  /**
@@ -34,16 +34,19 @@ declare function ReportingToolbar({ moduleName, showIncidentReporting, showFeedb
34
34
  * Reporting and feedback toolbar types + configuration for all Oplytics
35
35
  * subdomains — the header "User Actions" dropdown (ReportingToolbar).
36
36
  *
37
- * Feedback (Report Problem / Suggestion) is fully wired: the dialog collects a
38
- * service, an "is this blocking you?" signal, a short summary, the detail, and
39
- * an optional screenshot, then calls `onFeedbackSubmit` with the canonical
40
- * `FeedbackReport` shape. The host owns the transport (it POSTs to Portal's
41
- * `/api/service/feedback`).
37
+ * Both groups are fully wired. Feedback collects a service, an "is this
38
+ * blocking you?" signal, a short summary, the detail, and an optional
39
+ * screenshot, then calls `onFeedbackSubmit` with the canonical
40
+ * `FeedbackReport` shape. Report (Safety / Environmental / Stop Work) is
41
+ * three bespoke forms, one per category, each collecting the fields needed
42
+ * to conform to the external standard it's built against (see each
43
+ * category's comment below), then calls `onReportSubmit` with the
44
+ * discriminated `IncidentReport` union. Either way the host owns the
45
+ * transport (Feedback POSTs to Portal's `/api/service/feedback`; Report
46
+ * POSTs to Portal's `/api/service/reports`).
42
47
  *
43
- * Report (Safety / Environmental / Stop Work) is not wired yet — while
44
- * `reportsComingSoon` is true (the default), those items render disabled with a
45
- * "Coming soon" badge. A later release adds the per-type forms and flips the
46
- * default.
48
+ * `reportsComingSoon` defaults to `false` as of this version — pass `true`
49
+ * per-host only if that host's `onReportSubmit` doesn't actually persist yet.
47
50
  */
48
51
  type ReportCategory = "safety" | "environmental" | "stop-work";
49
52
  type FeedbackCategory = "problem" | "suggestion";
@@ -67,11 +70,27 @@ interface ReportScreenshot {
67
70
  dataBase64: string;
68
71
  mime: string;
69
72
  }
70
- interface IncidentReport {
71
- category: ReportCategory;
73
+ type IncidentSeverity = "low" | "medium" | "high" | "critical";
74
+ /**
75
+ * OSHA 29 CFR 1904's own injury-and-illness recordkeeping classification —
76
+ * the standard the Safety form's injury fields conform to, alongside
77
+ * ISO 45001 (the international OH&S management-system standard it sits
78
+ * under). Shown to the user on the form, not just used internally.
79
+ */
80
+ type OshaInjuryClassification = "none" | "first_aid_only" | "recordable" | "days_away_restricted_transfer" | "fatality";
81
+ /** ISO 14001's "environmental aspect" concept — which element of the
82
+ * environment an incident affects. The standard the Environmental form's
83
+ * classification field conforms to. */
84
+ type EnvironmentalAspect = "air_emission" | "water_discharge" | "soil_contamination" | "waste" | "spill" | "noise" | "other";
85
+ /** ISO 45001's hierarchy-of-controls hazard classification. Stop Work
86
+ * authority isn't itself a numbered standard — it's a control mechanism
87
+ * exercised under an ISO 45001 OH&S management system, which this
88
+ * classification maps to. */
89
+ type HazardCategory = "mechanical" | "electrical" | "chemical" | "working_at_height" | "confined_space" | "vehicle_traffic" | "other";
90
+ interface IncidentReportBase {
72
91
  description: string;
73
92
  location?: string;
74
- severity?: "low" | "medium" | "high" | "critical";
93
+ severity?: IncidentSeverity;
75
94
  /** Optional — what was done at the scene to make the area safe. */
76
95
  immediateAction?: string;
77
96
  moduleName: string;
@@ -80,6 +99,44 @@ interface IncidentReport {
80
99
  submittedBy?: ReportSubmitter;
81
100
  hierarchyContext?: ReportHierarchyContext;
82
101
  }
102
+ interface SafetyIncidentReport extends IncidentReportBase {
103
+ category: "safety";
104
+ injuryOccurred: boolean;
105
+ /** OSHA 1904 classification — see `OshaInjuryClassification`. */
106
+ injuryClassification?: OshaInjuryClassification;
107
+ /** OSHA DART-rate inputs — only meaningful when injuryClassification is
108
+ * "days_away_restricted_transfer". */
109
+ daysAwayFromWork?: number;
110
+ daysRestrictedOrTransferred?: number;
111
+ firstAiderAttended?: boolean;
112
+ witnessName?: string;
113
+ bodyPart?: string;
114
+ treatmentType?: string;
115
+ }
116
+ interface EnvironmentalIncidentReport extends IncidentReportBase {
117
+ category: "environmental";
118
+ /** ISO 14001 environmental aspect — see `EnvironmentalAspect`. */
119
+ environmentalAspect?: EnvironmentalAspect;
120
+ /** Does this trigger a duty to notify an external regulator? */
121
+ regulatoryReportable?: boolean;
122
+ substance?: string;
123
+ estimatedQuantity?: string;
124
+ contained?: boolean;
125
+ reachedWatercourse?: boolean;
126
+ }
127
+ interface StopWorkIncidentReport extends IncidentReportBase {
128
+ category: "stop-work";
129
+ /** ISO 45001 hazard classification — see `HazardCategory`. */
130
+ hazardCategory?: HazardCategory;
131
+ hazardDescription?: string;
132
+ areaMadeSafe?: boolean;
133
+ workResumed?: boolean;
134
+ }
135
+ /** Discriminated on `category` — narrow on it to get the right per-type
136
+ * fields (`report.category === "safety"` narrows to `SafetyIncidentReport`,
137
+ * etc.). Each variant maps 1:1 onto its own bespoke form; see
138
+ * ReportingToolbar.tsx's SafetyForm/EnvironmentalForm/StopWorkForm. */
139
+ type IncidentReport = SafetyIncidentReport | EnvironmentalIncidentReport | StopWorkIncidentReport;
83
140
  interface FeedbackReport {
84
141
  category: FeedbackCategory;
85
142
  /** Canonical service slug the feedback is about (from the picker). */
@@ -120,7 +177,8 @@ interface ReportingToolbarConfig {
120
177
  reportCategories?: ReportCategory[];
121
178
  /** Feedback categories to show (default: both). */
122
179
  feedbackCategories?: FeedbackCategory[];
123
- /** Called when an incident report is submitted (not wired yet). */
180
+ /** Called when an incident report is submitted. The host is responsible
181
+ * for actually persisting it — see `reportsComingSoon` above. */
124
182
  onReportSubmit: (report: IncidentReport) => Promise<void> | void;
125
183
  /** Called when feedback is submitted — the host POSTs it to Portal. */
126
184
  onFeedbackSubmit: (feedback: FeedbackReport) => Promise<void> | void;
@@ -141,6 +199,14 @@ interface FeedbackButtonDef {
141
199
  }
142
200
  declare const REPORT_BUTTONS: ReportButtonDef[];
143
201
  declare const FEEDBACK_BUTTONS: FeedbackButtonDef[];
202
+ interface ReportStandardReference {
203
+ category: ReportCategory;
204
+ /** Short label shown on the form, e.g. "OSHA 1904 · ISO 45001". */
205
+ label: string;
206
+ /** One sentence explaining what the standard governs here. */
207
+ description: string;
208
+ }
209
+ declare const REPORT_STANDARD_REFERENCES: Record<ReportCategory, ReportStandardReference>;
144
210
  /** Create a default ReportingToolbarConfig with sensible defaults. */
145
211
  declare function createReportingConfig(moduleName: string, handlers: {
146
212
  onReportSubmit: (report: IncidentReport) => Promise<void> | void;
@@ -163,4 +229,4 @@ declare function isAcceptableScreenshot(file: {
163
229
  };
164
230
  declare function readFileAsScreenshot(file: File): Promise<ReportScreenshot>;
165
231
 
166
- export { FEEDBACK_BUTTONS, FEEDBACK_SCREENSHOT_MAX_BYTES, type FeedbackButtonDef, type FeedbackCategory, type FeedbackReport, type IncidentReport, OPLYTICS_SERVICES, type OplyticsService, REPORT_BUTTONS, type ReportButtonDef, type ReportCategory, type ReportHierarchyContext, type ReportScreenshot, type ReportSubmitter, ReportingToolbar, type ReportingToolbarConfig, type ReportingToolbarProps, createReportingConfig, isAcceptableScreenshot, readFileAsScreenshot };
232
+ export { type EnvironmentalAspect, type EnvironmentalIncidentReport, FEEDBACK_BUTTONS, FEEDBACK_SCREENSHOT_MAX_BYTES, type FeedbackButtonDef, type FeedbackCategory, type FeedbackReport, type HazardCategory, type IncidentReport, type IncidentSeverity, OPLYTICS_SERVICES, type OplyticsService, type OshaInjuryClassification, REPORT_BUTTONS, REPORT_STANDARD_REFERENCES, type ReportButtonDef, type ReportCategory, type ReportHierarchyContext, type ReportScreenshot, type ReportStandardReference, type ReportSubmitter, ReportingToolbar, type ReportingToolbarConfig, type ReportingToolbarProps, type SafetyIncidentReport, type StopWorkIncidentReport, createReportingConfig, isAcceptableScreenshot, readFileAsScreenshot };