@fayz-ai/plugin-scribe 0.11.2 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import * as React7 from 'react';
2
- import React7__default from 'react';
1
+ import * as React5 from 'react';
2
+ import React5__default from 'react';
3
3
  import { getActiveTenantId, getSupabaseClientOptional, registerTranslations } from '@fayz-ai/core';
4
4
  import { create } from 'zustand';
5
5
  import { createPortal } from 'react-dom';
6
- import { useAgentSurface, registerTimelineSource, useAuth, useRightRailStore, useAccessOptional, openAssistant, usePublishAssistantActivity, useRegisterAssistantLauncher, resolveFayzAgentConnection } from '@fayz-ai/admin';
6
+ import { Mic, FileText, Sparkles, Loader2, Pause, Play, Square, Settings2, AlertTriangle, RefreshCw, Trash2, Check } from 'lucide-react';
7
7
  import { Button, toast, Skeleton, Badge, Tabs, TabsList, TabsTrigger, TabsContent, Card, CardContent, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Checkbox, ModalFooter, MarkdownEditor } from '@fayz-ai/ui/primitives';
8
- import { Mic, FileText, Sparkles, Loader2, Pause, Play, Square, Settings2, AlertTriangle, RefreshCw, Trash2, Check, ShieldCheck } from 'lucide-react';
9
8
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
+ import { openAssistantAs, useAgentSurface, registerTimelineSource, useAccessOptional, useHeaderAction, useAuth, usePublishAssistantActivity, useRegisterAssistantLauncher, resolveFayzAgentConnection } from '@fayz-ai/admin';
10
10
  import { SubpageHeader } from '@fayz-ai/ui/layout';
11
11
 
12
12
  // src/index.ts
@@ -1083,10 +1083,10 @@ function formatElapsed(ms) {
1083
1083
  }
1084
1084
  var MOBILE_BREAKPOINT = 768;
1085
1085
  function useIsMobile() {
1086
- const [isMobile, setIsMobile] = React7.useState(
1086
+ const [isMobile, setIsMobile] = React5.useState(
1087
1087
  () => typeof window !== "undefined" && window.innerWidth < MOBILE_BREAKPOINT
1088
1088
  );
1089
- React7.useEffect(() => {
1089
+ React5.useEffect(() => {
1090
1090
  const mq = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
1091
1091
  const onChange = (e) => setIsMobile(e.matches);
1092
1092
  setIsMobile(mq.matches);
@@ -1096,8 +1096,8 @@ function useIsMobile() {
1096
1096
  return isMobile;
1097
1097
  }
1098
1098
  function useTicker(active) {
1099
- const [, force] = React7.useReducer((n) => n + 1, 0);
1100
- React7.useEffect(() => {
1099
+ const [, force] = React5.useReducer((n) => n + 1, 0);
1100
+ React5.useEffect(() => {
1101
1101
  if (!active) return;
1102
1102
  let raf = 0;
1103
1103
  let last = 0;
@@ -1121,13 +1121,14 @@ function useTicker(active) {
1121
1121
  function ScribeRecordingPill({ labels, onOpenSession, mobileOnly }) {
1122
1122
  const state = useScribeStore((s) => s.state);
1123
1123
  const sessionId = useScribeStore((s) => s.sessionId);
1124
+ const subjectId = useScribeStore((s) => s.subjectId);
1124
1125
  const subjectName = useScribeStore((s) => s.subjectName);
1125
1126
  const isMobile = useIsMobile();
1126
- const [busy, setBusy] = React7.useState(false);
1127
+ const [busy, setBusy] = React5.useState(false);
1127
1128
  const active = state === "recording" || state === "paused" || state === "interrupted";
1128
1129
  useTicker(state === "recording");
1129
1130
  const elapsedMs2 = active ? getSnapshot().elapsedMs : 0;
1130
- const handleToggle = React7.useCallback(async () => {
1131
+ const handleToggle = React5.useCallback(async () => {
1131
1132
  setBusy(true);
1132
1133
  try {
1133
1134
  if (state === "recording") await pauseSession();
@@ -1136,15 +1137,15 @@ function ScribeRecordingPill({ labels, onOpenSession, mobileOnly }) {
1136
1137
  setBusy(false);
1137
1138
  }
1138
1139
  }, [state]);
1139
- const handleStop = React7.useCallback(async () => {
1140
+ const handleStop = React5.useCallback(async () => {
1140
1141
  setBusy(true);
1141
1142
  try {
1142
1143
  const id = await endSession();
1143
- if (id) onOpenSession?.(id);
1144
+ if (id) onOpenSession?.(id, subjectId);
1144
1145
  } finally {
1145
1146
  setBusy(false);
1146
1147
  }
1147
- }, [onOpenSession]);
1148
+ }, [onOpenSession, subjectId]);
1148
1149
  if (!active || !sessionId) return null;
1149
1150
  if (mobileOnly && !isMobile) return null;
1150
1151
  const tone = state === "recording" ? "bg-destructive/10 text-destructive border-destructive/30" : state === "interrupted" ? "bg-amber-500/10 text-amber-700 border-amber-500/30 dark:text-amber-400" : "bg-muted text-muted-foreground border-border";
@@ -1160,7 +1161,7 @@ function ScribeRecordingPill({ labels, onOpenSession, mobileOnly }) {
1160
1161
  "button",
1161
1162
  {
1162
1163
  type: "button",
1163
- onClick: () => onOpenSession?.(sessionId),
1164
+ onClick: () => onOpenSession?.(sessionId, subjectId),
1164
1165
  className: "flex items-center gap-1.5 text-sm font-medium tabular-nums",
1165
1166
  title: subjectName ?? labels.sessionSingular,
1166
1167
  children: [
@@ -1221,112 +1222,6 @@ function ScribeStartButton({ labels, disabled, onStart, variant = "default", siz
1221
1222
  labels.start
1222
1223
  ] });
1223
1224
  }
1224
- var RESUMABLE_WINDOW_MS = 30 * 60 * 1e3;
1225
- function ScribeRecoveryBanner({ labels, onOpenSession }) {
1226
- const orphans2 = useScribeStore((s) => s.orphans);
1227
- const state = useScribeStore((s) => s.state);
1228
- const [busy, setBusy] = React7.useState(null);
1229
- const live2 = state === "recording" || state === "paused" || state === "requesting";
1230
- if (live2) return null;
1231
- if (orphans2.length === 0) return null;
1232
- const session = orphans2[0];
1233
- const interruptedAgoMs = Date.now() - session.lastTickEpochMs;
1234
- const canResume = interruptedAgoMs < RESUMABLE_WINDOW_MS;
1235
- const durationLabel = formatElapsed(orphanDurationMs(session));
1236
- const run = async (fn) => {
1237
- setBusy(session.id);
1238
- try {
1239
- await fn();
1240
- } finally {
1241
- setBusy(null);
1242
- }
1243
- };
1244
- return /* @__PURE__ */ jsxs("div", { className: "mx-3 mb-3 flex flex-col gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3 md:mx-6 md:flex-row md:items-center md:justify-between", children: [
1245
- /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
1246
- /* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" }),
1247
- /* @__PURE__ */ jsxs("div", { className: "text-sm", children: [
1248
- /* @__PURE__ */ jsxs("p", { className: "font-medium", children: [
1249
- labels.sessionSingular,
1250
- " de ",
1251
- durationLabel,
1252
- " foi interrompido",
1253
- session.subjectName ? ` \u2014 ${session.subjectName}` : "",
1254
- "."
1255
- ] }),
1256
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: "O \xE1udio j\xE1 gravado est\xE1 seguro e continua sendo enviado." })
1257
- ] })
1258
- ] }),
1259
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap gap-1.5", children: [
1260
- canResume && /* @__PURE__ */ jsx(Button, { size: "sm", disabled: busy === session.id, onClick: () => void run(resumeSession), children: labels.resume }),
1261
- /* @__PURE__ */ jsx(
1262
- Button,
1263
- {
1264
- size: "sm",
1265
- variant: "outline",
1266
- disabled: busy === session.id,
1267
- onClick: () => void run(async () => {
1268
- const id = await endSession();
1269
- dismissOrphan(session.id);
1270
- if (id) onOpenSession?.(id);
1271
- }),
1272
- children: labels.finish
1273
- }
1274
- ),
1275
- /* @__PURE__ */ jsx(
1276
- Button,
1277
- {
1278
- size: "sm",
1279
- variant: "ghost",
1280
- disabled: busy === session.id,
1281
- onClick: () => void run(() => discardSession(session.id)),
1282
- children: labels.discard
1283
- }
1284
- )
1285
- ] })
1286
- ] });
1287
- }
1288
- function ScribeConsentDialog({
1289
- open,
1290
- labels,
1291
- subjectName,
1292
- defaultConsentMode,
1293
- onCancel,
1294
- onConfirm
1295
- }) {
1296
- const [acknowledged, setAcknowledged] = React7.useState(false);
1297
- React7.useEffect(() => {
1298
- if (open) setAcknowledged(false);
1299
- }, [open]);
1300
- return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (next) => !next && onCancel(), children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
1301
- /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsxs(ModalTitle, { className: "flex items-center gap-2", children: [
1302
- /* @__PURE__ */ jsx(Mic, { className: "h-4 w-4" }),
1303
- "Iniciar grava\xE7\xE3o"
1304
- ] }) }),
1305
- /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-3 text-sm", children: [
1306
- /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground", children: [
1307
- "O \xE1udio ",
1308
- subjectName ? `de ${subjectName}` : `deste ${labels.sessionSingular.toLowerCase()}`,
1309
- " ser\xE1 gravado e transcrito para gerar um documento. A grava\xE7\xE3o fica vis\xEDvel na barra superior enquanto durar."
1310
- ] }),
1311
- /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-start gap-2 rounded-md border p-3", children: [
1312
- /* @__PURE__ */ jsx(Checkbox, { checked: acknowledged, onChange: setAcknowledged, className: "mt-0.5" }),
1313
- /* @__PURE__ */ jsxs("span", { children: [
1314
- "Informei ",
1315
- subjectName ?? "o titular",
1316
- " de que este atendimento ser\xE1 gravado e obtive o consentimento."
1317
- ] })
1318
- ] }),
1319
- /* @__PURE__ */ jsxs("p", { className: "flex items-start gap-2 text-xs text-muted-foreground", children: [
1320
- /* @__PURE__ */ jsx(ShieldCheck, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
1321
- "O \xE1udio \xE9 armazenado de forma privada e pode ser apagado a qualquer momento \u2014 a transcri\xE7\xE3o e o documento assinado permanecem."
1322
- ] })
1323
- ] }),
1324
- /* @__PURE__ */ jsxs(ModalFooter, { children: [
1325
- /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: onCancel, children: "Cancelar" }),
1326
- /* @__PURE__ */ jsx(Button, { size: "sm", disabled: !acknowledged, onClick: () => onConfirm(defaultConsentMode), children: labels.start })
1327
- ] })
1328
- ] }) });
1329
- }
1330
1225
  function client() {
1331
1226
  const supabase = getSupabaseClientOptional();
1332
1227
  if (!supabase) throw new Error("Supabase n\xE3o inicializado");
@@ -1866,11 +1761,24 @@ function watchTranscript(sessionId, stt) {
1866
1761
  function transcriptText() {
1867
1762
  return useLiveTranscript.getState().segments.map((s) => s.gap ? "[trecho perdido]" : (s.text ?? "").trim()).filter(Boolean).join(" ");
1868
1763
  }
1764
+ var SCRIBE_START_EVENT = "scribe:start";
1765
+ var SCRIBE_AGENT_ROLE = "encounter";
1766
+ function requestScribeStart(detail) {
1767
+ window.dispatchEvent(new CustomEvent(SCRIBE_START_EVENT, { detail }));
1768
+ }
1769
+ function scribeSessionPath(args) {
1770
+ const { sessionId, subjectId, subjectEntity } = args;
1771
+ return subjectId ? `/${subjectEntity}/${subjectId}/scribe/${sessionId}` : `/scribe/${sessionId}`;
1772
+ }
1773
+ function startScribeSession(detail = {}) {
1774
+ if (!openAssistantAs(SCRIBE_AGENT_ROLE)) return;
1775
+ requestScribeStart(detail);
1776
+ }
1869
1777
 
1870
1778
  // src/components/ScribeAssistantBridge.tsx
1871
1779
  function useSecondTicker(active) {
1872
- const [, force] = React7.useReducer((n) => n + 1, 0);
1873
- React7.useEffect(() => {
1780
+ const [, force] = React5.useReducer((n) => n + 1, 0);
1781
+ React5.useEffect(() => {
1874
1782
  if (!active) return;
1875
1783
  const timer3 = setInterval(() => {
1876
1784
  if (document.visibilityState === "visible") force();
@@ -1883,18 +1791,83 @@ function useSecondTicker(active) {
1883
1791
  };
1884
1792
  }, [active]);
1885
1793
  }
1886
- function ScribeAssistantBridge({ labels, stt, onOpenSession }) {
1794
+ var RESUMABLE_WINDOW_MS = 30 * 60 * 1e3;
1795
+ function ScribeAssistantBridge({
1796
+ labels,
1797
+ retention: retention2,
1798
+ stt,
1799
+ locale,
1800
+ onOpenSession
1801
+ }) {
1802
+ const { user } = useAuth();
1887
1803
  const state = useScribeStore((s) => s.state);
1888
1804
  const sessionId = useScribeStore((s) => s.sessionId);
1805
+ const subjectId = useScribeStore((s) => s.subjectId);
1889
1806
  const subjectName = useScribeStore((s) => s.subjectName);
1807
+ const orphans2 = useScribeStore((s) => s.orphans);
1890
1808
  const live2 = state === "recording" || state === "paused" || state === "interrupted";
1891
1809
  useSecondTicker(state === "recording");
1892
- React7.useEffect(() => {
1810
+ React5.useEffect(() => {
1893
1811
  watchTranscript(live2 ? sessionId : null, stt);
1894
1812
  }, [live2, sessionId, stt]);
1895
1813
  const elapsedMs2 = live2 ? Math.floor(getSnapshot().elapsedMs / 1e3) * 1e3 : 0;
1896
- const [finished, setFinished] = React7.useState(null);
1897
- const activity = React7.useMemo(() => {
1814
+ const [pending, setPending] = React5.useState(null);
1815
+ const start = React5.useCallback(
1816
+ async (detail, consentMode) => {
1817
+ setPending(null);
1818
+ if (!user?.id) {
1819
+ toast.error("Fa\xE7a login para gravar um atendimento");
1820
+ return;
1821
+ }
1822
+ const result = await beginSession({
1823
+ userId: user.id,
1824
+ subjectId: detail.subjectId,
1825
+ subjectName: detail.subjectName,
1826
+ appointmentId: detail.appointmentId,
1827
+ templateKeys: detail.templateKeys,
1828
+ locale,
1829
+ consentMode
1830
+ });
1831
+ if (!result.ok) toast.error(`N\xE3o foi poss\xEDvel iniciar: ${result.reason}`);
1832
+ },
1833
+ [user?.id, locale]
1834
+ );
1835
+ React5.useEffect(() => {
1836
+ const onStart = (ev) => {
1837
+ const detail = ev.detail ?? {};
1838
+ if (retention2.requireConsent) setPending(detail);
1839
+ else void start(detail, retention2.defaultConsentMode);
1840
+ };
1841
+ window.addEventListener(SCRIBE_START_EVENT, onStart);
1842
+ return () => window.removeEventListener(SCRIBE_START_EVENT, onStart);
1843
+ }, [retention2.requireConsent, retention2.defaultConsentMode, start]);
1844
+ const activity = React5.useMemo(() => {
1845
+ if (pending && !live2) {
1846
+ const who = pending.subjectName;
1847
+ return {
1848
+ id: "scribe.session",
1849
+ kind: "recording",
1850
+ state: "pending",
1851
+ label: who ? `${labels.sessionSingular} \xB7 ${who}` : labels.sessionSingular,
1852
+ // A ticked box consents for nobody — the holder does. What it records
1853
+ // is the professional ASSERTING they obtained it, which is the fact
1854
+ // that has to be provable later (LGPD art. 5º II), stamped on the same
1855
+ // row as the audio rather than reconstructed afterwards.
1856
+ confirm: {
1857
+ acknowledge: `Informei ${who ?? "o titular"} de que este atendimento ser\xE1 gravado e obtive o consentimento.`,
1858
+ note: "O \xE1udio fica guardado de forma privada e pode ser apagado quando voc\xEA quiser \u2014 a transcri\xE7\xE3o e o documento permanecem."
1859
+ },
1860
+ actions: [
1861
+ { intent: "cancel", label: "Cancelar", run: () => setPending(null) },
1862
+ {
1863
+ intent: "start",
1864
+ label: labels.start,
1865
+ primary: true,
1866
+ run: () => void start(pending, retention2.defaultConsentMode)
1867
+ }
1868
+ ]
1869
+ };
1870
+ }
1898
1871
  if (live2) {
1899
1872
  const actions = [
1900
1873
  state === "paused" ? { intent: "resume", label: labels.resume, run: () => void resumeSession() } : { intent: "pause", label: labels.pause, run: () => void pauseSession() },
@@ -1902,10 +1875,15 @@ function ScribeAssistantBridge({ labels, stt, onOpenSession }) {
1902
1875
  intent: "stop",
1903
1876
  label: labels.finish,
1904
1877
  primary: true,
1878
+ // Stopping ends the assistant's part of this, and the app opens the
1879
+ // session. Upload and transcription were never waiting on a click —
1880
+ // the pump carries them — so an "encerrado, gerar?" step in between
1881
+ // was a screen asking permission for work already in progress, on the
1882
+ // surface least able to do the rest of it.
1905
1883
  run: async () => {
1906
1884
  const id = sessionId;
1907
1885
  await endSession();
1908
- if (id) setFinished({ id, label: subjectName ? `${labels.sessionSingular} \xB7 ${subjectName}` : labels.sessionSingular });
1886
+ if (id) onOpenSession?.(id, subjectId);
1909
1887
  }
1910
1888
  }
1911
1889
  ];
@@ -1920,32 +1898,58 @@ function ScribeAssistantBridge({ labels, stt, onOpenSession }) {
1920
1898
  stream: getCaptureStream
1921
1899
  };
1922
1900
  }
1923
- if (finished) {
1901
+ const orphan = orphans2[0];
1902
+ if (orphan) {
1903
+ const canResume = Date.now() - orphan.lastTickEpochMs < RESUMABLE_WINDOW_MS;
1904
+ const actions = [];
1905
+ if (canResume) {
1906
+ actions.push({ intent: "resume", label: labels.resume, run: () => void resumeSession() });
1907
+ }
1908
+ actions.push({
1909
+ intent: "stop",
1910
+ label: labels.finish,
1911
+ primary: true,
1912
+ run: async () => {
1913
+ const id = await endSession();
1914
+ dismissOrphan(orphan.id);
1915
+ if (id) onOpenSession?.(id, orphan.subjectId);
1916
+ }
1917
+ });
1918
+ actions.push({
1919
+ intent: "discard",
1920
+ label: labels.discard,
1921
+ run: () => void discardSession(orphan.id)
1922
+ });
1924
1923
  return {
1925
1924
  id: "scribe.session",
1926
1925
  kind: "recording",
1927
- label: `${finished.label} \u2014 ${labels.generate.toLowerCase()}?`,
1928
- state: "done",
1929
- actions: [
1930
- {
1931
- intent: "generate",
1932
- label: labels.generate,
1933
- primary: true,
1934
- run: () => {
1935
- onOpenSession?.(finished.id);
1936
- setFinished(null);
1937
- }
1938
- },
1939
- { intent: "discard", label: "Depois", run: () => setFinished(null) }
1940
- ]
1926
+ // Not `orphan.accumulatedMs` see orphanDurationMs: older sessions sit
1927
+ // at 0, and half an hour of audio announced itself as 00:00.
1928
+ elapsedMs: orphanDurationMs(orphan),
1929
+ label: orphan.subjectName ? `${labels.sessionSingular} interrompido \xB7 ${orphan.subjectName}` : `${labels.sessionSingular} interrompido`,
1930
+ state: "paused",
1931
+ actions
1941
1932
  };
1942
1933
  }
1943
1934
  return null;
1944
- }, [live2, state, elapsedMs2, subjectName, sessionId, finished, labels, onOpenSession]);
1935
+ }, [
1936
+ live2,
1937
+ state,
1938
+ elapsedMs2,
1939
+ subjectId,
1940
+ subjectName,
1941
+ sessionId,
1942
+ orphans2,
1943
+ labels,
1944
+ onOpenSession,
1945
+ pending,
1946
+ start,
1947
+ retention2.defaultConsentMode
1948
+ ]);
1945
1949
  usePublishAssistantActivity(activity);
1946
- const liveRef = React7.useRef({ live: live2, state, subjectName, sessionId });
1950
+ const liveRef = React5.useRef({ live: live2, state, subjectName, sessionId });
1947
1951
  liveRef.current = { live: live2, state, subjectName, sessionId };
1948
- const surface = React7.useMemo(
1952
+ const surface = React5.useMemo(
1949
1953
  () => ({
1950
1954
  id: "scribe.session",
1951
1955
  pluginId: "scribe",
@@ -1978,138 +1982,38 @@ function ScribeAssistantBridge({ labels, stt, onOpenSession }) {
1978
1982
  useAgentSurface(surface);
1979
1983
  const supported = isCaptureSupported();
1980
1984
  useRegisterAssistantLauncher(
1981
- live2 ? null : {
1985
+ live2 || pending ? null : {
1982
1986
  id: "scribe.start",
1983
1987
  kind: "recording",
1988
+ role: SCRIBE_AGENT_ROLE,
1984
1989
  label: labels.start,
1985
1990
  disabled: !supported,
1986
1991
  disabledReason: "Este navegador n\xE3o suporta grava\xE7\xE3o de \xE1udio",
1987
- run: () => {
1988
- openAssistant();
1989
- requestScribeStart({});
1990
- }
1992
+ // Already in her thread, so this is the plain request — no need to
1993
+ // re-open the panel it is being pressed inside.
1994
+ run: () => requestScribeStart({})
1991
1995
  }
1992
1996
  );
1993
1997
  return null;
1994
1998
  }
1995
- var SCRIBE_START_EVENT = "scribe:start";
1996
1999
  function ScribeShellMount({ labels, retention: retention2, stt, locale, onOpenSession }) {
1997
- const { user } = useAuth();
1998
- const railOffset = useRightRailStore((s) => s.open ? s.width : 0);
1999
- const contentTop = useContentTop();
2000
- const bannerRef = React7.useRef(null);
2001
- useReserveSpace(bannerRef);
2002
- const [pending, setPending] = React7.useState(null);
2003
- React7.useEffect(() => {
2000
+ React5.useEffect(() => {
2004
2001
  wireScribeStore();
2005
2002
  }, []);
2006
- React7.useEffect(() => {
2007
- const onStart = (ev) => {
2008
- const detail = ev.detail ?? {};
2009
- if (retention2.requireConsent) setPending(detail);
2010
- else void start(detail, retention2.defaultConsentMode);
2011
- };
2012
- window.addEventListener(SCRIBE_START_EVENT, onStart);
2013
- return () => window.removeEventListener(SCRIBE_START_EVENT, onStart);
2014
- }, [retention2.requireConsent, retention2.defaultConsentMode, user?.id, locale]);
2015
- const start = React7.useCallback(
2016
- async (detail, consentMode) => {
2017
- if (!user?.id) {
2018
- toast.error("Fa\xE7a login para gravar um atendimento");
2019
- return;
2020
- }
2021
- const result = await beginSession({
2022
- userId: user.id,
2023
- subjectId: detail.subjectId,
2024
- subjectName: detail.subjectName,
2025
- appointmentId: detail.appointmentId,
2026
- templateKeys: detail.templateKeys,
2027
- locale,
2028
- consentMode
2029
- });
2030
- if (!result.ok) {
2031
- toast.error(`N\xE3o foi poss\xEDvel iniciar: ${result.reason}`);
2032
- return;
2033
- }
2034
- toast.success(`${labels.sessionSingular} iniciado`);
2035
- },
2036
- [user?.id, locale, labels.sessionSingular]
2037
- );
2038
2003
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2039
- /* @__PURE__ */ jsx(ScribeAssistantBridge, { labels, stt, onOpenSession }),
2040
- /* @__PURE__ */ jsx(ScribeRecordingPill, { labels, onOpenSession, mobileOnly: true }),
2041
2004
  /* @__PURE__ */ jsx(
2042
- ScribeConsentDialog,
2005
+ ScribeAssistantBridge,
2043
2006
  {
2044
- open: pending !== null,
2045
2007
  labels,
2046
- subjectName: pending?.subjectName,
2047
- defaultConsentMode: retention2.defaultConsentMode,
2048
- onCancel: () => setPending(null),
2049
- onConfirm: (mode) => {
2050
- const detail = pending;
2051
- setPending(null);
2052
- if (detail) void start(detail, mode);
2053
- }
2008
+ retention: retention2,
2009
+ stt,
2010
+ locale,
2011
+ onOpenSession
2054
2012
  }
2055
2013
  ),
2056
- typeof document !== "undefined" && createPortal(
2057
- /* @__PURE__ */ jsx(
2058
- "div",
2059
- {
2060
- ref: bannerRef,
2061
- className: "pointer-events-auto fixed left-0 z-40",
2062
- style: { top: contentTop, right: railOffset },
2063
- children: /* @__PURE__ */ jsx(ScribeRecoveryBanner, { labels, onOpenSession })
2064
- }
2065
- ),
2066
- document.body
2067
- )
2014
+ /* @__PURE__ */ jsx(ScribeRecordingPill, { labels, onOpenSession, mobileOnly: true })
2068
2015
  ] });
2069
2016
  }
2070
- function useReserveSpace(ref) {
2071
- React7.useEffect(() => {
2072
- if (typeof document === "undefined") return;
2073
- const main = document.querySelector("main");
2074
- const node = ref.current;
2075
- if (!main || !node) return;
2076
- const previous = main.style.paddingTop;
2077
- const apply = () => {
2078
- const height = node.getBoundingClientRect().height;
2079
- main.style.paddingTop = height > 0 ? `${Math.ceil(height)}px` : previous;
2080
- };
2081
- apply();
2082
- const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(apply) : null;
2083
- observer?.observe(node);
2084
- return () => {
2085
- observer?.disconnect();
2086
- main.style.paddingTop = previous;
2087
- };
2088
- }, [ref]);
2089
- }
2090
- function useContentTop() {
2091
- const [top, setTop] = React7.useState(56);
2092
- React7.useEffect(() => {
2093
- if (typeof document === "undefined") return;
2094
- const measure = () => {
2095
- const main2 = document.querySelector("main");
2096
- if (main2) setTop(Math.round(main2.getBoundingClientRect().top));
2097
- };
2098
- measure();
2099
- window.addEventListener("resize", measure);
2100
- const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(measure) : null;
2101
- const main = document.querySelector("main");
2102
- if (observer && main) observer.observe(main);
2103
- return () => {
2104
- window.removeEventListener("resize", measure);
2105
- observer?.disconnect();
2106
- };
2107
- }, []);
2108
- return top;
2109
- }
2110
- function requestScribeStart(detail) {
2111
- window.dispatchEvent(new CustomEvent(SCRIBE_START_EVENT, { detail }));
2112
- }
2113
2017
 
2114
2018
  // src/lib/prompt.ts
2115
2019
  var PROMPT_VERSION = "scribe/narrative@1";
@@ -2277,15 +2181,15 @@ function DraftPanel({
2277
2181
  onDiscarded
2278
2182
  }) {
2279
2183
  const original = generation.markdown ?? "";
2280
- const [value, setValue] = React7.useState(generation.markdownEdited ?? original);
2281
- const [saving, setSaving] = React7.useState(false);
2282
- const [committing, setCommitting] = React7.useState(false);
2283
- const timer3 = React7.useRef(null);
2284
- React7.useEffect(() => {
2184
+ const [value, setValue] = React5.useState(generation.markdownEdited ?? original);
2185
+ const [saving, setSaving] = React5.useState(false);
2186
+ const [committing, setCommitting] = React5.useState(false);
2187
+ const timer3 = React5.useRef(null);
2188
+ React5.useEffect(() => {
2285
2189
  setValue(generation.markdownEdited ?? generation.markdown ?? "");
2286
2190
  }, [generation.id, generation.markdown, generation.markdownEdited]);
2287
2191
  const edited = value.trim() !== original.trim();
2288
- const handleChange = React7.useCallback(
2192
+ const handleChange = React5.useCallback(
2289
2193
  (next) => {
2290
2194
  setValue(next);
2291
2195
  if (timer3.current) clearTimeout(timer3.current);
@@ -2296,7 +2200,7 @@ function DraftPanel({
2296
2200
  },
2297
2201
  [generation.id]
2298
2202
  );
2299
- React7.useEffect(() => {
2203
+ React5.useEffect(() => {
2300
2204
  return () => {
2301
2205
  if (timer3.current) {
2302
2206
  clearTimeout(timer3.current);
@@ -2304,7 +2208,7 @@ function DraftPanel({
2304
2208
  }
2305
2209
  };
2306
2210
  }, [generation.id, value]);
2307
- const handleCommit = React7.useCallback(async () => {
2211
+ const handleCommit = React5.useCallback(async () => {
2308
2212
  setCommitting(true);
2309
2213
  try {
2310
2214
  if (timer3.current) clearTimeout(timer3.current);
@@ -2494,36 +2398,38 @@ function ScribeSessionPage({
2494
2398
  stt,
2495
2399
  generateEndpoint,
2496
2400
  onOpenDocument,
2497
- onBack
2401
+ onBack,
2402
+ onSessionLoaded
2498
2403
  }) {
2499
- const [session, setSession] = React7.useState(null);
2500
- const [segments, setSegments] = React7.useState([]);
2501
- const [generations, setGenerations] = React7.useState([]);
2502
- const [loading, setLoading] = React7.useState(true);
2503
- const [generating, setGenerating] = React7.useState(false);
2504
- const [tab, setTab] = React7.useState("transcript");
2505
- const draining2 = React7.useRef(false);
2506
- const refresh = React7.useCallback(async () => {
2404
+ const [session, setSession] = React5.useState(null);
2405
+ const [segments, setSegments] = React5.useState([]);
2406
+ const [generations, setGenerations] = React5.useState([]);
2407
+ const [loading, setLoading] = React5.useState(true);
2408
+ const [generating, setGenerating] = React5.useState(false);
2409
+ const [tab, setTab] = React5.useState("transcript");
2410
+ const draining2 = React5.useRef(false);
2411
+ const refresh = React5.useCallback(async () => {
2507
2412
  const [s, segs, gens] = await Promise.all([
2508
2413
  fetchSession(sessionId),
2509
2414
  fetchSegments(sessionId),
2510
2415
  fetchGenerations(sessionId)
2511
2416
  ]);
2512
2417
  setSession(s);
2418
+ if (s) onSessionLoaded?.(s);
2513
2419
  setSegments(segs);
2514
2420
  setGenerations(gens);
2515
2421
  return { session: s, segments: segs };
2516
- }, [sessionId]);
2517
- React7.useEffect(() => {
2422
+ }, [sessionId, onSessionLoaded]);
2423
+ React5.useEffect(() => {
2518
2424
  setLoading(true);
2519
2425
  void refresh().finally(() => setLoading(false));
2520
2426
  }, [refresh]);
2521
- const pendingStt = React7.useMemo(
2427
+ const pendingStt = React5.useMemo(
2522
2428
  () => segments.filter((s) => s.uploadState === "uploaded" && ["pending", "failed"].includes(s.sttState) && s.sttAttempts < 4),
2523
2429
  [segments]
2524
2430
  );
2525
2431
  const busy = pendingStt.length > 0 || segments.some((s) => s.sttState === "running");
2526
- React7.useEffect(() => {
2432
+ React5.useEffect(() => {
2527
2433
  if (!busy) return;
2528
2434
  const id = setInterval(() => {
2529
2435
  void (async () => {
@@ -2552,7 +2458,7 @@ function ScribeSessionPage({
2552
2458
  return () => clearInterval(id);
2553
2459
  }, [busy, pendingStt.length, sessionId, stt, refresh]);
2554
2460
  const hasTranscript = segments.some((s) => !s.gap && s.text?.trim());
2555
- const handleGenerate = React7.useCallback(
2461
+ const handleGenerate = React5.useCallback(
2556
2462
  async (preset) => {
2557
2463
  setGenerating(true);
2558
2464
  try {
@@ -2583,7 +2489,7 @@ function ScribeSessionPage({
2583
2489
  );
2584
2490
  const activeGeneration = generations.find((g) => g.id === tab);
2585
2491
  useAgentSurface(
2586
- React7.useMemo(
2492
+ React5.useMemo(
2587
2493
  () => ({
2588
2494
  id: "scribe.session",
2589
2495
  describe: () => ({
@@ -2725,25 +2631,24 @@ function statusLabel(status) {
2725
2631
  };
2726
2632
  return map[status] ?? status;
2727
2633
  }
2728
- function ScribeHeaderAction({ labels, item, embedded }) {
2634
+ function ScribeHeaderAction({ labels, item, entityId }) {
2729
2635
  const subjectId = item?.personId ?? item?.id;
2730
2636
  const subjectName = item?.name ?? item?.full_name;
2731
2637
  const { entitled } = useAccessOptional();
2732
- if (!subjectId) return null;
2733
- if (!entitled("scribe")) return null;
2734
- if (!isCaptureSupported()) return null;
2735
- return /* @__PURE__ */ jsx(
2736
- ScribeStartButton,
2737
- {
2738
- labels,
2739
- variant: "outline",
2740
- size: embedded ? "sm" : "sm",
2741
- onStart: () => {
2742
- openAssistant();
2743
- requestScribeStart({ subjectId, subjectName });
2744
- }
2745
- }
2638
+ const busy = useScribeStore((s) => s.state === "recording" || s.state === "paused" || s.state === "interrupted" || s.state === "requesting");
2639
+ const available = !!subjectId && entitled("scribe") && isCaptureSupported();
2640
+ useHeaderAction(
2641
+ entityId ? `${entityId}.detail.header.actions` : "",
2642
+ available ? {
2643
+ id: "scribe-start",
2644
+ label: labels.start,
2645
+ icon: Mic,
2646
+ intent: "secondary",
2647
+ disabled: busy,
2648
+ onSelect: () => startScribeSession({ subjectId, subjectName })
2649
+ } : null
2746
2650
  );
2651
+ return null;
2747
2652
  }
2748
2653
 
2749
2654
  // src/migrations/index.ts
@@ -3272,8 +3177,8 @@ function registerScribeTimelineSource() {
3272
3177
  }
3273
3178
  var BARS = 8;
3274
3179
  function DeviceLevel({ deviceId, onSignal }) {
3275
- const barsRef = React7.useRef([]);
3276
- React7.useEffect(() => {
3180
+ const barsRef = React5.useRef([]);
3181
+ React5.useEffect(() => {
3277
3182
  let cancelled = false;
3278
3183
  let stream = null;
3279
3184
  let context = null;
@@ -3334,12 +3239,12 @@ function DeviceLevel({ deviceId, onSignal }) {
3334
3239
  )) });
3335
3240
  }
3336
3241
  function ScribeDevicePicker({ open, onOpenChange }) {
3337
- const [devices, setDevices] = React7.useState([]);
3338
- const [selected, setSelected] = React7.useState(null);
3339
- const [remember, setRemember] = React7.useState(true);
3340
- const [signals, setSignals] = React7.useState({});
3341
- const [busy, setBusy] = React7.useState(false);
3342
- React7.useEffect(() => {
3242
+ const [devices, setDevices] = React5.useState([]);
3243
+ const [selected, setSelected] = React5.useState(null);
3244
+ const [remember, setRemember] = React5.useState(true);
3245
+ const [signals, setSignals] = React5.useState({});
3246
+ const [busy, setBusy] = React5.useState(false);
3247
+ React5.useEffect(() => {
3343
3248
  if (!open) return;
3344
3249
  let cancelled = false;
3345
3250
  const load2 = async () => {
@@ -3360,7 +3265,7 @@ function ScribeDevicePicker({ open, onOpenChange }) {
3360
3265
  cancelled = true;
3361
3266
  };
3362
3267
  }, [open]);
3363
- const markSignal = React7.useCallback((deviceId) => {
3268
+ const markSignal = React5.useCallback((deviceId) => {
3364
3269
  setSignals((prev) => prev[deviceId] ? prev : { ...prev, [deviceId]: true });
3365
3270
  }, []);
3366
3271
  const confirm = async () => {
@@ -3428,11 +3333,11 @@ function ScribeLivePanel({ labels, onOpenSession }) {
3428
3333
  const sessionId = useScribeStore((s) => s.sessionId);
3429
3334
  const subjectName = useScribeStore((s) => s.subjectName);
3430
3335
  const segments = useLiveTranscript((s) => s.segments);
3431
- const [busy, setBusy] = React7.useState(false);
3432
- const [pickerOpen, setPickerOpen] = React7.useState(false);
3433
- const [, force] = React7.useReducer((n) => n + 1, 0);
3336
+ const [busy, setBusy] = React5.useState(false);
3337
+ const [pickerOpen, setPickerOpen] = React5.useState(false);
3338
+ const [, force] = React5.useReducer((n) => n + 1, 0);
3434
3339
  const live2 = state === "recording" || state === "paused" || state === "interrupted";
3435
- React7.useEffect(() => {
3340
+ React5.useEffect(() => {
3436
3341
  if (state !== "recording") return;
3437
3342
  const timer3 = setInterval(() => {
3438
3343
  if (document.visibilityState === "visible") force();
@@ -3556,33 +3461,37 @@ function createScribePlugin(options) {
3556
3461
  registerScribeTimelineSource();
3557
3462
  initScribeRuntime({ retention: retention2 });
3558
3463
  wireScribeStore();
3559
- const openSession = (sessionId) => {
3560
- window.location.hash = `/scribe/${sessionId}`;
3464
+ const subjectEntities = options.subjectEntities ?? ["clients", "patients", "people", "persons", "customers"];
3465
+ const sessionPath = options.sessionPath ?? ((args) => scribeSessionPath({ ...args, subjectEntity: subjectEntities[0] }));
3466
+ const openSession = (sessionId, subjectId) => {
3467
+ window.location.hash = sessionPath({ sessionId, subjectId });
3561
3468
  };
3562
- const ShellMount = () => React7__default.createElement(ScribeShellMount, { labels, retention: retention2, stt: options.stt, locale, onOpenSession: openSession });
3469
+ const ShellMount = () => React5__default.createElement(ScribeShellMount, { labels, retention: retention2, stt: options.stt, locale, onOpenSession: openSession });
3563
3470
  ShellMount.displayName = "ScribeShellMount";
3564
3471
  const SessionRoute = () => {
3565
3472
  const hash = typeof window !== "undefined" ? window.location.hash : "";
3566
3473
  const sessionId = hash.split("/scribe/")[1]?.split("/")[0] ?? "";
3567
3474
  if (!sessionId) return null;
3568
- return React7__default.createElement(ScribeSessionPage, {
3475
+ return React5__default.createElement(ScribeSessionPage, {
3569
3476
  sessionId,
3570
3477
  labels,
3571
3478
  presets,
3572
3479
  stt: options.stt,
3573
3480
  generateEndpoint,
3481
+ onSessionLoaded: (session) => {
3482
+ if (session.subjectId) openSession(session.id, session.subjectId);
3483
+ },
3574
3484
  onBack: () => {
3575
- window.location.hash = "/clients";
3485
+ window.location.hash = `/${subjectEntities[0]}`;
3576
3486
  },
3577
3487
  onOpenDocument: () => {
3578
- window.location.hash = "/clients";
3488
+ window.location.hash = `/${subjectEntities[0]}`;
3579
3489
  }
3580
3490
  });
3581
3491
  };
3582
3492
  SessionRoute.displayName = "ScribeSessionRoute";
3583
- const HeaderAction = (props) => React7__default.createElement(ScribeHeaderAction, { ...props, labels });
3493
+ const HeaderAction = (props) => React5__default.createElement(ScribeHeaderAction, { ...props, labels });
3584
3494
  HeaderAction.displayName = "ScribeHeaderAction";
3585
- const subjectEntities = options.subjectEntities ?? ["clients", "patients", "people", "persons", "customers"];
3586
3495
  return {
3587
3496
  id: "scribe",
3588
3497
  name: labels.sessionPlural,
@@ -3597,6 +3506,10 @@ function createScribePlugin(options) {
3597
3506
  // impressa passaria a depender de bucket, duas edge functions e chave de
3598
3507
  // modelo.
3599
3508
  dependencies: ["custom_forms"],
3509
+ // O trabalho deste plugin é de quem está NA SALA — grava o atendimento e o
3510
+ // transforma em documento. O plugin diz o papel; o app diz o nome (no salão,
3511
+ // a Auxiliar de Atendimento). Ver AgentRole em @fayz-ai/core.
3512
+ defaultAgentRole: SCRIBE_AGENT_ROLE,
3600
3513
  navigation: [],
3601
3514
  routes: [
3602
3515
  {
@@ -3675,6 +3588,6 @@ function createScribePlugin(options) {
3675
3588
  };
3676
3589
  }
3677
3590
 
3678
- export { PROMPT_VERSION, SCRIBE_START_EVENT, ScribeDevicePicker, ScribeLivePanel, ScribeSessionPage, ScribeStartButton, beginSession, createScribePlugin, deleteSessionAudio, discardSession, endSession, fetchGenerations, fetchOpenSessions, fetchSegments, fetchSession, fetchSessionsForSubject, formatElapsed, formatOffset, isCaptureSupported, isNarrativeSchema, parseSections, pauseSession, renderTranscript, requestScribeStart, resumeSession, useScribeStore };
3591
+ export { PROMPT_VERSION, SCRIBE_AGENT_ROLE, SCRIBE_START_EVENT, ScribeDevicePicker, ScribeLivePanel, ScribeSessionPage, ScribeStartButton, beginSession, createScribePlugin, deleteSessionAudio, discardSession, endSession, fetchGenerations, fetchOpenSessions, fetchSegments, fetchSession, fetchSessionsForSubject, formatElapsed, formatOffset, isCaptureSupported, isNarrativeSchema, parseSections, pauseSession, renderTranscript, requestScribeStart, resumeSession, scribeSessionPath, startScribeSession, useScribeStore };
3679
3592
  //# sourceMappingURL=index.js.map
3680
3593
  //# sourceMappingURL=index.js.map