@umodeapporg/ui 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,12 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
- import { useRef } from "react";
2
+ import { useRef, useEffect } from "react";
3
3
  import classNames from "classnames";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import ControlledTooltip from "../UI/ControlledTooltip.js";
6
- import { useVoiceTranscription, VOICE_COMMANDS } from "../../hooks/useVoiceTranscription.js";
6
+ import { useVoiceTranscription, VOICE_COMMANDS, applyVoiceUpdate, appendInterimTranscript } from "../../hooks/useVoiceTranscription.js";
7
7
  import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
8
8
  import ControlledButton from "./ControlledButton.js";
9
9
  import ControlledTextarea from "./ControlledTextarea.js";
10
- const appendText = (current, addition) => {
11
- if (!addition) return current;
12
- const needsSpace = current.length > 0 && !/\s$/.test(current) && !/^[\s.,!?]/.test(addition);
13
- return current + (needsSpace ? " " : "") + addition;
14
- };
15
- const applyVoiceUpdate = (current, update) => {
16
- let next = current;
17
- for (let i = 0; i < update.deleteWords; i += 1) {
18
- next = next.replace(/\s*\S+\s*$/, "");
19
- }
20
- return appendText(next, update.insert);
21
- };
22
10
  const ControlledVoiceTextarea = ({
23
11
  value,
24
12
  onChange,
@@ -30,18 +18,34 @@ const ControlledVoiceTextarea = ({
30
18
  }) => {
31
19
  const { t } = useTranslation("ui");
32
20
  const committedRef = useRef(value);
33
- const { isSupported, isListening, toggle } = useVoiceTranscription({
21
+ const lastEmittedValueRef = useRef(value);
22
+ useEffect(() => {
23
+ if (value === lastEmittedValueRef.current) return;
24
+ committedRef.current = value;
25
+ lastEmittedValueRef.current = value;
26
+ }, [value]);
27
+ const emitValue = (next) => {
28
+ lastEmittedValueRef.current = next;
29
+ onChange(next);
30
+ };
31
+ const { isSupported, isListening, error, toggle } = useVoiceTranscription({
34
32
  lang,
35
33
  onCommit: (update) => {
36
34
  const next = applyVoiceUpdate(committedRef.current, update);
37
35
  committedRef.current = next;
38
- onChange(next);
36
+ emitValue(next);
39
37
  },
40
- onInterim: (text) => onChange(appendText(committedRef.current, text))
38
+ onInterim: (text) => emitValue(appendInterimTranscript(committedRef.current, text))
41
39
  });
42
40
  const handleManualChange = (next) => {
43
41
  committedRef.current = next;
44
- onChange(next);
42
+ emitValue(next);
43
+ };
44
+ const errorMessageByCode = {
45
+ permission: t("voiceInput.errors.permission"),
46
+ microphone: t("voiceInput.errors.microphone"),
47
+ network: t("voiceInput.errors.network"),
48
+ unavailable: t("voiceInput.errors.unavailable")
45
49
  };
46
50
  const commandHint = VOICE_COMMANDS.map((command) => command.phrases[0]).join(", ");
47
51
  return /* @__PURE__ */ jsxs("div", { className: classNames("relative w-full", className), children: [
@@ -79,6 +83,10 @@ const ControlledVoiceTextarea = ({
79
83
  )
80
84
  }
81
85
  ) })
86
+ ] }),
87
+ isSupported && error && /* @__PURE__ */ jsxs("span", { role: "alert", className: "mt-1 flex items-center gap-1 text-xs text-danger-emphasis", children: [
88
+ /* @__PURE__ */ jsx(MaterialSymbol, { icon: "mic_off", size: 14 }),
89
+ errorMessageByCode[error]
82
90
  ] })
83
91
  ] });
84
92
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ControlledVoiceTextarea.js","sources":["../../../src/components/Form/ControlledVoiceTextarea.tsx"],"sourcesContent":["import { useRef } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { IVoiceTranscriptionUpdate, useVoiceTranscription, VOICE_COMMANDS } from '@/hooks/useVoiceTranscription';\n\nimport { MaterialSymbol } from '../UI/MaterialSymbol';\n\nimport ControlledButton from './ControlledButton';\nimport ControlledTextarea from './ControlledTextarea';\n\ninterface IControlledVoiceTextareaProps {\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n rows?: number;\n lang?: string;\n className?: string;\n disabled?: boolean;\n}\n\nconst appendText = (current: string, addition: string): string => {\n if (!addition) return current;\n const needsSpace = current.length > 0 && !/\\s$/.test(current) && !/^[\\s.,!?]/.test(addition);\n return current + (needsSpace ? ' ' : '') + addition;\n};\n\nconst applyVoiceUpdate = (current: string, update: IVoiceTranscriptionUpdate): string => {\n let next = current;\n for (let i = 0; i < update.deleteWords; i += 1) {\n next = next.replace(/\\s*\\S+\\s*$/, '');\n }\n return appendText(next, update.insert);\n};\n\nconst ControlledVoiceTextarea: React.FC<IControlledVoiceTextareaProps> = ({\n value,\n onChange,\n placeholder,\n rows = 4,\n lang = 'pt-BR',\n className,\n disabled,\n}) => {\n const { t } = useTranslation('ui');\n\n const committedRef = useRef(value);\n\n const { isSupported, isListening, toggle } = useVoiceTranscription({\n lang,\n onCommit: (update) => {\n const next = applyVoiceUpdate(committedRef.current, update);\n committedRef.current = next;\n onChange(next);\n },\n onInterim: (text) => onChange(appendText(committedRef.current, text)),\n });\n\n const handleManualChange = (next: string) => {\n committedRef.current = next;\n onChange(next);\n };\n\n const commandHint = VOICE_COMMANDS.map((command) => command.phrases[0]).join(', ');\n\n return (\n <div className={classNames('relative w-full', className)}>\n <ControlledTextarea\n value={value}\n rows={rows}\n resize=\"none\"\n disabled={disabled}\n placeholder={placeholder}\n onChange={(e) => handleManualChange(e.target.value)}\n className=\"pr-12\"\n />\n\n {isSupported && (\n <div className=\"absolute right-2 top-2 flex items-center gap-1\">\n <ControlledTooltip content={t('voiceInput.commandsHint', { commands: commandHint })} className=\"w-56\">\n <span className=\"flex size-7 items-center justify-center rounded-full text-foreground-faint\">\n <MaterialSymbol icon=\"help\" size={16} />\n </span>\n </ControlledTooltip>\n <ControlledTooltip content={isListening ? t('voiceInput.stop') : t('voiceInput.start')}>\n <ControlledButton\n defaultHtmlButton\n disabled={disabled}\n onClick={toggle}\n className={classNames(\n 'flex size-8 items-center justify-center rounded-full transition-colors',\n isListening\n ? 'bg-danger-soft text-danger-emphasis hover:bg-danger-soft'\n : 'text-foreground-faint hover:bg-muted-hover hover:text-brand-emphasis',\n )}\n >\n <MaterialSymbol\n icon={isListening ? 'stop' : 'mic'}\n size={18}\n className={isListening ? 'animate-pulse' : ''}\n />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n )}\n </div>\n );\n};\n\nexport default ControlledVoiceTextarea;\n"],"names":[],"mappings":";;;;;;;;;AAsBA,MAAM,aAAa,CAAC,SAAiB,aAA6B;AAChE,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,aAAa,QAAQ,SAAS,KAAK,CAAC,MAAM,KAAK,OAAO,KAAK,CAAC,YAAY,KAAK,QAAQ;AAC3F,SAAO,WAAW,aAAa,MAAM,MAAM;AAC7C;AAEA,MAAM,mBAAmB,CAAC,SAAiB,WAA8C;AACvF,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,OAAO,aAAa,KAAK,GAAG;AAC9C,WAAO,KAAK,QAAQ,cAAc,EAAE;AAAA,EACtC;AACA,SAAO,WAAW,MAAM,OAAO,MAAM;AACvC;AAEA,MAAM,0BAAmE,CAAC;AAAA,EACxE;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,QAAM,eAAe,OAAO,KAAK;AAEjC,QAAM,EAAE,aAAa,aAAa,OAAA,IAAW,sBAAsB;AAAA,IACjE;AAAA,IACA,UAAU,CAAC,WAAW;AACpB,YAAM,OAAO,iBAAiB,aAAa,SAAS,MAAM;AAC1D,mBAAa,UAAU;AACvB,eAAS,IAAI;AAAA,IACf;AAAA,IACA,WAAW,CAAC,SAAS,SAAS,WAAW,aAAa,SAAS,IAAI,CAAC;AAAA,EAAA,CACrE;AAED,QAAM,qBAAqB,CAAC,SAAiB;AAC3C,iBAAa,UAAU;AACvB,aAAS,IAAI;AAAA,EACf;AAEA,QAAM,cAAc,eAAe,IAAI,CAAC,YAAY,QAAQ,QAAQ,CAAC,CAAC,EAAE,KAAK,IAAI;AAEjF,8BACG,OAAA,EAAI,WAAW,WAAW,mBAAmB,SAAS,GACrD,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,QAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,UAAU,CAAC,MAAM,mBAAmB,EAAE,OAAO,KAAK;AAAA,QAClD,WAAU;AAAA,MAAA;AAAA,IAAA;AAAA,IAGX,eACC,qBAAC,OAAA,EAAI,WAAU,kDACb,UAAA;AAAA,MAAA,oBAAC,mBAAA,EAAkB,SAAS,EAAE,2BAA2B,EAAE,UAAU,YAAA,CAAa,GAAG,WAAU,QAC7F,8BAAC,QAAA,EAAK,WAAU,8EACd,UAAA,oBAAC,gBAAA,EAAe,MAAK,QAAO,MAAM,IAAI,EAAA,CACxC,EAAA,CACF;AAAA,MACA,oBAAC,qBAAkB,SAAS,cAAc,EAAE,iBAAiB,IAAI,EAAE,kBAAkB,GACnF,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,mBAAiB;AAAA,UACjB;AAAA,UACA,SAAS;AAAA,UACT,WAAW;AAAA,YACT;AAAA,YACA,cACI,6DACA;AAAA,UAAA;AAAA,UAGN,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAM,cAAc,SAAS;AAAA,cAC7B,MAAM;AAAA,cACN,WAAW,cAAc,kBAAkB;AAAA,YAAA;AAAA,UAAA;AAAA,QAC7C;AAAA,MAAA,EACF,CACF;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GAEJ;AAEJ;"}
1
+ {"version":3,"file":"ControlledVoiceTextarea.js","sources":["../../../src/components/Form/ControlledVoiceTextarea.tsx"],"sourcesContent":["import { useEffect, useRef } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport {\n appendInterimTranscript,\n applyVoiceUpdate,\n TVoiceTranscriptionError,\n useVoiceTranscription,\n VOICE_COMMANDS,\n} from '@/hooks/useVoiceTranscription';\n\nimport { MaterialSymbol } from '../UI/MaterialSymbol';\n\nimport ControlledButton from './ControlledButton';\nimport ControlledTextarea from './ControlledTextarea';\n\ninterface IControlledVoiceTextareaProps {\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n rows?: number;\n lang?: string;\n className?: string;\n disabled?: boolean;\n}\n\nconst ControlledVoiceTextarea: React.FC<IControlledVoiceTextareaProps> = ({\n value,\n onChange,\n placeholder,\n rows = 4,\n lang = 'pt-BR',\n className,\n disabled,\n}) => {\n const { t } = useTranslation('ui');\n\n const committedRef = useRef(value);\n const lastEmittedValueRef = useRef(value);\n\n useEffect(() => {\n if (value === lastEmittedValueRef.current) return;\n committedRef.current = value;\n lastEmittedValueRef.current = value;\n }, [value]);\n\n const emitValue = (next: string) => {\n lastEmittedValueRef.current = next;\n onChange(next);\n };\n\n const { isSupported, isListening, error, toggle } = useVoiceTranscription({\n lang,\n onCommit: (update) => {\n const next = applyVoiceUpdate(committedRef.current, update);\n committedRef.current = next;\n emitValue(next);\n },\n onInterim: (text) => emitValue(appendInterimTranscript(committedRef.current, text)),\n });\n\n const handleManualChange = (next: string) => {\n committedRef.current = next;\n emitValue(next);\n };\n\n const errorMessageByCode: Record<TVoiceTranscriptionError, string> = {\n permission: t('voiceInput.errors.permission'),\n microphone: t('voiceInput.errors.microphone'),\n network: t('voiceInput.errors.network'),\n unavailable: t('voiceInput.errors.unavailable'),\n };\n\n const commandHint = VOICE_COMMANDS.map((command) => command.phrases[0]).join(', ');\n\n return (\n <div className={classNames('relative w-full', className)}>\n <ControlledTextarea\n value={value}\n rows={rows}\n resize=\"none\"\n disabled={disabled}\n placeholder={placeholder}\n onChange={(e) => handleManualChange(e.target.value)}\n className=\"pr-12\"\n />\n\n {isSupported && (\n <div className=\"absolute right-2 top-2 flex items-center gap-1\">\n <ControlledTooltip content={t('voiceInput.commandsHint', { commands: commandHint })} className=\"w-56\">\n <span className=\"flex size-7 items-center justify-center rounded-full text-foreground-faint\">\n <MaterialSymbol icon=\"help\" size={16} />\n </span>\n </ControlledTooltip>\n <ControlledTooltip content={isListening ? t('voiceInput.stop') : t('voiceInput.start')}>\n <ControlledButton\n defaultHtmlButton\n disabled={disabled}\n onClick={toggle}\n className={classNames(\n 'flex size-8 items-center justify-center rounded-full transition-colors',\n isListening\n ? 'bg-danger-soft text-danger-emphasis hover:bg-danger-soft'\n : 'text-foreground-faint hover:bg-muted-hover hover:text-brand-emphasis',\n )}\n >\n <MaterialSymbol\n icon={isListening ? 'stop' : 'mic'}\n size={18}\n className={isListening ? 'animate-pulse' : ''}\n />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n )}\n\n {isSupported && error && (\n <span role=\"alert\" className=\"mt-1 flex items-center gap-1 text-xs text-danger-emphasis\">\n <MaterialSymbol icon=\"mic_off\" size={14} />\n {errorMessageByCode[error]}\n </span>\n )}\n </div>\n );\n};\n\nexport default ControlledVoiceTextarea;\n"],"names":[],"mappings":";;;;;;;;;AA4BA,MAAM,0BAAmE,CAAC;AAAA,EACxE;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,QAAM,eAAe,OAAO,KAAK;AACjC,QAAM,sBAAsB,OAAO,KAAK;AAExC,YAAU,MAAM;AACd,QAAI,UAAU,oBAAoB,QAAS;AAC3C,iBAAa,UAAU;AACvB,wBAAoB,UAAU;AAAA,EAChC,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,YAAY,CAAC,SAAiB;AAClC,wBAAoB,UAAU;AAC9B,aAAS,IAAI;AAAA,EACf;AAEA,QAAM,EAAE,aAAa,aAAa,OAAO,OAAA,IAAW,sBAAsB;AAAA,IACxE;AAAA,IACA,UAAU,CAAC,WAAW;AACpB,YAAM,OAAO,iBAAiB,aAAa,SAAS,MAAM;AAC1D,mBAAa,UAAU;AACvB,gBAAU,IAAI;AAAA,IAChB;AAAA,IACA,WAAW,CAAC,SAAS,UAAU,wBAAwB,aAAa,SAAS,IAAI,CAAC;AAAA,EAAA,CACnF;AAED,QAAM,qBAAqB,CAAC,SAAiB;AAC3C,iBAAa,UAAU;AACvB,cAAU,IAAI;AAAA,EAChB;AAEA,QAAM,qBAA+D;AAAA,IACnE,YAAY,EAAE,8BAA8B;AAAA,IAC5C,YAAY,EAAE,8BAA8B;AAAA,IAC5C,SAAS,EAAE,2BAA2B;AAAA,IACtC,aAAa,EAAE,+BAA+B;AAAA,EAAA;AAGhD,QAAM,cAAc,eAAe,IAAI,CAAC,YAAY,QAAQ,QAAQ,CAAC,CAAC,EAAE,KAAK,IAAI;AAEjF,8BACG,OAAA,EAAI,WAAW,WAAW,mBAAmB,SAAS,GACrD,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,QAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,UAAU,CAAC,MAAM,mBAAmB,EAAE,OAAO,KAAK;AAAA,QAClD,WAAU;AAAA,MAAA;AAAA,IAAA;AAAA,IAGX,eACC,qBAAC,OAAA,EAAI,WAAU,kDACb,UAAA;AAAA,MAAA,oBAAC,mBAAA,EAAkB,SAAS,EAAE,2BAA2B,EAAE,UAAU,YAAA,CAAa,GAAG,WAAU,QAC7F,8BAAC,QAAA,EAAK,WAAU,8EACd,UAAA,oBAAC,gBAAA,EAAe,MAAK,QAAO,MAAM,IAAI,EAAA,CACxC,EAAA,CACF;AAAA,MACA,oBAAC,qBAAkB,SAAS,cAAc,EAAE,iBAAiB,IAAI,EAAE,kBAAkB,GACnF,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,mBAAiB;AAAA,UACjB;AAAA,UACA,SAAS;AAAA,UACT,WAAW;AAAA,YACT;AAAA,YACA,cACI,6DACA;AAAA,UAAA;AAAA,UAGN,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAM,cAAc,SAAS;AAAA,cAC7B,MAAM;AAAA,cACN,WAAW,cAAc,kBAAkB;AAAA,YAAA;AAAA,UAAA;AAAA,QAC7C;AAAA,MAAA,EACF,CACF;AAAA,IAAA,GACF;AAAA,IAGD,eAAe,SACd,qBAAC,UAAK,MAAK,SAAQ,WAAU,6DAC3B,UAAA;AAAA,MAAA,oBAAC,gBAAA,EAAe,MAAK,WAAU,MAAM,IAAI;AAAA,MACxC,mBAAmB,KAAK;AAAA,IAAA,EAAA,CAC3B;AAAA,EAAA,GAEJ;AAEJ;"}
@@ -103,7 +103,7 @@ const NoDataFoundActionButton = ({
103
103
  isPrimary ? "shadow-md shadow-brand/20 hover:-translate-y-px hover:shadow-lg hover:shadow-brand/25" : "text-foreground-muted hover:bg-muted-hover"
104
104
  ),
105
105
  children: [
106
- action.icon ? /* @__PURE__ */ jsx(MaterialSymbol, { icon: action.icon, size: 16 }) : null,
106
+ action.icon ? /* @__PURE__ */ jsx(MaterialSymbol, { icon: action.icon, size: 16, "aria-hidden": true }) : null,
107
107
  /* @__PURE__ */ jsx("span", { children: action.label })
108
108
  ]
109
109
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/components/UI/NoDataFound/index.tsx"],"sourcesContent":["import { isValidElement, memo } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '../../Form/ControlledButton';\nimport { MaterialSymbol, SymbolCodepoints } from '../MaterialSymbol';\n\nimport {\n NO_DATA_FOUND_SIZES,\n NO_DATA_FOUND_TONES,\n NO_DATA_FOUND_VARIANTS,\n TNoDataFoundSize,\n TNoDataFoundTone,\n TNoDataFoundVariant,\n} from './noDataFoundPresets';\n\nexport type { TNoDataFoundSize, TNoDataFoundTone, TNoDataFoundVariant } from './noDataFoundPresets';\n\nexport interface INoDataFoundAction {\n label: string;\n onClick: () => void;\n icon?: SymbolCodepoints;\n disabled?: boolean;\n}\n\ntype TNoDataFoundActionProp = INoDataFoundAction | React.ReactNode;\n\nexport interface INoDataFoundProps {\n variant?: TNoDataFoundVariant;\n size?: TNoDataFoundSize;\n tone?: TNoDataFoundTone;\n title?: string;\n /** Alias of `title`, kept because most call sites already speak this name. */\n message?: string;\n description?: string;\n icon?: SymbolCodepoints;\n illustration?: React.ReactNode;\n action?: TNoDataFoundActionProp;\n secondaryAction?: TNoDataFoundActionProp;\n /** Sugar for `size=\"inline\"`: a single row for dropdowns, list slots and dense panels. */\n compact?: boolean;\n children?: React.ReactNode;\n className?: string;\n /** Turn off the live region when the host already announces the state itself. */\n announce?: boolean;\n}\n\ntype TToneClasses = (typeof NO_DATA_FOUND_TONES)[TNoDataFoundTone];\ntype TSizeClasses = (typeof NO_DATA_FOUND_SIZES)[Exclude<TNoDataFoundSize, 'inline'>];\n\nconst isActionConfig = (action: TNoDataFoundActionProp): action is INoDataFoundAction =>\n typeof action === 'object' && action !== null && !isValidElement(action) && 'label' in action;\n\ninterface INoDataFoundVisualProps {\n icon: SymbolCodepoints;\n toneClasses: TToneClasses;\n sizeClasses: TSizeClasses;\n}\n\nconst NoDataFoundVisual: React.FC<INoDataFoundVisualProps> = ({ icon, toneClasses, sizeClasses }) => (\n <span\n aria-hidden\n className={classNames(\n 'relative flex shrink-0 animate-state-reveal items-center justify-center',\n sizeClasses.visual,\n )}\n >\n <span\n className={classNames(\n 'absolute inset-0 m-auto animate-state-breathe rounded-full blur-2xl',\n toneClasses.aura,\n sizeClasses.aura,\n )}\n />\n <span\n className={classNames(\n 'absolute inset-0 m-auto animate-state-halo rounded-full border',\n toneClasses.halo,\n sizeClasses.halo,\n )}\n />\n <span\n className={classNames(\n 'absolute inset-0 m-auto animate-state-halo rounded-full border [animation-delay:1.5s]',\n toneClasses.halo,\n sizeClasses.halo,\n )}\n />\n\n <span\n className={classNames(\n 'relative flex animate-state-float items-center justify-center border bg-gradient-to-br shadow-elevation3 glass-tint',\n toneClasses.tile,\n sizeClasses.tile,\n )}\n >\n <MaterialSymbol icon={icon} weight={300} className={toneClasses.icon} />\n </span>\n\n <span\n className={classNames(\n 'absolute right-[6%] top-[18%] size-1.5 animate-state-drift rounded-full',\n toneClasses.accent,\n )}\n />\n <span\n className={classNames(\n 'absolute bottom-[16%] left-[9%] size-1 animate-state-drift rounded-full [animation-delay:2.5s]',\n toneClasses.accent,\n )}\n />\n <span\n className={classNames(\n 'absolute left-[24%] top-[4%] size-1 animate-state-drift rounded-full [animation-delay:5s]',\n toneClasses.accent,\n )}\n />\n </span>\n);\n\nconst NoDataFoundActionButton: React.FC<{ action: INoDataFoundAction; isPrimary: boolean }> = ({\n action,\n isPrimary,\n}) => (\n <ControlledButton\n size=\"sm\"\n variant={isPrimary ? 'filled' : 'text'}\n tone={isPrimary ? 'brand' : 'neutral'}\n disabled={action.disabled}\n onClick={action.onClick}\n className={classNames(\n 'flex items-center gap-x-1.5 px-3.5 text-xs transition-interactive duration-base',\n isPrimary\n ? 'shadow-md shadow-brand/20 hover:-translate-y-px hover:shadow-lg hover:shadow-brand/25'\n : 'text-foreground-muted hover:bg-muted-hover',\n )}\n >\n {action.icon ? <MaterialSymbol icon={action.icon} size={16} /> : null}\n <span>{action.label}</span>\n </ControlledButton>\n);\n\nconst renderAction = (action: TNoDataFoundActionProp, isPrimary: boolean): React.ReactNode => {\n if (!action) return null;\n if (isActionConfig(action)) return <NoDataFoundActionButton action={action} isPrimary={isPrimary} />;\n\n return action;\n};\n\nconst NoDataFound: React.FC<INoDataFoundProps> = ({\n variant = 'empty',\n size = 'md',\n tone,\n title,\n message,\n description,\n icon,\n illustration,\n action,\n secondaryAction,\n compact = false,\n children,\n className,\n announce = true,\n}) => {\n const { t } = useTranslation('ui');\n\n const preset = NO_DATA_FOUND_VARIANTS[variant] ?? NO_DATA_FOUND_VARIANTS.empty;\n const toneClasses = NO_DATA_FOUND_TONES[tone ?? preset.tone] ?? NO_DATA_FOUND_TONES.neutral;\n const resolvedIcon = icon ?? preset.icon;\n const resolvedTitle = title ?? message ?? t(preset.titleKey);\n const liveRegionProps = announce ? { role: 'status', 'aria-live': 'polite' as const } : {};\n\n const primaryAction = renderAction(action, true);\n const supportAction = renderAction(secondaryAction, false);\n\n if (compact || size === 'inline') {\n return (\n <div {...liveRegionProps} className={twMerge('flex w-full items-center gap-x-1.5 text-left', className)}>\n <MaterialSymbol icon={resolvedIcon} size={16} className={classNames('shrink-0', toneClasses.icon)} />\n <span className=\"min-w-0 text-xs text-foreground-subtle\">{resolvedTitle}</span>\n\n {description ? (\n <>\n <span aria-hidden className=\"size-1 shrink-0 rounded-full bg-border-emphasis\" />\n <span className=\"min-w-0 truncate text-xs text-foreground-faint\">{description}</span>\n </>\n ) : null}\n\n {primaryAction ? <span className=\"ml-auto shrink-0\">{primaryAction}</span> : null}\n </div>\n );\n }\n\n const sizeClasses = NO_DATA_FOUND_SIZES[size] ?? NO_DATA_FOUND_SIZES.md;\n\n return (\n <div\n {...liveRegionProps}\n className={twMerge(\n classNames(\n 'relative flex w-full flex-col items-center justify-center overflow-hidden text-center @container',\n sizeClasses.root,\n ),\n className,\n )}\n >\n {sizeClasses.hasTexture ? (\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-0 [mask-image:radial-gradient(60%_55%_at_50%_35%,black,transparent)]\"\n >\n <span className=\"absolute inset-0 bg-[radial-gradient(circle,rgba(148,163,184,0.3)_1px,transparent_1px)] [background-size:1.25rem_1.25rem]\" />\n </span>\n ) : null}\n\n {illustration ?? <NoDataFoundVisual icon={resolvedIcon} toneClasses={toneClasses} sizeClasses={sizeClasses} />}\n\n <p\n className={classNames(\n 'relative animate-state-rise font-semibold tracking-[-0.01em] text-foreground [animation-delay:80ms]',\n sizeClasses.titleClass,\n )}\n >\n {resolvedTitle}\n </p>\n\n {description ? (\n <p\n className={classNames(\n 'relative animate-state-rise text-balance leading-relaxed text-foreground-subtle [animation-delay:140ms]',\n sizeClasses.descriptionClass,\n )}\n >\n {description}\n </p>\n ) : null}\n\n {children ? <div className=\"relative w-full animate-state-rise [animation-delay:180ms]\">{children}</div> : null}\n\n {primaryAction || supportAction ? (\n <div\n className={classNames(\n 'relative flex animate-state-rise flex-wrap items-center justify-center gap-2 [animation-delay:220ms]',\n sizeClasses.actions,\n )}\n >\n {primaryAction}\n {supportAction}\n </div>\n ) : null}\n </div>\n );\n};\n\nexport default memo(NoDataFound);\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/components/UI/NoDataFound/index.tsx"],"sourcesContent":["import { isValidElement, memo } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '../../Form/ControlledButton';\nimport { MaterialSymbol, SymbolCodepoints } from '../MaterialSymbol';\n\nimport {\n NO_DATA_FOUND_SIZES,\n NO_DATA_FOUND_TONES,\n NO_DATA_FOUND_VARIANTS,\n TNoDataFoundSize,\n TNoDataFoundTone,\n TNoDataFoundVariant,\n} from './noDataFoundPresets';\n\nexport type { TNoDataFoundSize, TNoDataFoundTone, TNoDataFoundVariant } from './noDataFoundPresets';\n\nexport interface INoDataFoundAction {\n label: string;\n onClick: () => void;\n icon?: SymbolCodepoints;\n disabled?: boolean;\n}\n\ntype TNoDataFoundActionProp = INoDataFoundAction | React.ReactNode;\n\nexport interface INoDataFoundProps {\n variant?: TNoDataFoundVariant;\n size?: TNoDataFoundSize;\n tone?: TNoDataFoundTone;\n title?: string;\n /** Alias of `title`, kept because most call sites already speak this name. */\n message?: string;\n description?: string;\n icon?: SymbolCodepoints;\n illustration?: React.ReactNode;\n action?: TNoDataFoundActionProp;\n secondaryAction?: TNoDataFoundActionProp;\n /** Sugar for `size=\"inline\"`: a single row for dropdowns, list slots and dense panels. */\n compact?: boolean;\n children?: React.ReactNode;\n className?: string;\n /** Turn off the live region when the host already announces the state itself. */\n announce?: boolean;\n}\n\ntype TToneClasses = (typeof NO_DATA_FOUND_TONES)[TNoDataFoundTone];\ntype TSizeClasses = (typeof NO_DATA_FOUND_SIZES)[Exclude<TNoDataFoundSize, 'inline'>];\n\nconst isActionConfig = (action: TNoDataFoundActionProp): action is INoDataFoundAction =>\n typeof action === 'object' && action !== null && !isValidElement(action) && 'label' in action;\n\ninterface INoDataFoundVisualProps {\n icon: SymbolCodepoints;\n toneClasses: TToneClasses;\n sizeClasses: TSizeClasses;\n}\n\nconst NoDataFoundVisual: React.FC<INoDataFoundVisualProps> = ({ icon, toneClasses, sizeClasses }) => (\n <span\n aria-hidden\n className={classNames(\n 'relative flex shrink-0 animate-state-reveal items-center justify-center',\n sizeClasses.visual,\n )}\n >\n <span\n className={classNames(\n 'absolute inset-0 m-auto animate-state-breathe rounded-full blur-2xl',\n toneClasses.aura,\n sizeClasses.aura,\n )}\n />\n <span\n className={classNames(\n 'absolute inset-0 m-auto animate-state-halo rounded-full border',\n toneClasses.halo,\n sizeClasses.halo,\n )}\n />\n <span\n className={classNames(\n 'absolute inset-0 m-auto animate-state-halo rounded-full border [animation-delay:1.5s]',\n toneClasses.halo,\n sizeClasses.halo,\n )}\n />\n\n <span\n className={classNames(\n 'relative flex animate-state-float items-center justify-center border bg-gradient-to-br shadow-elevation3 glass-tint',\n toneClasses.tile,\n sizeClasses.tile,\n )}\n >\n <MaterialSymbol icon={icon} weight={300} className={toneClasses.icon} />\n </span>\n\n <span\n className={classNames(\n 'absolute right-[6%] top-[18%] size-1.5 animate-state-drift rounded-full',\n toneClasses.accent,\n )}\n />\n <span\n className={classNames(\n 'absolute bottom-[16%] left-[9%] size-1 animate-state-drift rounded-full [animation-delay:2.5s]',\n toneClasses.accent,\n )}\n />\n <span\n className={classNames(\n 'absolute left-[24%] top-[4%] size-1 animate-state-drift rounded-full [animation-delay:5s]',\n toneClasses.accent,\n )}\n />\n </span>\n);\n\nconst NoDataFoundActionButton: React.FC<{ action: INoDataFoundAction; isPrimary: boolean }> = ({\n action,\n isPrimary,\n}) => (\n <ControlledButton\n size=\"sm\"\n variant={isPrimary ? 'filled' : 'text'}\n tone={isPrimary ? 'brand' : 'neutral'}\n disabled={action.disabled}\n onClick={action.onClick}\n className={classNames(\n 'flex items-center gap-x-1.5 px-3.5 text-xs transition-interactive duration-base',\n isPrimary\n ? 'shadow-md shadow-brand/20 hover:-translate-y-px hover:shadow-lg hover:shadow-brand/25'\n : 'text-foreground-muted hover:bg-muted-hover',\n )}\n >\n {action.icon ? <MaterialSymbol icon={action.icon} size={16} aria-hidden /> : null}\n <span>{action.label}</span>\n </ControlledButton>\n);\n\nconst renderAction = (action: TNoDataFoundActionProp, isPrimary: boolean): React.ReactNode => {\n if (!action) return null;\n if (isActionConfig(action)) return <NoDataFoundActionButton action={action} isPrimary={isPrimary} />;\n\n return action;\n};\n\nconst NoDataFound: React.FC<INoDataFoundProps> = ({\n variant = 'empty',\n size = 'md',\n tone,\n title,\n message,\n description,\n icon,\n illustration,\n action,\n secondaryAction,\n compact = false,\n children,\n className,\n announce = true,\n}) => {\n const { t } = useTranslation('ui');\n\n const preset = NO_DATA_FOUND_VARIANTS[variant] ?? NO_DATA_FOUND_VARIANTS.empty;\n const toneClasses = NO_DATA_FOUND_TONES[tone ?? preset.tone] ?? NO_DATA_FOUND_TONES.neutral;\n const resolvedIcon = icon ?? preset.icon;\n const resolvedTitle = title ?? message ?? t(preset.titleKey);\n const liveRegionProps = announce ? { role: 'status', 'aria-live': 'polite' as const } : {};\n\n const primaryAction = renderAction(action, true);\n const supportAction = renderAction(secondaryAction, false);\n\n if (compact || size === 'inline') {\n return (\n <div {...liveRegionProps} className={twMerge('flex w-full items-center gap-x-1.5 text-left', className)}>\n <MaterialSymbol icon={resolvedIcon} size={16} className={classNames('shrink-0', toneClasses.icon)} />\n <span className=\"min-w-0 text-xs text-foreground-subtle\">{resolvedTitle}</span>\n\n {description ? (\n <>\n <span aria-hidden className=\"size-1 shrink-0 rounded-full bg-border-emphasis\" />\n <span className=\"min-w-0 truncate text-xs text-foreground-faint\">{description}</span>\n </>\n ) : null}\n\n {primaryAction ? <span className=\"ml-auto shrink-0\">{primaryAction}</span> : null}\n </div>\n );\n }\n\n const sizeClasses = NO_DATA_FOUND_SIZES[size] ?? NO_DATA_FOUND_SIZES.md;\n\n return (\n <div\n {...liveRegionProps}\n className={twMerge(\n classNames(\n 'relative flex w-full flex-col items-center justify-center overflow-hidden text-center @container',\n sizeClasses.root,\n ),\n className,\n )}\n >\n {sizeClasses.hasTexture ? (\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-0 [mask-image:radial-gradient(60%_55%_at_50%_35%,black,transparent)]\"\n >\n <span className=\"absolute inset-0 bg-[radial-gradient(circle,rgba(148,163,184,0.3)_1px,transparent_1px)] [background-size:1.25rem_1.25rem]\" />\n </span>\n ) : null}\n\n {illustration ?? <NoDataFoundVisual icon={resolvedIcon} toneClasses={toneClasses} sizeClasses={sizeClasses} />}\n\n <p\n className={classNames(\n 'relative animate-state-rise font-semibold tracking-[-0.01em] text-foreground [animation-delay:80ms]',\n sizeClasses.titleClass,\n )}\n >\n {resolvedTitle}\n </p>\n\n {description ? (\n <p\n className={classNames(\n 'relative animate-state-rise text-balance leading-relaxed text-foreground-subtle [animation-delay:140ms]',\n sizeClasses.descriptionClass,\n )}\n >\n {description}\n </p>\n ) : null}\n\n {children ? <div className=\"relative w-full animate-state-rise [animation-delay:180ms]\">{children}</div> : null}\n\n {primaryAction || supportAction ? (\n <div\n className={classNames(\n 'relative flex animate-state-rise flex-wrap items-center justify-center gap-2 [animation-delay:220ms]',\n sizeClasses.actions,\n )}\n >\n {primaryAction}\n {supportAction}\n </div>\n ) : null}\n </div>\n );\n};\n\nexport default memo(NoDataFound);\n"],"names":[],"mappings":"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"}
@@ -10,6 +10,9 @@ export interface IVoiceTranscriptionUpdate {
10
10
  insert: string;
11
11
  deleteWords: number;
12
12
  }
13
+ export declare const applyVoiceUpdate: (current: string, update: IVoiceTranscriptionUpdate) => string;
14
+ export declare const appendInterimTranscript: (committed: string, interim: string) => string;
15
+ export type TVoiceTranscriptionError = 'permission' | 'microphone' | 'network' | 'unavailable';
13
16
  interface IUseVoiceTranscriptionParams {
14
17
  lang?: string;
15
18
  onCommit: (update: IVoiceTranscriptionUpdate) => void;
@@ -18,6 +21,7 @@ interface IUseVoiceTranscriptionParams {
18
21
  interface IUseVoiceTranscriptionResult {
19
22
  isSupported: boolean;
20
23
  isListening: boolean;
24
+ error: TVoiceTranscriptionError | null;
21
25
  toggle: () => void;
22
26
  stop: () => void;
23
27
  }
@@ -45,34 +45,69 @@ const buildUpdateFromTranscript = (transcript) => {
45
45
  }
46
46
  return { insert, deleteWords };
47
47
  };
48
+ const appendTranscriptText = (current, addition) => {
49
+ if (!addition) return current;
50
+ const needsSpace = current.length > 0 && !/\s$/.test(current) && !/^[\s.,!?]/.test(addition);
51
+ return current + (needsSpace ? " " : "") + addition;
52
+ };
53
+ const applyVoiceUpdate = (current, update) => {
54
+ let next = current;
55
+ for (let wordIndex = 0; wordIndex < update.deleteWords; wordIndex += 1) {
56
+ next = next.replace(/\s*\S+\s*$/, "");
57
+ }
58
+ return appendTranscriptText(next, update.insert);
59
+ };
60
+ const appendInterimTranscript = (committed, interim) => appendTranscriptText(committed, interim);
61
+ const RECOGNITION_ERROR_TO_VOICE_ERROR = {
62
+ "not-allowed": "permission",
63
+ "service-not-allowed": "permission",
64
+ "audio-capture": "microphone",
65
+ network: "network",
66
+ "language-not-supported": "unavailable",
67
+ "bad-grammar": "unavailable"
68
+ };
69
+ const MAX_SILENT_RESTARTS = 2;
70
+ const detachRecognition = (recognition) => {
71
+ recognition.onresult = null;
72
+ recognition.onerror = null;
73
+ recognition.onend = null;
74
+ };
48
75
  const useVoiceTranscription = ({
49
76
  lang = "pt-BR",
50
77
  onCommit,
51
78
  onInterim
52
79
  }) => {
53
80
  const [isListening, setIsListening] = useState(false);
81
+ const [error, setError] = useState(null);
54
82
  const recognitionRef = useRef(null);
83
+ const wantsListeningRef = useRef(false);
84
+ const silentRestartCountRef = useRef(0);
55
85
  const onCommitRef = useRef(onCommit);
56
86
  const onInterimRef = useRef(onInterim);
57
87
  onCommitRef.current = onCommit;
58
88
  onInterimRef.current = onInterim;
59
89
  const isSupported = getRecognitionCtor() !== null;
60
90
  const stop = useCallback(() => {
91
+ wantsListeningRef.current = false;
61
92
  recognitionRef.current?.stop();
62
93
  }, []);
63
- const start = useCallback(() => {
94
+ const startSession = useCallback(() => {
64
95
  const Ctor = getRecognitionCtor();
65
- if (!Ctor) return;
96
+ if (!Ctor) return false;
66
97
  const recognition = new Ctor();
67
98
  recognition.lang = lang;
68
99
  recognition.continuous = true;
69
100
  recognition.interimResults = true;
101
+ let sessionHeardSpeech = false;
102
+ let sessionEndedByError = false;
70
103
  recognition.onresult = (event) => {
104
+ if (recognitionRef.current !== recognition) return;
105
+ sessionHeardSpeech = true;
71
106
  let interim = "";
72
- for (let i = event.resultIndex; i < event.results.length; i += 1) {
73
- const result = event.results[i];
74
- const transcript = result[0].transcript;
75
- if (result.isFinal) {
107
+ for (let resultIndex = event.resultIndex; resultIndex < event.results.length; resultIndex += 1) {
108
+ const result = event.results[resultIndex];
109
+ const transcript = result?.[0]?.transcript ?? "";
110
+ if (result?.isFinal) {
76
111
  onCommitRef.current(buildUpdateFromTranscript(transcript));
77
112
  } else {
78
113
  interim += transcript;
@@ -81,26 +116,66 @@ const useVoiceTranscription = ({
81
116
  onInterimRef.current?.(interim.trim());
82
117
  };
83
118
  recognition.onerror = (event) => {
119
+ if (recognitionRef.current !== recognition) return;
120
+ if (event.error === "no-speech") return;
121
+ sessionEndedByError = true;
122
+ if (event.error === "aborted") return;
84
123
  uiLogger.warn("[useVoiceTranscription] error", event.error);
85
- setIsListening(false);
124
+ setError(RECOGNITION_ERROR_TO_VOICE_ERROR[event.error] ?? "unavailable");
86
125
  };
87
126
  recognition.onend = () => {
127
+ if (recognitionRef.current !== recognition) return;
88
128
  onInterimRef.current?.("");
129
+ silentRestartCountRef.current = sessionHeardSpeech ? 0 : silentRestartCountRef.current + 1;
130
+ const shouldResume = wantsListeningRef.current && !sessionEndedByError && silentRestartCountRef.current <= MAX_SILENT_RESTARTS;
131
+ if (shouldResume && startSession()) return;
132
+ wantsListeningRef.current = false;
133
+ recognitionRef.current = null;
89
134
  setIsListening(false);
90
135
  };
91
136
  recognitionRef.current = recognition;
92
- recognition.start();
93
- setIsListening(true);
137
+ try {
138
+ recognition.start();
139
+ return true;
140
+ } catch (startError) {
141
+ uiLogger.warn("[useVoiceTranscription] start failed", startError);
142
+ detachRecognition(recognition);
143
+ recognitionRef.current = null;
144
+ setError("unavailable");
145
+ return false;
146
+ }
94
147
  }, [lang]);
148
+ const start = useCallback(() => {
149
+ setError(null);
150
+ silentRestartCountRef.current = 0;
151
+ wantsListeningRef.current = true;
152
+ if (startSession()) {
153
+ setIsListening(true);
154
+ return;
155
+ }
156
+ wantsListeningRef.current = false;
157
+ }, [startSession]);
95
158
  const toggle = useCallback(() => {
96
159
  if (isListening) stop();
97
160
  else start();
98
161
  }, [isListening, start, stop]);
99
- useEffect(() => () => recognitionRef.current?.stop(), []);
100
- return { isSupported, isListening, toggle, stop };
162
+ useEffect(
163
+ () => () => {
164
+ wantsListeningRef.current = false;
165
+ const recognition = recognitionRef.current;
166
+ if (!recognition) return;
167
+ detachRecognition(recognition);
168
+ recognitionRef.current = null;
169
+ recognition.abort();
170
+ },
171
+ []
172
+ );
173
+ return { isSupported, isListening, error, toggle, stop };
101
174
  };
102
175
  export {
103
176
  VOICE_COMMANDS,
177
+ appendInterimTranscript,
178
+ applyVoiceUpdate,
104
179
  useVoiceTranscription
105
180
  };
106
181
  //# sourceMappingURL=useVoiceTranscription.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useVoiceTranscription.js","sources":["../../src/hooks/useVoiceTranscription.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { uiLogger } from '@/helpers/uiLogger';\n\ninterface ISpeechRecognitionAlternative {\n transcript: string;\n}\n\ninterface ISpeechRecognitionResult {\n 0: ISpeechRecognitionAlternative;\n isFinal: boolean;\n}\n\ninterface ISpeechRecognitionEvent {\n resultIndex: number;\n results: { length: number; [index: number]: ISpeechRecognitionResult };\n}\n\ninterface ISpeechRecognition {\n lang: string;\n continuous: boolean;\n interimResults: boolean;\n start: () => void;\n stop: () => void;\n onresult: ((event: ISpeechRecognitionEvent) => void) | null;\n onerror: ((event: { error: string }) => void) | null;\n onend: (() => void) | null;\n}\n\ntype TSpeechRecognitionCtor = new () => ISpeechRecognition;\n\nconst getRecognitionCtor = (): TSpeechRecognitionCtor | null => {\n const w = window as unknown as {\n SpeechRecognition?: TSpeechRecognitionCtor;\n webkitSpeechRecognition?: TSpeechRecognitionCtor;\n };\n return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;\n};\n\nexport type TVoiceCommandKey = 'comma' | 'period' | 'newline' | 'space' | 'question' | 'exclamation' | 'deleteWord';\n\ninterface IVoiceCommand {\n key: TVoiceCommandKey;\n phrases: Array<string>;\n insert?: string;\n action?: 'deleteWord';\n}\n\n// Spoken keywords matched against the user's speech. They must stay as literal\n// recognition tokens (not UI strings), so the i18n check is disabled here.\n/* eslint-disable i18n-check/no-portuguese-strings */\nexport const VOICE_COMMANDS: Array<IVoiceCommand> = [\n { key: 'comma', phrases: ['vírgula', 'virgula', 'comma'], insert: ', ' },\n { key: 'period', phrases: ['ponto', 'ponto final', 'period'], insert: '. ' },\n { key: 'newline', phrases: ['nova linha', 'quebra de linha', 'parágrafo', 'paragrafo', 'new line'], insert: '\\n' },\n { key: 'space', phrases: ['espaço', 'espaco', 'space'], insert: ' ' },\n {\n key: 'question',\n phrases: ['interrogação', 'interrogacao', 'ponto de interrogação', 'question mark'],\n insert: '? ',\n },\n {\n key: 'exclamation',\n phrases: ['exclamação', 'exclamacao', 'ponto de exclamação', 'exclamation mark'],\n insert: '! ',\n },\n { key: 'deleteWord', phrases: ['apagar', 'apaga', 'apagar palavra', 'delete'], action: 'deleteWord' },\n];\n/* eslint-enable i18n-check/no-portuguese-strings */\n\nconst PHRASE_TO_COMMAND = new Map<string, IVoiceCommand>();\nfor (const command of VOICE_COMMANDS) {\n for (const phrase of command.phrases) PHRASE_TO_COMMAND.set(phrase, command);\n}\n\nconst normalize = (value: string): string =>\n value\n .trim()\n .toLowerCase()\n .replace(/[.,!?;:]+$/, '');\n\nexport interface IVoiceTranscriptionUpdate {\n insert: string;\n deleteWords: number;\n}\n\nconst buildUpdateFromTranscript = (transcript: string): IVoiceTranscriptionUpdate => {\n const words = transcript.trim().split(/\\s+/).filter(Boolean);\n let insert = '';\n let deleteWords = 0;\n\n for (const word of words) {\n const command = PHRASE_TO_COMMAND.get(normalize(word));\n if (command?.action === 'deleteWord') {\n deleteWords += 1;\n insert = '';\n continue;\n }\n if (command?.insert !== undefined) {\n insert = insert.replace(/\\s+$/, '') + command.insert;\n continue;\n }\n insert += (insert && !insert.endsWith(' ') && !insert.endsWith('\\n') ? ' ' : '') + word;\n }\n\n return { insert, deleteWords };\n};\n\ninterface IUseVoiceTranscriptionParams {\n lang?: string;\n onCommit: (update: IVoiceTranscriptionUpdate) => void;\n onInterim?: (text: string) => void;\n}\n\ninterface IUseVoiceTranscriptionResult {\n isSupported: boolean;\n isListening: boolean;\n toggle: () => void;\n stop: () => void;\n}\n\nexport const useVoiceTranscription = ({\n lang = 'pt-BR',\n onCommit,\n onInterim,\n}: IUseVoiceTranscriptionParams): IUseVoiceTranscriptionResult => {\n const [isListening, setIsListening] = useState(false);\n const recognitionRef = useRef<ISpeechRecognition | null>(null);\n const onCommitRef = useRef(onCommit);\n const onInterimRef = useRef(onInterim);\n\n onCommitRef.current = onCommit;\n onInterimRef.current = onInterim;\n\n const isSupported = getRecognitionCtor() !== null;\n\n const stop = useCallback(() => {\n recognitionRef.current?.stop();\n }, []);\n\n const start = useCallback(() => {\n const Ctor = getRecognitionCtor();\n if (!Ctor) return;\n\n const recognition = new Ctor();\n recognition.lang = lang;\n recognition.continuous = true;\n recognition.interimResults = true;\n\n recognition.onresult = (event) => {\n let interim = '';\n for (let i = event.resultIndex; i < event.results.length; i += 1) {\n const result = event.results[i];\n const transcript = result[0].transcript;\n if (result.isFinal) {\n onCommitRef.current(buildUpdateFromTranscript(transcript));\n } else {\n interim += transcript;\n }\n }\n onInterimRef.current?.(interim.trim());\n };\n\n recognition.onerror = (event) => {\n uiLogger.warn('[useVoiceTranscription] error', event.error);\n setIsListening(false);\n };\n\n recognition.onend = () => {\n onInterimRef.current?.('');\n setIsListening(false);\n };\n\n recognitionRef.current = recognition;\n recognition.start();\n setIsListening(true);\n }, [lang]);\n\n const toggle = useCallback(() => {\n if (isListening) stop();\n else start();\n }, [isListening, start, stop]);\n\n useEffect(() => () => recognitionRef.current?.stop(), []);\n\n return { isSupported, isListening, toggle, stop };\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useVoiceTranscription.js","sources":["../../src/hooks/useVoiceTranscription.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { uiLogger } from '@/helpers/uiLogger';\n\ninterface ISpeechRecognitionAlternative {\n transcript: string;\n}\n\ninterface ISpeechRecognitionResult {\n 0: ISpeechRecognitionAlternative;\n isFinal: boolean;\n}\n\ninterface ISpeechRecognitionEvent {\n resultIndex: number;\n results: { length: number; [index: number]: ISpeechRecognitionResult };\n}\n\ninterface ISpeechRecognition {\n lang: string;\n continuous: boolean;\n interimResults: boolean;\n start: () => void;\n stop: () => void;\n abort: () => void;\n onresult: ((event: ISpeechRecognitionEvent) => void) | null;\n onerror: ((event: { error: string }) => void) | null;\n onend: (() => void) | null;\n}\n\ntype TSpeechRecognitionCtor = new () => ISpeechRecognition;\n\nconst getRecognitionCtor = (): TSpeechRecognitionCtor | null => {\n const w = window as unknown as {\n SpeechRecognition?: TSpeechRecognitionCtor;\n webkitSpeechRecognition?: TSpeechRecognitionCtor;\n };\n return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;\n};\n\nexport type TVoiceCommandKey = 'comma' | 'period' | 'newline' | 'space' | 'question' | 'exclamation' | 'deleteWord';\n\ninterface IVoiceCommand {\n key: TVoiceCommandKey;\n phrases: Array<string>;\n insert?: string;\n action?: 'deleteWord';\n}\n\n// Spoken keywords matched against the user's speech. They must stay as literal\n// recognition tokens (not UI strings), so the i18n check is disabled here.\n/* eslint-disable i18n-check/no-portuguese-strings */\nexport const VOICE_COMMANDS: Array<IVoiceCommand> = [\n { key: 'comma', phrases: ['vírgula', 'virgula', 'comma'], insert: ', ' },\n { key: 'period', phrases: ['ponto', 'ponto final', 'period'], insert: '. ' },\n { key: 'newline', phrases: ['nova linha', 'quebra de linha', 'parágrafo', 'paragrafo', 'new line'], insert: '\\n' },\n { key: 'space', phrases: ['espaço', 'espaco', 'space'], insert: ' ' },\n {\n key: 'question',\n phrases: ['interrogação', 'interrogacao', 'ponto de interrogação', 'question mark'],\n insert: '? ',\n },\n {\n key: 'exclamation',\n phrases: ['exclamação', 'exclamacao', 'ponto de exclamação', 'exclamation mark'],\n insert: '! ',\n },\n { key: 'deleteWord', phrases: ['apagar', 'apaga', 'apagar palavra', 'delete'], action: 'deleteWord' },\n];\n/* eslint-enable i18n-check/no-portuguese-strings */\n\nconst PHRASE_TO_COMMAND = new Map<string, IVoiceCommand>();\nfor (const command of VOICE_COMMANDS) {\n for (const phrase of command.phrases) PHRASE_TO_COMMAND.set(phrase, command);\n}\n\nconst normalize = (value: string): string =>\n value\n .trim()\n .toLowerCase()\n .replace(/[.,!?;:]+$/, '');\n\nexport interface IVoiceTranscriptionUpdate {\n insert: string;\n deleteWords: number;\n}\n\nconst buildUpdateFromTranscript = (transcript: string): IVoiceTranscriptionUpdate => {\n const words = transcript.trim().split(/\\s+/).filter(Boolean);\n let insert = '';\n let deleteWords = 0;\n\n for (const word of words) {\n const command = PHRASE_TO_COMMAND.get(normalize(word));\n if (command?.action === 'deleteWord') {\n deleteWords += 1;\n insert = '';\n continue;\n }\n if (command?.insert !== undefined) {\n insert = insert.replace(/\\s+$/, '') + command.insert;\n continue;\n }\n insert += (insert && !insert.endsWith(' ') && !insert.endsWith('\\n') ? ' ' : '') + word;\n }\n\n return { insert, deleteWords };\n};\n\nconst appendTranscriptText = (current: string, addition: string): string => {\n if (!addition) return current;\n const needsSpace = current.length > 0 && !/\\s$/.test(current) && !/^[\\s.,!?]/.test(addition);\n return current + (needsSpace ? ' ' : '') + addition;\n};\n\nexport const applyVoiceUpdate = (current: string, update: IVoiceTranscriptionUpdate): string => {\n let next = current;\n for (let wordIndex = 0; wordIndex < update.deleteWords; wordIndex += 1) {\n next = next.replace(/\\s*\\S+\\s*$/, '');\n }\n return appendTranscriptText(next, update.insert);\n};\n\nexport const appendInterimTranscript = (committed: string, interim: string): string =>\n appendTranscriptText(committed, interim);\n\nexport type TVoiceTranscriptionError = 'permission' | 'microphone' | 'network' | 'unavailable';\n\nconst RECOGNITION_ERROR_TO_VOICE_ERROR: Record<string, TVoiceTranscriptionError> = {\n 'not-allowed': 'permission',\n 'service-not-allowed': 'permission',\n 'audio-capture': 'microphone',\n network: 'network',\n 'language-not-supported': 'unavailable',\n 'bad-grammar': 'unavailable',\n};\n\nconst MAX_SILENT_RESTARTS = 2;\n\ninterface IUseVoiceTranscriptionParams {\n lang?: string;\n onCommit: (update: IVoiceTranscriptionUpdate) => void;\n onInterim?: (text: string) => void;\n}\n\ninterface IUseVoiceTranscriptionResult {\n isSupported: boolean;\n isListening: boolean;\n error: TVoiceTranscriptionError | null;\n toggle: () => void;\n stop: () => void;\n}\n\nconst detachRecognition = (recognition: ISpeechRecognition) => {\n recognition.onresult = null;\n recognition.onerror = null;\n recognition.onend = null;\n};\n\nexport const useVoiceTranscription = ({\n lang = 'pt-BR',\n onCommit,\n onInterim,\n}: IUseVoiceTranscriptionParams): IUseVoiceTranscriptionResult => {\n const [isListening, setIsListening] = useState(false);\n const [error, setError] = useState<TVoiceTranscriptionError | null>(null);\n const recognitionRef = useRef<ISpeechRecognition | null>(null);\n const wantsListeningRef = useRef(false);\n const silentRestartCountRef = useRef(0);\n const onCommitRef = useRef(onCommit);\n const onInterimRef = useRef(onInterim);\n\n onCommitRef.current = onCommit;\n onInterimRef.current = onInterim;\n\n const isSupported = getRecognitionCtor() !== null;\n\n const stop = useCallback(() => {\n wantsListeningRef.current = false;\n recognitionRef.current?.stop();\n }, []);\n\n const startSession = useCallback((): boolean => {\n const Ctor = getRecognitionCtor();\n if (!Ctor) return false;\n\n const recognition = new Ctor();\n recognition.lang = lang;\n recognition.continuous = true;\n recognition.interimResults = true;\n\n let sessionHeardSpeech = false;\n let sessionEndedByError = false;\n\n recognition.onresult = (event) => {\n if (recognitionRef.current !== recognition) return;\n sessionHeardSpeech = true;\n let interim = '';\n for (let resultIndex = event.resultIndex; resultIndex < event.results.length; resultIndex += 1) {\n const result = event.results[resultIndex];\n const transcript = result?.[0]?.transcript ?? '';\n if (result?.isFinal) {\n onCommitRef.current(buildUpdateFromTranscript(transcript));\n } else {\n interim += transcript;\n }\n }\n onInterimRef.current?.(interim.trim());\n };\n\n recognition.onerror = (event) => {\n if (recognitionRef.current !== recognition) return;\n if (event.error === 'no-speech') return;\n sessionEndedByError = true;\n if (event.error === 'aborted') return;\n uiLogger.warn('[useVoiceTranscription] error', event.error);\n setError(RECOGNITION_ERROR_TO_VOICE_ERROR[event.error] ?? 'unavailable');\n };\n\n recognition.onend = () => {\n if (recognitionRef.current !== recognition) return;\n onInterimRef.current?.('');\n silentRestartCountRef.current = sessionHeardSpeech ? 0 : silentRestartCountRef.current + 1;\n const shouldResume =\n wantsListeningRef.current && !sessionEndedByError && silentRestartCountRef.current <= MAX_SILENT_RESTARTS;\n if (shouldResume && startSession()) return;\n wantsListeningRef.current = false;\n recognitionRef.current = null;\n setIsListening(false);\n };\n\n recognitionRef.current = recognition;\n try {\n recognition.start();\n return true;\n } catch (startError) {\n uiLogger.warn('[useVoiceTranscription] start failed', startError);\n detachRecognition(recognition);\n recognitionRef.current = null;\n setError('unavailable');\n return false;\n }\n }, [lang]);\n\n const start = useCallback(() => {\n setError(null);\n silentRestartCountRef.current = 0;\n wantsListeningRef.current = true;\n if (startSession()) {\n setIsListening(true);\n return;\n }\n wantsListeningRef.current = false;\n }, [startSession]);\n\n const toggle = useCallback(() => {\n if (isListening) stop();\n else start();\n }, [isListening, start, stop]);\n\n useEffect(\n () => () => {\n wantsListeningRef.current = false;\n const recognition = recognitionRef.current;\n if (!recognition) return;\n detachRecognition(recognition);\n recognitionRef.current = null;\n recognition.abort();\n },\n [],\n );\n\n return { isSupported, isListening, error, toggle, stop };\n};\n"],"names":[],"mappings":"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"}
@@ -502,7 +502,13 @@ declare const _default: {
502
502
  "voiceInput": {
503
503
  "start": "Transcribe by voice",
504
504
  "stop": "Stop transcription",
505
- "commandsHint": "Voice commands: say {{commands}} to insert punctuation or delete the last word."
505
+ "commandsHint": "Voice commands: say {{commands}} to insert punctuation or delete the last word.",
506
+ "errors": {
507
+ "permission": "Microphone access was blocked. Allow the microphone in your browser permissions and try again.",
508
+ "microphone": "No microphone was found. Connect a microphone and try again.",
509
+ "network": "The browser's transcription service did not respond. Check your connection or use Google Chrome or Microsoft Edge.",
510
+ "unavailable": "Voice transcription is not available in this browser. Use Google Chrome or Microsoft Edge."
511
+ }
506
512
  }
507
513
  }
508
514
  ;
@@ -32,7 +32,7 @@ const unifiedSearchBar = { "clearFilter": "Clear filter" };
32
32
  const uploader = { "dragOr": "Drag or", "selectFile": "select a file", "orPaste": "or paste (Ctrl+V)", "allowedFormats": "Allowed formats:", "upTo": "up to", "megabytesSuffix": "MB", "sendFiles": "Send file(s)", "filesIgnoredMaxSize": "Some files were ignored because they exceeded the maximum size of {{size}}MB", "invalidFormat": "Please add files in the allowed formats", "allowedFormatsTooltip": "Allowed formats: {{formats}} up to {{size}}MB" };
33
33
  const urlLinkCell = { "clickToAccess": "Click to access" };
34
34
  const userMenu = { "loading": "Loading...", "yourAccounts": "Your Accounts", "searchAccount": "Search account", "noAccounts": "No active accounts available", "logout": "Sign out" };
35
- const voiceInput = { "start": "Transcribe by voice", "stop": "Stop transcription", "commandsHint": "Voice commands: say {{commands}} to insert punctuation or delete the last word." };
35
+ const voiceInput = { "start": "Transcribe by voice", "stop": "Stop transcription", "commandsHint": "Voice commands: say {{commands}} to insert punctuation or delete the last word.", "errors": { "permission": "Microphone access was blocked. Allow the microphone in your browser permissions and try again.", "microphone": "No microphone was found. Connect a microphone and try again.", "network": "The browser's transcription service did not respond. Check your connection or use Google Chrome or Microsoft Edge.", "unavailable": "Voice transcription is not available in this browser. Use Google Chrome or Microsoft Edge." } };
36
36
  const en = {
37
37
  accordion,
38
38
  actions,
@@ -505,6 +505,12 @@ export declare const uiLocales: {
505
505
  start: string;
506
506
  stop: string;
507
507
  commandsHint: string;
508
+ errors: {
509
+ permission: string;
510
+ microphone: string;
511
+ network: string;
512
+ unavailable: string;
513
+ };
508
514
  };
509
515
  };
510
516
  readonly pt: {
@@ -1012,6 +1018,12 @@ export declare const uiLocales: {
1012
1018
  start: string;
1013
1019
  stop: string;
1014
1020
  commandsHint: string;
1021
+ errors: {
1022
+ permission: string;
1023
+ microphone: string;
1024
+ network: string;
1025
+ unavailable: string;
1026
+ };
1015
1027
  };
1016
1028
  };
1017
1029
  };
@@ -502,7 +502,13 @@ declare const _default: {
502
502
  "voiceInput": {
503
503
  "start": "Transcrever por voz",
504
504
  "stop": "Parar transcrição",
505
- "commandsHint": "Comandos de voz: diga {{commands}} para inserir pontuação ou apagar a última palavra."
505
+ "commandsHint": "Comandos de voz: diga {{commands}} para inserir pontuação ou apagar a última palavra.",
506
+ "errors": {
507
+ "permission": "O acesso ao microfone foi bloqueado. Libere o microfone nas permissões do navegador e tente de novo.",
508
+ "microphone": "Nenhum microfone foi encontrado. Conecte um microfone e tente de novo.",
509
+ "network": "O serviço de transcrição do navegador não respondeu. Verifique a conexão ou use o Google Chrome ou o Microsoft Edge.",
510
+ "unavailable": "A transcrição por voz não está disponível neste navegador. Use o Google Chrome ou o Microsoft Edge."
511
+ }
506
512
  }
507
513
  }
508
514
  ;
@@ -32,7 +32,7 @@ const unifiedSearchBar = { "clearFilter": "Limpar Filtro" };
32
32
  const uploader = { "dragOr": "Arraste ou", "selectFile": "selecione um arquivo", "orPaste": "ou cole (Ctrl+V)", "allowedFormats": "Formatos permitidos:", "upTo": "até", "megabytesSuffix": "MB", "sendFiles": "Enviar arquivo(s)", "filesIgnoredMaxSize": "Alguns arquivos foram ignorados porque excederam o tamanho máximo de {{size}}MB", "invalidFormat": "Por favor, insira arquivos nos formatos permitidos", "allowedFormatsTooltip": "Formatos permitidos: {{formats}} até {{size}}MB" };
33
33
  const urlLinkCell = { "clickToAccess": "Clique para acessar" };
34
34
  const userMenu = { "loading": "Carregando...", "yourAccounts": "Suas Contas", "searchAccount": "Buscar conta", "noAccounts": "Nenhuma conta ativa disponível", "logout": "Sair" };
35
- const voiceInput = { "start": "Transcrever por voz", "stop": "Parar transcrição", "commandsHint": "Comandos de voz: diga {{commands}} para inserir pontuação ou apagar a última palavra." };
35
+ const voiceInput = { "start": "Transcrever por voz", "stop": "Parar transcrição", "commandsHint": "Comandos de voz: diga {{commands}} para inserir pontuação ou apagar a última palavra.", "errors": { "permission": "O acesso ao microfone foi bloqueado. Libere o microfone nas permissões do navegador e tente de novo.", "microphone": "Nenhum microfone foi encontrado. Conecte um microfone e tente de novo.", "network": "O serviço de transcrição do navegador não respondeu. Verifique a conexão ou use o Google Chrome ou o Microsoft Edge.", "unavailable": "A transcrição por voz não está disponível neste navegador. Use o Google Chrome ou o Microsoft Edge." } };
36
36
  const pt = {
37
37
  accordion,
38
38
  actions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [