analytica-frontend-lib 1.7.20 → 1.7.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/ActivitiesHistory/index.js +6 -6
  2. package/dist/ActivitiesHistory/index.mjs +6 -6
  3. package/dist/ActivityCardQuestionBanks/index.js +6 -6
  4. package/dist/ActivityCardQuestionBanks/index.mjs +6 -6
  5. package/dist/ActivityCardQuestionPreview/index.js +6 -6
  6. package/dist/ActivityCardQuestionPreview/index.mjs +6 -6
  7. package/dist/ActivityCreate/ActivityCreateContent/index.js +6 -6
  8. package/dist/ActivityCreate/ActivityCreateContent/index.mjs +6 -6
  9. package/dist/ActivityCreate/index.js +6 -6
  10. package/dist/ActivityCreate/index.mjs +6 -6
  11. package/dist/ActivityDetails/index.js +6 -6
  12. package/dist/ActivityDetails/index.mjs +6 -6
  13. package/dist/ActivityFilters/index.js +6 -6
  14. package/dist/ActivityFilters/index.mjs +6 -6
  15. package/dist/ActivityPageLayout/index.js +6 -6
  16. package/dist/ActivityPageLayout/index.mjs +6 -6
  17. package/dist/ActivityPreview/index.js +6 -6
  18. package/dist/ActivityPreview/index.mjs +6 -6
  19. package/dist/AlertManager/index.js +6 -6
  20. package/dist/AlertManager/index.mjs +6 -6
  21. package/dist/CheckBoxGroup/index.js +6 -6
  22. package/dist/CheckBoxGroup/index.mjs +6 -6
  23. package/dist/EnemMoment/index.js +6 -6
  24. package/dist/EnemMoment/index.mjs +6 -6
  25. package/dist/EnemMomentStudentModal/index.js +6 -6
  26. package/dist/EnemMomentStudentModal/index.mjs +6 -6
  27. package/dist/ExamDetailsLayout/ExamDetailsPage/index.js +6 -6
  28. package/dist/ExamDetailsLayout/ExamDetailsPage/index.mjs +6 -6
  29. package/dist/ExamPageLayout/index.js +6 -6
  30. package/dist/ExamPageLayout/index.mjs +6 -6
  31. package/dist/KnowledgePerformanceCard/index.js +6 -6
  32. package/dist/KnowledgePerformanceCard/index.mjs +6 -6
  33. package/dist/LessonCardPreview/index.js +6 -6
  34. package/dist/LessonCardPreview/index.mjs +6 -6
  35. package/dist/LessonMediaSections/index.js +6 -6
  36. package/dist/LessonMediaSections/index.mjs +6 -6
  37. package/dist/LessonsCatalog/index.js +6 -6
  38. package/dist/LessonsCatalog/index.mjs +6 -6
  39. package/dist/PrintableUsersTable/index.js +6 -6
  40. package/dist/PrintableUsersTable/index.mjs +6 -6
  41. package/dist/QuestionsPerformanceCard/index.js +6 -6
  42. package/dist/QuestionsPerformanceCard/index.mjs +6 -6
  43. package/dist/RecommendedLessonCreate/index.js +6 -6
  44. package/dist/RecommendedLessonCreate/index.mjs +6 -6
  45. package/dist/RecommendedLessonsHistory/index.js +6 -6
  46. package/dist/RecommendedLessonsHistory/index.mjs +6 -6
  47. package/dist/SendActivityModal/SendActivityModal.js +6 -6
  48. package/dist/SendActivityModal/SendActivityModal.mjs +6 -6
  49. package/dist/SendActivityModal/index.js +6 -6
  50. package/dist/SendActivityModal/index.mjs +6 -6
  51. package/dist/SendLessonModal/index.js +6 -6
  52. package/dist/SendLessonModal/index.mjs +6 -6
  53. package/dist/SimulatedPerformance/SimulatedPerformanceView/index.js +6 -6
  54. package/dist/SimulatedPerformance/SimulatedPerformanceView/index.mjs +6 -6
  55. package/dist/SimulationsPage/index.js +6 -6
  56. package/dist/SimulationsPage/index.mjs +6 -6
  57. package/dist/Support/TicketModal/index.js +3 -3
  58. package/dist/Support/TicketModal/index.mjs +2 -2
  59. package/dist/Support/index.js +4 -4
  60. package/dist/Support/index.mjs +3 -3
  61. package/dist/SupportChatWidget/index.js +3 -3
  62. package/dist/SupportChatWidget/index.mjs +2 -2
  63. package/dist/SupportUserPanel/index.js +3 -3
  64. package/dist/SupportUserPanel/index.mjs +2 -2
  65. package/dist/TableProvider/index.js +6 -6
  66. package/dist/TableProvider/index.mjs +6 -6
  67. package/dist/UnifiedDraftModelPage/index.js +6 -6
  68. package/dist/UnifiedDraftModelPage/index.mjs +6 -6
  69. package/dist/UnifiedHistoryPage/index.js +6 -6
  70. package/dist/UnifiedHistoryPage/index.mjs +6 -6
  71. package/dist/{chunk-NP6KVOAV.js → chunk-64WFAQC6.js} +13 -2
  72. package/dist/chunk-64WFAQC6.js.map +1 -0
  73. package/dist/{chunk-56PRQ24B.mjs → chunk-7LS5UQCQ.mjs} +7 -5
  74. package/dist/chunk-7LS5UQCQ.mjs.map +1 -0
  75. package/dist/{chunk-PH2DDIS4.mjs → chunk-B4QODMQ5.mjs} +1 -1
  76. package/dist/{chunk-PH2DDIS4.mjs.map → chunk-B4QODMQ5.mjs.map} +1 -1
  77. package/dist/{chunk-TVWATS3U.mjs → chunk-C4SBX6R2.mjs} +11 -4
  78. package/dist/chunk-C4SBX6R2.mjs.map +1 -0
  79. package/dist/{chunk-FLGSHGJY.js → chunk-CYSODZGX.js} +8 -2
  80. package/dist/chunk-CYSODZGX.js.map +1 -0
  81. package/dist/{chunk-OMBS4ACE.mjs → chunk-MVT7IVMY.mjs} +12 -1
  82. package/dist/chunk-MVT7IVMY.mjs.map +1 -0
  83. package/dist/{chunk-KBWT73XY.mjs → chunk-OES54VU6.mjs} +8 -2
  84. package/dist/{chunk-KBWT73XY.mjs.map → chunk-OES54VU6.mjs.map} +1 -1
  85. package/dist/{chunk-BWTNSVJE.js → chunk-OGD2ATAP.js} +1 -1
  86. package/dist/{chunk-BWTNSVJE.js.map → chunk-OGD2ATAP.js.map} +1 -1
  87. package/dist/{chunk-DXA4VBUN.mjs → chunk-VKQ5AJDT.mjs} +2 -2
  88. package/dist/{chunk-HE66Y2T3.js → chunk-VQ7MSYIH.js} +5 -5
  89. package/dist/{chunk-HE66Y2T3.js.map → chunk-VQ7MSYIH.js.map} +1 -1
  90. package/dist/{chunk-IMMKWOYJ.js → chunk-WUXIPIUG.js} +42 -35
  91. package/dist/chunk-WUXIPIUG.js.map +1 -0
  92. package/dist/{chunk-SQDLSJBL.js → chunk-YRTIDZTN.js} +15 -13
  93. package/dist/chunk-YRTIDZTN.js.map +1 -0
  94. package/dist/components/Support/Support.d.ts +4 -2
  95. package/dist/components/Support/Support.d.ts.map +1 -1
  96. package/dist/components/SupportChatWidget/guestSupportChatAdapter.d.ts.map +1 -1
  97. package/dist/components/SupportUserPanel/SupportHistoryTab.d.ts.map +1 -1
  98. package/dist/components/SupportUserPanel/SupportTicketDetailsTab.d.ts.map +1 -1
  99. package/dist/hooks/useAppContent/index.js +6 -6
  100. package/dist/hooks/useAppContent/index.mjs +6 -6
  101. package/dist/hooks/useSupportFeatureFlag/index.js +1 -1
  102. package/dist/hooks/useSupportFeatureFlag/index.mjs +1 -1
  103. package/dist/index.d.ts +2 -2
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +11 -7
  106. package/dist/index.js.map +1 -1
  107. package/dist/index.mjs +10 -6
  108. package/dist/types/support/index.d.ts +20 -0
  109. package/dist/types/support/index.d.ts.map +1 -1
  110. package/dist/types/support/index.js +6 -2
  111. package/dist/types/support/index.js.map +1 -1
  112. package/dist/types/support/index.mjs +5 -1
  113. package/dist/types/supportUserPanel/index.d.ts +7 -0
  114. package/dist/types/supportUserPanel/index.d.ts.map +1 -1
  115. package/dist/types/supportUserPanel/index.js +2 -2
  116. package/dist/types/supportUserPanel/index.mjs +1 -1
  117. package/dist/types/supportUserPanel.d.ts +7 -0
  118. package/dist/types/supportUserPanel.d.ts.map +1 -1
  119. package/package.json +1 -1
  120. package/dist/chunk-56PRQ24B.mjs.map +0 -1
  121. package/dist/chunk-FLGSHGJY.js.map +0 -1
  122. package/dist/chunk-IMMKWOYJ.js.map +0 -1
  123. package/dist/chunk-NP6KVOAV.js.map +0 -1
  124. package/dist/chunk-OMBS4ACE.mjs.map +0 -1
  125. package/dist/chunk-SQDLSJBL.js.map +0 -1
  126. package/dist/chunk-TVWATS3U.mjs.map +0 -1
  127. /package/dist/{chunk-DXA4VBUN.mjs.map → chunk-VKQ5AJDT.mjs.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/SupportUserPanel/SupportUserPanel.tsx","../src/components/SupportUserPanel/SupportProfileTab.tsx","../src/components/SupportUserPanel/SupportPanelShared.tsx","../src/components/SupportUserPanel/supportUserPanelUtils.ts","../src/components/SupportUserPanel/SupportHistoryTab.tsx","../src/components/SupportUserPanel/SupportTicketDetailsTab.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { Menu, MenuContent, MenuItem } from '../Menu/Menu';\nimport { cn } from '../../utils/utils';\nimport {\n SUPPORT_USER_PANEL_TAB,\n type SupportUserPanelTab,\n} from '../../types/supportUserPanel';\nimport SupportProfileTab, {\n type SupportProfileTabProps,\n} from './SupportProfileTab';\nimport SupportHistoryTab, {\n type SupportHistoryTabProps,\n} from './SupportHistoryTab';\nimport SupportTicketDetailsTab, {\n type SupportTicketDetailsTabProps,\n} from './SupportTicketDetailsTab';\n\nconst TABS: { value: SupportUserPanelTab; label: string }[] = [\n { value: SUPPORT_USER_PANEL_TAB.PROFILE, label: 'Perfil' },\n { value: SUPPORT_USER_PANEL_TAB.HISTORY, label: 'Histórico' },\n { value: SUPPORT_USER_PANEL_TAB.TICKET, label: 'Detalhes' },\n];\n\n/** Props for the support user panel */\nexport interface SupportUserPanelProps {\n /** Controlled tab. Omit to let the panel keep it (default: ticket details) */\n tab?: SupportUserPanelTab;\n /**\n * Notified on tab change. Keep the panel mounted (or control `tab`) while\n * the agent moves between tickets so the chosen tab persists.\n */\n onTabChange?: (tab: SupportUserPanelTab) => void;\n /** Enables Alt+1 / Alt+2 / Alt+3 to switch tabs (default: true) */\n enableShortcuts?: boolean;\n profile: SupportProfileTabProps;\n history: SupportHistoryTabProps;\n ticket: SupportTicketDetailsTabProps;\n className?: string;\n}\n\n/**\n * Right-side panel of the agent's ticket screen (300px). Shows the contact\n * of the open conversation and the current ticket in three tabs: Perfil,\n * Histórico and Detalhes (the current ticket).\n *\n * Presentational: data comes in through props and every change goes out\n * through callbacks. Saves made from the ticket tab are optimistic, with a\n * \"Salvo\" toast, or a rollback and an error toast when the callback rejects.\n */\nexport default function SupportUserPanel({\n tab,\n onTabChange,\n enableShortcuts = true,\n profile,\n history,\n ticket,\n className,\n}: Readonly<SupportUserPanelProps>) {\n const [internalTab, setInternalTab] = useState<SupportUserPanelTab>(\n SUPPORT_USER_PANEL_TAB.TICKET\n );\n const active = tab ?? internalTab;\n\n const changeTab = (next: SupportUserPanelTab) => {\n if (tab === undefined) setInternalTab(next);\n onTabChange?.(next);\n };\n\n useEffect(() => {\n if (!enableShortcuts) return undefined;\n const handleKeyDown = (event: KeyboardEvent) => {\n if (!event.altKey || event.ctrlKey || event.metaKey) return;\n // `code` instead of `key`: on macOS Alt+1 types \"¡\", not \"1\".\n const index = ['Digit1', 'Digit2', 'Digit3'].indexOf(event.code);\n if (index === -1) return;\n event.preventDefault();\n const next = TABS[index].value;\n if (tab === undefined) setInternalTab(next);\n onTabChange?.(next);\n };\n globalThis.addEventListener('keydown', handleKeyDown);\n return () => globalThis.removeEventListener('keydown', handleKeyDown);\n }, [enableShortcuts, onTabChange, tab]);\n\n return (\n <aside\n aria-label=\"Painel do usuário\"\n className={cn(\n 'flex h-full w-[300px] shrink-0 flex-col border-l border-border-100 bg-background',\n className\n )}\n >\n <div className=\"shrink-0 border-b border-border-100 px-4\">\n <Menu\n defaultValue={SUPPORT_USER_PANEL_TAB.TICKET}\n value={active}\n variant=\"menu2\"\n semantics=\"tabs\"\n onValueChange={(value) => changeTab(value as SupportUserPanelTab)}\n className=\"!py-0\"\n >\n {/* Equal widths (`w-full`) leave ~89px per tab in 300px. Each tab\n takes the width of its label and the leftover is shared, so\n longer labels still fit on one line. */}\n <MenuContent\n variant=\"menu2\"\n className=\"!gap-1 [&>li]:!w-auto [&>li]:flex-auto [&>li]:!px-0\"\n >\n {TABS.map((item) => (\n <MenuItem\n key={item.value}\n value={item.value}\n variant=\"menu2\"\n aria-controls={`support-user-panel-${item.value}`}\n className=\"!px-0 !text-[13px] whitespace-nowrap\"\n >\n {item.label}\n </MenuItem>\n ))}\n </MenuContent>\n </Menu>\n </div>\n\n <div\n id={`support-user-panel-${active}`}\n role=\"tabpanel\"\n aria-label={TABS.find((item) => item.value === active)?.label}\n className=\"min-h-0 flex-1 overflow-y-auto p-4\"\n >\n {active === SUPPORT_USER_PANEL_TAB.PROFILE && (\n <SupportProfileTab {...profile} />\n )}\n {active === SUPPORT_USER_PANEL_TAB.HISTORY && (\n <SupportHistoryTab {...history} />\n )}\n {active === SUPPORT_USER_PANEL_TAB.TICKET && (\n <SupportTicketDetailsTab {...ticket} />\n )}\n </div>\n </aside>\n );\n}\n","import { useId, useState } from 'react';\nimport Avatar from '../Avatar/Avatar';\nimport Button from '../Button/Button';\nimport Text from '../Text/Text';\nimport TextArea from '../TextArea/TextArea';\nimport { Skeleton, SkeletonText } from '../Skeleton/Skeleton';\nimport useToastStore from '../Toast/utils/ToastStore';\nimport { cn } from '../../utils/utils';\nimport type { SupportContact } from '../../types/supportUserPanel';\nimport { FieldLabel } from './SupportPanelShared';\nimport {\n formatDate,\n formatPhone,\n formatRelativeDay,\n} from './supportUserPanelUtils';\n\n/** Props for the profile tab */\nexport interface SupportProfileTabProps {\n contact?: SupportContact | null;\n loading?: boolean;\n /** Opens \"Usuários / Contatos – Detalhe do contato\" */\n onOpenContact?: () => void;\n /** Starts linking an unlinked contact to a platform user */\n onLinkContact?: () => void;\n /** Persists a note; reject to keep the text in the box */\n onAddNote?: (text: string) => Promise<void> | void;\n}\n\nconst linkClasses =\n 'underline underline-offset-2 rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indicator-info';\n\n/** Read-only field; clicking copies the value */\nconst CopyField = ({ label, value }: { label: string; value?: string }) => {\n const addToast = useToastStore((state) => state.addToast);\n const labelId = useId();\n\n const copy = async () => {\n if (!value) return;\n try {\n await navigator.clipboard.writeText(value);\n addToast({ title: 'Copiado', action: 'success', position: 'top-right' });\n } catch {\n addToast({\n title: 'Não foi possível copiar',\n action: 'warning',\n position: 'top-right',\n });\n }\n };\n\n return (\n <div className=\"flex flex-col gap-1.5\">\n <FieldLabel id={labelId}>{label}</FieldLabel>\n {value ? (\n <Button\n variant=\"raw\"\n onClick={copy}\n aria-labelledby={labelId}\n aria-describedby={`${labelId}-value`}\n title=\"Clique para copiar\"\n className=\"w-full rounded-lg border border-border-300 bg-background px-3 py-2.5 text-left hover:border-border-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indicator-info\"\n >\n <Text\n as=\"span\"\n id={`${labelId}-value`}\n size=\"sm\"\n color=\"text-text-950\"\n className=\"block truncate\"\n >\n {value}\n </Text>\n </Button>\n ) : (\n <div\n aria-labelledby={labelId}\n className=\"w-full rounded-lg border border-border-300 bg-background px-3 py-2.5\"\n >\n <Text as=\"span\" size=\"sm\" color=\"text-text-400\">\n —\n </Text>\n </div>\n )}\n </div>\n );\n};\n\nconst ProfileSkeleton = () => (\n <div\n className=\"flex flex-col gap-4\"\n aria-busy=\"true\"\n aria-label=\"Carregando perfil\"\n >\n <div className=\"flex items-center gap-3\">\n <Skeleton variant=\"circular\" width={44} height={44} />\n <div className=\"flex flex-1 flex-col gap-1.5\">\n <SkeletonText width=\"60%\" />\n <SkeletonText width=\"80%\" />\n </div>\n </div>\n <Skeleton variant=\"rounded\" height={40} />\n <Skeleton variant=\"rounded\" height={40} />\n <SkeletonText lines={5} spacing=\"medium\" />\n </div>\n);\n\n/**\n * Profile tab: contact header, link badge, e-mail/phone, key/value facts\n * and the team notes about the user.\n */\nexport default function SupportProfileTab({\n contact,\n loading = false,\n onOpenContact,\n onLinkContact,\n onAddNote,\n}: Readonly<SupportProfileTabProps>) {\n const [note, setNote] = useState('');\n const [savingNote, setSavingNote] = useState(false);\n const addToast = useToastStore((state) => state.addToast);\n\n if (loading || !contact) return <ProfileSkeleton />;\n\n const canAddNote =\n Boolean(onAddNote) && note.trim().length > 0 && !savingNote;\n\n const submitNote = async () => {\n if (!canAddNote || !onAddNote) return;\n setSavingNote(true);\n try {\n await onAddNote(note.trim());\n setNote('');\n } catch {\n // The text stays in the box so the agent can retry.\n addToast({\n title: 'Não foi possível adicionar a nota',\n action: 'warning',\n position: 'top-right',\n });\n } finally {\n setSavingNote(false);\n }\n };\n\n const facts: [string, string][] = [\n ['Turma / cargo', contact.classOrPosition || '—'],\n ['Instituição', contact.institution || '—'],\n ['Primeiro contato', formatDate(contact.firstContactAt)],\n ['Último contato', formatRelativeDay(contact.lastContactAt)],\n ['Total de atendimentos', String(contact.ticketsCount)],\n ['Dispositivo', contact.device || '—'],\n ['Canal preferido', contact.preferredChannel],\n ];\n\n const notes = [...contact.notes].sort(\n (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()\n );\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-center gap-3\">\n <Avatar\n src={contact.avatarUrl}\n name={contact.name}\n size={44}\n fallback=\"initials\"\n />\n <div className=\"flex min-w-0 flex-col\">\n <Text\n size=\"sm\"\n weight=\"bold\"\n color=\"text-text-950\"\n className=\"truncate\"\n >\n {contact.name}\n </Text>\n <Text size=\"xs\" color=\"text-text-600\" className=\"truncate\">\n {contact.role} · {contact.institution}\n </Text>\n </div>\n </div>\n\n {contact.linked ? (\n <Text\n size=\"xs\"\n color=\"text-success-800\"\n className=\"rounded-lg border border-success-200 bg-success-background px-3 py-2\"\n >\n ✓ Vinculado à plataforma\n {onOpenContact && (\n <>\n {' · '}\n <Button\n variant=\"raw\"\n onClick={onOpenContact}\n className={linkClasses}\n >\n ver cadastro\n </Button>\n </>\n )}\n </Text>\n ) : (\n <Text\n size=\"xs\"\n color=\"text-warning-800\"\n className=\"rounded-lg bg-warning-background px-3 py-2\"\n >\n Usuário não vinculado\n {onLinkContact && (\n <>\n {' · '}\n <Button\n variant=\"raw\"\n onClick={onLinkContact}\n className={linkClasses}\n >\n vincular\n </Button>\n </>\n )}\n </Text>\n )}\n\n <CopyField label=\"E-mail\" value={contact.email ?? undefined} />\n <CopyField\n label=\"Telefone\"\n value={contact.phone ? formatPhone(contact.phone) : undefined}\n />\n\n <dl className=\"flex flex-col\">\n {facts.map(([key, value]) => (\n <div\n key={key}\n className=\"flex items-baseline justify-between gap-3 border-b border-border-100 py-2 last:border-b-0\"\n >\n <Text as=\"dt\" size=\"xs\" color=\"text-text-600\">\n {key}\n </Text>\n <Text\n as=\"dd\"\n size=\"xs\"\n weight=\"medium\"\n color=\"text-text-950\"\n className=\"text-right\"\n >\n {value}\n </Text>\n </div>\n ))}\n </dl>\n\n <hr className=\"border-border-100\" />\n\n <div className=\"flex flex-col gap-2\">\n <FieldLabel>Notas sobre o usuário</FieldLabel>\n <TextArea\n value={note}\n onChange={(event) => setNote(event.target.value)}\n placeholder=\"Escreva uma nota visível para a equipe\"\n aria-label=\"Nota sobre o usuário\"\n size=\"small\"\n className=\"!h-16 min-h-16\"\n disabled={!onAddNote}\n />\n <div className=\"flex justify-end\">\n <Button\n size=\"small\"\n className=\"rounded-full\"\n onClick={submitNote}\n disabled={!canAddNote}\n >\n Adicionar nota\n </Button>\n </div>\n {notes.length > 0 && (\n <ul\n aria-label=\"Notas sobre o usuário\"\n className=\"flex flex-col gap-2\"\n >\n {notes.map((item) => (\n <li\n key={item.id}\n className={cn(\n 'flex flex-col gap-1 rounded-lg border border-warning-200 bg-exam-3 px-3 py-2'\n )}\n >\n <Text\n size=\"xs\"\n color=\"text-text-950\"\n className=\"whitespace-pre-line\"\n >\n {item.text}\n </Text>\n <Text size=\"2xs\" color=\"text-warning-800\">\n {item.author} · {formatDate(item.createdAt)}\n </Text>\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n );\n}\n","import { useCallback, useEffect, useState, type ReactNode } from 'react';\nimport Text from '../Text/Text';\nimport useToastStore from '../Toast/utils/ToastStore';\n\n/** Grey stat tile: small muted label over a bold value */\nexport const StatTile = ({\n label,\n value,\n valueSize = 'sm',\n}: {\n label: string;\n value: ReactNode;\n /** `sm` (14px) on the ticket grid, `md` (16px) on the history summary */\n valueSize?: 'sm' | 'md';\n}) => (\n <div className=\"flex min-w-0 flex-col gap-0.5 rounded-lg bg-background-100 px-3 py-2.5\">\n <Text size=\"xs\" color=\"text-text-600\" className=\"truncate\">\n {label}\n </Text>\n <Text\n size={valueSize}\n weight=\"bold\"\n color=\"text-text-950\"\n className=\"truncate\"\n >\n {value}\n </Text>\n </div>\n);\n\n/** Section title used above fields (13px / 600) */\nexport const FieldLabel = ({\n children,\n id,\n}: {\n children: ReactNode;\n id?: string;\n}) => (\n <Text id={id} size=\"sm\" weight=\"semibold\" color=\"text-text-950\">\n {children}\n </Text>\n);\n\n/**\n * Optimistic auto-save: applies the value right away, calls `save`, shows\n * \"Salvo\" on success and rolls back with an error toast on failure.\n *\n * @param value - Value coming from props (source of truth after a save)\n * @param save - Persists the new value; reject to trigger the rollback\n * @returns The value to render and the setter that saves it\n */\nexport function useOptimisticValue<T>(\n value: T,\n save: ((next: T) => Promise<void> | void) | undefined\n): [T, (next: T) => Promise<void>] {\n const [current, setCurrent] = useState(value);\n const addToast = useToastStore((state) => state.addToast);\n\n useEffect(() => {\n setCurrent(value);\n }, [value]);\n\n const update = useCallback(\n async (next: T) => {\n if (!save) return;\n const previous = current;\n setCurrent(next);\n try {\n await save(next);\n addToast({ title: 'Salvo', action: 'success', position: 'top-right' });\n } catch {\n setCurrent(previous);\n addToast({\n title: 'Não foi possível salvar',\n description: 'A alteração foi desfeita. Tente novamente.',\n action: 'warning',\n position: 'top-right',\n });\n }\n },\n [addToast, current, save]\n );\n\n return [current, update];\n}\n\n/**\n * Current time, refreshed every `intervalMs` while `active` — drives the\n * live wait/total counters of an open ticket.\n */\nexport function useNow(active: boolean, intervalMs = 30_000): Date {\n const [now, setNow] = useState(() => new Date());\n\n useEffect(() => {\n if (!active) return undefined;\n setNow(new Date());\n const id = setInterval(() => setNow(new Date()), intervalMs);\n return () => clearInterval(id);\n }, [active, intervalMs]);\n\n return now;\n}\n","import {\n SUPPORT_HISTORY_FILTER,\n SUPPORT_TICKET_CARD_STATUS,\n type SupportHistoryFilter,\n type SupportTicketSummary,\n} from '../../types/supportUserPanel';\n\nconst pad = (value: number) => String(value).padStart(2, '0');\n\nconst isSameDay = (a: Date, b: Date) =>\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate();\n\nconst isYesterday = (date: Date, now: Date) => {\n const yesterday = new Date(now);\n yesterday.setDate(now.getDate() - 1);\n return isSameDay(date, yesterday);\n};\n\n/** `dd/mm/aaaa` */\nexport const formatDate = (iso: string): string => {\n const date = new Date(iso);\n return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()}`;\n};\n\n/** `HH:MM` */\nexport const formatTime = (iso: string): string => {\n const date = new Date(iso);\n return `${pad(date.getHours())}:${pad(date.getMinutes())}`;\n};\n\n/** `Hoje`, `Ontem` or `dd/mm/aaaa` — used for the contact's last contact */\nexport const formatRelativeDay = (iso: string, now = new Date()): string => {\n const date = new Date(iso);\n if (isSameDay(date, now)) return 'Hoje';\n if (isYesterday(date, now)) return 'Ontem';\n return formatDate(iso);\n};\n\n/** `Hoje, HH:MM` or `dd/mm/aaaa` — history cards */\nexport const formatCardDate = (iso: string, now = new Date()): string =>\n isSameDay(new Date(iso), now) ? `Hoje, ${formatTime(iso)}` : formatDate(iso);\n\n/** `Hoje, HH:MM` or `dd/mm, HH:MM` — \"Aberto em\" stat */\nexport const formatOpenedAt = (iso: string, now = new Date()): string => {\n const date = new Date(iso);\n if (isSameDay(date, now)) return `Hoje, ${formatTime(iso)}`;\n return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}, ${formatTime(iso)}`;\n};\n\n/** `HH:MM` for today, `dd/mm HH:MM` otherwise — status timeline */\nexport const formatTimelineTime = (iso: string, now = new Date()): string => {\n const date = new Date(iso);\n if (isSameDay(date, now)) return formatTime(iso);\n return `${pad(date.getDate())}/${pad(date.getMonth() + 1)} ${formatTime(iso)}`;\n};\n\n/** `(DD) 9XXXX-XXXX`; values that are not a BR number are returned as given */\nexport const formatPhone = (phone: string): string => {\n const digits = phone.replace(/\\D/g, '');\n if (digits.length === 11) {\n return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7)}`;\n }\n if (digits.length === 10) {\n return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6)}`;\n }\n return phone;\n};\n\n/** `18 min`, `1h 05min` */\nexport const formatDuration = (minutes: number): string => {\n const total = Math.max(0, Math.floor(minutes));\n if (total < 60) return `${total} min`;\n return `${Math.floor(total / 60)}h ${pad(total % 60)}min`;\n};\n\n/** Whole minutes between two ISO dates (or between a date and `now`) */\nexport const minutesBetween = (\n fromIso: string,\n toIso: string | null | undefined,\n now = new Date()\n): number => {\n const to = toIso ? new Date(toIso) : now;\n return Math.max(\n 0,\n Math.floor((to.getTime() - new Date(fromIso).getTime()) / 60_000)\n );\n};\n\n/** Whether a ticket card matches the selected history filter */\nexport const matchesHistoryFilter = (\n ticket: SupportTicketSummary,\n filter: SupportHistoryFilter\n): boolean => {\n const done = ticket.status === SUPPORT_TICKET_CARD_STATUS.DONE;\n switch (filter) {\n case SUPPORT_HISTORY_FILTER.OPEN:\n return !done;\n case SUPPORT_HISTORY_FILTER.DONE:\n return done;\n case SUPPORT_HISTORY_FILTER.AI:\n return done && ticket.resolvedBy?.type === 'ai';\n default:\n return true;\n }\n};\n","import { useId, useState } from 'react';\nimport { CaretDownIcon } from '@phosphor-icons/react/dist/csr/CaretDown';\nimport Badge from '../Badge/Badge';\nimport Button from '../Button/Button';\nimport Chips from '../Chips/Chips';\nimport Text from '../Text/Text';\nimport { Skeleton } from '../Skeleton/Skeleton';\nimport { cn } from '../../utils/utils';\nimport {\n SUPPORT_HISTORY_FILTER,\n SUPPORT_TICKET_CARD_STATUS,\n SUPPORT_TICKET_PRIORITY,\n type SupportHistoryFilter,\n type SupportHistorySummary,\n type SupportTicketCardStatus,\n type SupportTicketId,\n type SupportTicketPriority,\n type SupportTicketSummary,\n} from '../../types/supportUserPanel';\nimport { StatTile } from './SupportPanelShared';\nimport {\n formatCardDate,\n formatDuration,\n matchesHistoryFilter,\n} from './supportUserPanelUtils';\n\n/** Cards shown before \"Ver todos os atendimentos\" */\nconst INITIAL_VISIBLE = 5;\n/** Cards added by each \"Ver todos os atendimentos\" click */\nconst PAGE_SIZE = 10;\n\n/** Props for the history tab */\nexport interface SupportHistoryTabProps {\n /** Id of the ticket open in the conversation (pinned on top, not clickable) */\n currentTicketId: SupportTicketId;\n /** Every ticket of the contact, including the current one */\n tickets: SupportTicketSummary[];\n summary: SupportHistorySummary;\n loading?: boolean;\n /** Whether the server has tickets beyond `tickets` */\n hasMore?: boolean;\n /** Asks the consumer for the next page once the loaded cards run out */\n onLoadMore?: () => void;\n /** Opens another ticket in the central area */\n onOpenTicket?: (ticketId: SupportTicketId) => void;\n /** Notified on filter change (e.g. to filter on the server) */\n onFilterChange?: (filter: SupportHistoryFilter) => void;\n}\n\nconst FILTERS: { value: SupportHistoryFilter; label: string }[] = [\n { value: SUPPORT_HISTORY_FILTER.ALL, label: 'Todos' },\n { value: SUPPORT_HISTORY_FILTER.OPEN, label: 'Abertos' },\n { value: SUPPORT_HISTORY_FILTER.DONE, label: 'Concluídos' },\n { value: SUPPORT_HISTORY_FILTER.AI, label: 'IA' },\n];\n\n// `warning` rather than `orange` for the human statuses: the `orange-*`\n// colors are not theme tokens, so they would not follow dark mode.\nconst STATUS_BADGE: Record<\n SupportTicketCardStatus,\n { label: string; action: 'info' | 'warning' | 'success' }\n> = {\n [SUPPORT_TICKET_CARD_STATUS.IN_PROGRESS]: {\n label: 'EM ANDAMENTO',\n action: 'info',\n },\n [SUPPORT_TICKET_CARD_STATUS.WAITING_HUMAN]: {\n label: 'AGUARDANDO HUMANO',\n action: 'warning',\n },\n [SUPPORT_TICKET_CARD_STATUS.HUMAN]: {\n label: 'HUMANO',\n action: 'warning',\n },\n [SUPPORT_TICKET_CARD_STATUS.DONE]: {\n label: 'CONCLUÍDO',\n action: 'success',\n },\n};\n\nconst resolverLabel = (ticket: SupportTicketSummary) => {\n if (!ticket.resolvedBy) return null;\n return ticket.resolvedBy.type === 'ai' ? 'IA' : ticket.resolvedBy.name;\n};\n\nconst PRIORITY_LABELS: Record<SupportTicketPriority, string> = {\n [SUPPORT_TICKET_PRIORITY.LOW]: 'Baixa',\n [SUPPORT_TICKET_PRIORITY.NORMAL]: 'Normal',\n [SUPPORT_TICKET_PRIORITY.HIGH]: 'Alta',\n [SUPPORT_TICKET_PRIORITY.URGENT]: 'Urgente',\n};\n\n/**\n * Ticket of the history list. The primary info is always visible; the\n * chevron expands the secondary details (priority, channel, duration) and\n * the link that opens the ticket.\n */\nconst TicketCard = ({\n ticket,\n current,\n onOpen,\n}: {\n ticket: SupportTicketSummary;\n current: boolean;\n onOpen?: () => void;\n}) => {\n const [expanded, setExpanded] = useState(false);\n const detailsId = useId();\n const badge = STATUS_BADGE[ticket.status];\n const resolver = current ? 'Você' : resolverLabel(ticket);\n const meta = [\n formatCardDate(ticket.createdAt),\n ticket.reason,\n ticket.subreason,\n ]\n .filter(Boolean)\n .join(' · ');\n\n const details: [string, string][] = [];\n if (ticket.priority) {\n details.push(['Prioridade', PRIORITY_LABELS[ticket.priority]]);\n }\n const channel = [ticket.channel, ticket.origin].filter(Boolean).join(' · ');\n if (channel) details.push(['Canal', channel]);\n if (current) details.push(['Duração', 'Ticket atual']);\n else if (ticket.durationMin != null) {\n details.push(['Duração', formatDuration(ticket.durationMin)]);\n }\n const canOpen = !current && Boolean(onOpen);\n const expandable = details.length > 0 || canOpen;\n\n return (\n <div\n aria-current={current ? 'true' : undefined}\n className={cn(\n 'flex w-full flex-col gap-1.5 rounded-xl border border-border-200 bg-background px-3 py-2.5 hover:border-primary-100'\n )}\n >\n <div className=\"flex items-center justify-between gap-2\">\n {/* The current ticket is marked by color only (no background). */}\n <Text\n size=\"xs\"\n weight={current ? 'semibold' : undefined}\n color={current ? 'text-primary-800' : 'text-text-600'}\n >\n {ticket.code ?? `#${ticket.id}`}\n </Text>\n <div className=\"flex shrink-0 items-center gap-1\">\n <Badge\n variant=\"solid\"\n action={badge.action}\n size=\"small\"\n className=\"font-bold\"\n >\n {badge.label}\n </Badge>\n {expandable && (\n <Button\n variant=\"raw\"\n onClick={() => setExpanded((open) => !open)}\n aria-expanded={expanded}\n aria-controls={detailsId}\n aria-label={`${expanded ? 'Recolher' : 'Expandir'} ticket ${ticket.code ?? `#${ticket.id}`}`}\n className=\"rounded-sm p-0.5 text-text-600 hover:text-text-950 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indicator-info\"\n >\n <CaretDownIcon\n size={14}\n aria-hidden=\"true\"\n className={cn('transition-transform', expanded && 'rotate-180')}\n />\n </Button>\n )}\n </div>\n </div>\n <Text\n size=\"sm\"\n weight=\"semibold\"\n color=\"text-text-950\"\n className=\"line-clamp-2\"\n >\n {ticket.title}\n </Text>\n <Text size=\"xs\" color=\"text-text-600\">\n {meta}\n </Text>\n {resolver && (\n <Text size=\"xs\" color=\"text-text-600\">\n {current ? 'Atendido por: ' : 'Resolvido por: '}\n <Text as=\"span\" size=\"xs\" weight=\"medium\" color=\"text-text-950\">\n {resolver}\n </Text>\n </Text>\n )}\n {expandable && expanded && (\n <div\n id={detailsId}\n className=\"flex flex-col gap-1.5 border-t border-border-100 pt-2\"\n >\n {details.length > 0 && (\n <dl className=\"flex flex-col gap-1\">\n {details.map(([key, value]) => (\n <div\n key={key}\n className=\"flex items-baseline justify-between gap-3\"\n >\n <Text as=\"dt\" size=\"xs\" color=\"text-text-600\">\n {key}\n </Text>\n <Text as=\"dd\" size=\"xs\" weight=\"medium\" color=\"text-text-950\">\n {value}\n </Text>\n </div>\n ))}\n </dl>\n )}\n {canOpen && (\n <Button\n variant=\"link\"\n size=\"extra-small\"\n onClick={onOpen}\n className=\"self-start p-0! font-medium\"\n >\n Abrir atendimento\n </Button>\n )}\n </div>\n )}\n </div>\n );\n};\n\n/**\n * History tab: summary stats, single-choice filter and the contact's\n * tickets, current one pinned on top.\n */\nexport default function SupportHistoryTab({\n currentTicketId,\n tickets,\n summary,\n loading = false,\n hasMore = false,\n onLoadMore,\n onOpenTicket,\n onFilterChange,\n}: Readonly<SupportHistoryTabProps>) {\n const [filter, setFilter] = useState<SupportHistoryFilter>(\n SUPPORT_HISTORY_FILTER.ALL\n );\n const [visible, setVisible] = useState(INITIAL_VISIBLE);\n\n const selectFilter = (next: SupportHistoryFilter) => {\n setFilter(next);\n setVisible(INITIAL_VISIBLE);\n onFilterChange?.(next);\n };\n\n const current = tickets.find((ticket) => ticket.id === currentTicketId);\n const others = tickets\n .filter((ticket) => ticket.id !== currentTicketId)\n .sort(\n (a, b) =>\n new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()\n );\n const ordered = [...(current ? [current] : []), ...others].filter((ticket) =>\n matchesHistoryFilter(ticket, filter)\n );\n const shown = ordered.slice(0, visible);\n const canShowMore = ordered.length > visible || hasMore;\n const isFirstContact = !loading && tickets.length <= 1;\n\n const showMore = () => {\n if (ordered.length <= visible + PAGE_SIZE && hasMore) onLoadMore?.();\n setVisible((count) => count + PAGE_SIZE);\n };\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"grid grid-cols-3 gap-2\">\n <StatTile label=\"Tickets\" value={summary.total} valueSize=\"md\" />\n <StatTile label=\"Resolvidos\" value={summary.resolved} valueSize=\"md\" />\n <StatTile\n label=\"Média\"\n value={summary.avgMin == null ? '—' : formatDuration(summary.avgMin)}\n valueSize=\"md\"\n />\n </div>\n\n <div\n role=\"radiogroup\"\n aria-label=\"Filtrar tickets\"\n className=\"flex flex-wrap gap-1.5\"\n >\n {FILTERS.map((item) => (\n <Chips\n key={item.value}\n role=\"radio\"\n aria-checked={filter === item.value}\n selected={filter === item.value}\n onClick={() => selectFilter(item.value)}\n className=\"px-3 py-1 text-xs\"\n >\n {item.label}\n </Chips>\n ))}\n </div>\n\n {loading ? (\n <div\n className=\"flex flex-col gap-2\"\n aria-busy=\"true\"\n aria-label=\"Carregando histórico\"\n >\n <Skeleton variant=\"rounded\" height={96} />\n <Skeleton variant=\"rounded\" height={96} />\n <Skeleton variant=\"rounded\" height={96} />\n </div>\n ) : (\n <>\n {shown.length === 0 ? (\n <Text size=\"xs\" color=\"text-text-600\" className=\"text-center\">\n Nenhum ticket com esse filtro\n </Text>\n ) : (\n <ul aria-label=\"Tickets do usuário\" className=\"flex flex-col gap-2\">\n {shown.map((ticket) => {\n const isCurrent = ticket.id === currentTicketId;\n return (\n <li key={ticket.id}>\n <TicketCard\n ticket={ticket}\n current={isCurrent}\n onOpen={\n onOpenTicket ? () => onOpenTicket(ticket.id) : undefined\n }\n />\n </li>\n );\n })}\n </ul>\n )}\n\n {isFirstContact && filter === SUPPORT_HISTORY_FILTER.ALL && (\n <Text size=\"xs\" color=\"text-text-600\" className=\"text-center\">\n Primeiro atendimento deste usuário\n </Text>\n )}\n\n {canShowMore && (\n <Button\n variant=\"link\"\n size=\"extra-small\"\n onClick={showMore}\n className=\"self-center p-0! font-medium\"\n >\n Ver todos os atendimentos\n </Button>\n )}\n </>\n )}\n </div>\n );\n}\n","import { useId } from 'react';\nimport Select, {\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../Select/Select';\nimport Text from '../Text/Text';\nimport { Skeleton } from '../Skeleton/Skeleton';\nimport SupportTimelineItem from '../SupportTimeline/SupportTimelineItem';\nimport {\n SUPPORT_TICKET_PRIORITY,\n type SupportReasonOption,\n type SupportTicketDetails,\n type SupportTicketPriority,\n} from '../../types/supportUserPanel';\nimport {\n FieldLabel,\n StatTile,\n useNow,\n useOptimisticValue,\n} from './SupportPanelShared';\nimport {\n formatDuration,\n formatOpenedAt,\n formatTimelineTime,\n minutesBetween,\n} from './supportUserPanelUtils';\n\n/** Props for the ticket details tab */\nexport interface SupportTicketDetailsTabProps {\n ticket?: SupportTicketDetails | null;\n loading?: boolean;\n /** Reason catalog (motivos / FAQ) */\n reasonOptions: SupportReasonOption[];\n /** Persists the reason; reject to roll back */\n onReasonChange?: (reason: string) => Promise<void> | void;\n /** Persists the priority; reject to roll back */\n onPriorityChange?: (priority: SupportTicketPriority) => Promise<void> | void;\n /**\n * Current status of the ticket (e.g. \"EM_ANDAMENTO\"). With `statusOptions`\n * the tab shows a Status select on top; it stays editable on a concluded\n * ticket, so the agent can reopen it.\n */\n status?: string;\n statusOptions?: SupportReasonOption[];\n /** Persists the status; reject to roll back */\n onStatusChange?: (status: string) => Promise<void> | void;\n /** Who handles the ticket (e.g. \"HUMANO\"); shown with `handlerOptions` */\n handler?: string;\n handlerOptions?: SupportReasonOption[];\n /** Persists who handles the ticket; reject to roll back */\n onHandlerChange?: (handler: string) => Promise<void> | void;\n}\n\n/** Labelled select of the tab, with optimistic save and rollback */\nconst OptionSelect = ({\n label,\n value,\n options,\n onChange,\n disabled,\n placeholder,\n}: {\n label: string;\n value: string;\n options: SupportReasonOption[];\n onChange: (value: string) => void;\n disabled: boolean;\n placeholder?: string;\n}) => {\n const labelId = useId();\n return (\n <div className=\"flex flex-col gap-1.5\">\n <FieldLabel id={labelId}>{label}</FieldLabel>\n <Select\n value={value}\n onValueChange={(next) => {\n if (next !== value) onChange(next);\n }}\n >\n <SelectTrigger\n aria-labelledby={labelId}\n disabled={disabled}\n className=\"w-full\"\n >\n <SelectValue placeholder={placeholder} />\n </SelectTrigger>\n <SelectContent>\n {options.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n );\n};\n\nconst PRIORITIES: { value: SupportTicketPriority; label: string }[] = [\n { value: SUPPORT_TICKET_PRIORITY.LOW, label: 'Baixa' },\n { value: SUPPORT_TICKET_PRIORITY.NORMAL, label: 'Normal' },\n { value: SUPPORT_TICKET_PRIORITY.HIGH, label: 'Alta' },\n { value: SUPPORT_TICKET_PRIORITY.URGENT, label: 'Urgente' },\n];\n\nconst DetailsSkeleton = () => (\n <div\n className=\"flex flex-col gap-4\"\n aria-busy=\"true\"\n aria-label=\"Carregando ticket\"\n >\n <div className=\"grid grid-cols-2 gap-2\">\n {[0, 1, 2, 3].map((key) => (\n <Skeleton key={key} variant=\"rounded\" height={56} />\n ))}\n </div>\n <Skeleton variant=\"rounded\" height={40} />\n <Skeleton variant=\"rounded\" height={36} />\n <Skeleton variant=\"rounded\" height={88} />\n </div>\n);\n\n/**\n * Ticket details tab: channel/time stats, the optional status and \"Atendido\n * por\" selects, the editable reason and priority (read-only once the ticket\n * is concluded, except the status, which can reopen it), the AI summary and\n * the status timeline.\n */\nexport default function SupportTicketDetailsTab({\n ticket,\n loading = false,\n reasonOptions,\n onReasonChange,\n onPriorityChange,\n status,\n statusOptions,\n onStatusChange,\n handler,\n handlerOptions,\n onHandlerChange,\n}: Readonly<SupportTicketDetailsTabProps>) {\n const reasonLabelId = useId();\n const priorityLabelId = useId();\n const done = ticket?.done ?? false;\n const now = useNow(Boolean(ticket) && !done);\n const [reason, setReason] = useOptimisticValue(\n ticket?.reason ?? '',\n onReasonChange\n );\n const [statusValue, setStatusValue] = useOptimisticValue(\n status ?? '',\n onStatusChange\n );\n const [handlerValue, setHandlerValue] = useOptimisticValue(\n handler ?? '',\n onHandlerChange\n );\n const [priority, setPriority] = useOptimisticValue<SupportTicketPriority>(\n ticket?.priority ?? SUPPORT_TICKET_PRIORITY.NORMAL,\n onPriorityChange\n );\n\n if (loading || !ticket) return <DetailsSkeleton />;\n\n // Wait = opening → first human reply; it keeps running while nobody has\n // answered yet. Total = opening → conclusion (or now while open).\n const waitEnd = ticket.firstHumanAt ?? ticket.closedAt;\n const waitingMin = minutesBetween(ticket.openedAt, waitEnd, now);\n const totalMin = minutesBetween(ticket.openedAt, ticket.closedAt, now);\n const readOnly = done;\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"grid grid-cols-2 gap-2\">\n <StatTile\n label=\"Canal\"\n value={\n ticket.origin\n ? `${ticket.channel} · ${ticket.origin}`\n : ticket.channel\n }\n />\n <StatTile label=\"Aberto em\" value={formatOpenedAt(ticket.openedAt)} />\n <StatTile label=\"Tempo de espera\" value={formatDuration(waitingMin)} />\n <StatTile label=\"Tempo total\" value={formatDuration(totalMin)} />\n </div>\n\n {statusOptions && statusOptions.length > 0 && (\n <OptionSelect\n label=\"Status\"\n value={statusValue}\n options={statusOptions}\n onChange={(value) => void setStatusValue(value)}\n disabled={!onStatusChange}\n />\n )}\n\n {handlerOptions && handlerOptions.length > 0 && (\n <OptionSelect\n label=\"Atendido por\"\n value={handlerValue}\n options={handlerOptions}\n onChange={(value) => void setHandlerValue(value)}\n disabled={readOnly || !onHandlerChange}\n />\n )}\n\n <div className=\"flex flex-col gap-1.5\">\n <FieldLabel id={reasonLabelId}>Motivo</FieldLabel>\n <Select value={reason} onValueChange={setReason}>\n <SelectTrigger\n aria-labelledby={reasonLabelId}\n disabled={readOnly || !onReasonChange}\n className=\"w-full\"\n >\n <SelectValue placeholder=\"Não definido\" />\n </SelectTrigger>\n <SelectContent>\n {reasonOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <FieldLabel id={priorityLabelId}>Prioridade</FieldLabel>\n <Select\n value={priority}\n onValueChange={(value) => {\n if (value !== priority) {\n setPriority(value as SupportTicketPriority);\n }\n }}\n >\n <SelectTrigger\n aria-labelledby={priorityLabelId}\n disabled={readOnly || !onPriorityChange}\n className=\"w-full\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {PRIORITIES.map((item) => (\n <SelectItem key={item.value} value={item.value}>\n {item.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <section\n aria-label=\"Resumo da IA\"\n className=\"flex flex-col gap-1.5 rounded-xl border border-info-100 bg-info-background p-3\"\n >\n <Text\n as=\"h3\"\n size=\"xs\"\n weight=\"bold\"\n color=\"text-info-800\"\n className=\"uppercase\"\n >\n <span aria-hidden=\"true\">✦ </span>Resumo da IA\n </Text>\n <Text\n size=\"xs\"\n color={ticket.aiSummary ? 'text-text-950' : 'text-text-600'}\n className=\"leading-[1.45]\"\n >\n {ticket.aiSummary ||\n 'A IA ainda não gerou um resumo para este ticket.'}\n </Text>\n </section>\n\n {ticket.timeline.length > 0 && (\n <ol aria-label=\"Linha do tempo de status\" className=\"flex flex-col\">\n {ticket.timeline.map((event) => (\n <li key={`${event.status}-${event.at}`}>\n <SupportTimelineItem\n status={event.status}\n time={formatTimelineTime(event.at)}\n />\n </li>\n ))}\n </ol>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["useEffect","useState","useState","jsx","jsxs","useState","useId","useState","Fragment","jsx","jsxs","useState","useId","useId","jsx","jsxs","useId","jsx","jsxs","useState","useEffect"]}
@@ -23,6 +23,9 @@ var _chunkBIKQB5M6js = require('./chunk-BIKQB5M6.js');
23
23
  var _chunkPOT3ALY3js = require('./chunk-POT3ALY3.js');
24
24
 
25
25
 
26
+ var _chunk64WFAQC6js = require('./chunk-64WFAQC6.js');
27
+
28
+
26
29
  var _chunkVVO3W2SMjs = require('./chunk-VVO3W2SM.js');
27
30
 
28
31
 
@@ -1112,6 +1115,7 @@ var createGuestSupportChatAdapter = ({
1112
1115
  return {
1113
1116
  async startConversation({
1114
1117
  message,
1118
+ channel,
1115
1119
  institutionId,
1116
1120
  requesterName,
1117
1121
  email,
@@ -1122,7 +1126,9 @@ var createGuestSupportChatAdapter = ({
1122
1126
  description,
1123
1127
  ...institutionId ? { institutionId } : {},
1124
1128
  ...requesterName ? { requesterName } : {},
1125
- ...email ? { email } : {}
1129
+ ...email ? { email } : {},
1130
+ // The app the chat runs in, when it is one the API knows
1131
+ ..._chunk64WFAQC6js.isSupportTicketOrigin.call(void 0, channel) ? { origin: channel } : {}
1126
1132
  });
1127
1133
  const ticketId = data.data.id;
1128
1134
  sessions.set(ticketId, {
@@ -1427,4 +1433,4 @@ var createMockSupportChatAdapter = ({
1427
1433
 
1428
1434
 
1429
1435
  exports.useSupportChat = useSupportChat; exports.SupportChatWidget = SupportChatWidget; exports.fromSupportAnswer = fromSupportAnswer; exports.createGuestSupportChatAdapter = createGuestSupportChatAdapter; exports.NativeSupportChat = NativeSupportChat; exports.createMockSupportChatAdapter = createMockSupportChatAdapter;
1430
- //# sourceMappingURL=chunk-FLGSHGJY.js.map
1436
+ //# sourceMappingURL=chunk-CYSODZGX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-CYSODZGX.js","../src/components/SupportChatWidget/SupportChatWidget.tsx","../src/hooks/useSupportChat.ts","../src/components/SupportChatWidget/SupportChatContactForm.tsx","../src/components/SupportChatWidget/SupportChatArticleView.tsx","../src/components/SupportChatWidget/supportChatContent.tsx","../src/components/SupportChatWidget/NativeSupportChat.tsx","../src/components/SupportChatWidget/guestSupportChatAdapter.ts","../src/components/SupportChatWidget/mockSupportChatAdapter.ts"],"names":["useState","jsxs","jsx","useCallback","useMemo"],"mappings":"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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-CYSODZGX.js","sourcesContent":[null,"import {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ChangeEvent,\n type KeyboardEvent,\n} from 'react';\nimport {\n ChatCircleDotsIcon,\n MinusIcon,\n PaperclipIcon,\n PaperPlaneTiltIcon,\n XIcon,\n} from '@phosphor-icons/react';\nimport Text from '../Text/Text';\nimport Button from '../Button/Button';\nimport { useSupportChat } from '../../hooks/useSupportChat';\nimport { SupportChatContactForm } from './SupportChatContactForm';\nimport { SupportChatArticleView } from './SupportChatArticleView';\nimport {\n AttachmentList,\n MessageText,\n PendingAttachment,\n SUPPORT_CHAT_ACCEPT,\n pickAttachments,\n} from './supportChatContent';\nimport { SupportType } from '../../types/support';\nimport {\n SUPPORT_CHAT_AUTHOR,\n SUPPORT_CHAT_MAX_ATTACHMENTS,\n type SupportChatAdapter,\n type SupportChatFlags,\n type SupportChatMessage,\n type SupportChatTheme,\n type SupportChatUser,\n} from '../../types/supportChat';\nimport { cn } from '../../utils/utils';\n\n/** Visibility of the chat window */\nexport type SupportChatWindowState = 'closed' | 'open' | 'minimized';\n\nexport interface SupportChatWidgetProps {\n /** Data layer (createMockSupportChatAdapter for now) */\n adapter: SupportChatAdapter;\n /** App the widget runs in, sent with the ticket (e.g. \"login\", \"backoffice\") */\n channel: string;\n institutionId?: string;\n /** Logged user; omit for visitors */\n user?: SupportChatUser;\n flags?: SupportChatFlags;\n theme?: SupportChatTheme;\n onOpen?: () => void;\n onSend?: (message: SupportChatMessage) => void;\n /**\n * Called when the window is closed with X. Closing only hides it: the\n * conversation stays connected and new replies count as unread.\n */\n onClose?: () => void;\n /**\n * Link of a FAQ article, e.g. `(id) => '/artigo/' + id`. With it, the\n * \"Baseado na FAQ\" line under an AI answer opens the article; without it\n * the line only names the article.\n */\n faqArticleUrl?: (articleId: string) => string;\n}\n\n/** Label read by screen readers on the unread badge of the launcher */\nconst formatNewMessagesLabel = (count: number): string =>\n count === 1 ? '1 nova mensagem' : `${count} novas mensagens`;\n\n/** The requester side of the conversation (user or visitor) */\nconst isOwnMessage = (message: SupportChatMessage) =>\n message.senderRole === SUPPORT_CHAT_AUTHOR.USER ||\n message.senderRole === SUPPORT_CHAT_AUTHOR.GUEST;\n\nconst formatTime = (createdAt: string) =>\n new Date(createdAt).toLocaleTimeString('pt-BR', {\n hour: '2-digit',\n minute: '2-digit',\n });\n\n/** \"Baseado na FAQ: <title>\" under an AI answer, linked when possible */\nconst FaqReference = ({\n faq,\n faqArticleUrl,\n onOpenArticle,\n}: {\n faq: { id: string; title: string };\n faqArticleUrl?: (articleId: string) => string;\n onOpenArticle?: (article: { id: string; title: string }) => void;\n}) => {\n const linked = !!onOpenArticle || !!faqArticleUrl;\n const label = (\n <Text\n as=\"span\"\n size=\"xs\"\n color=\"text-primary-700\"\n className={cn(linked && 'underline')}\n >\n Baseado na FAQ: {faq.title}\n </Text>\n );\n let content = label;\n if (onOpenArticle) {\n // Opens inside the chat, like the Zendesk messenger (no new tab)\n content = (\n <a\n href={faqArticleUrl?.(faq.id) ?? '#'}\n onClick={(event) => {\n event.preventDefault();\n onOpenArticle(faq);\n }}\n >\n {label}\n </a>\n );\n } else if (faqArticleUrl) {\n content = (\n <a href={faqArticleUrl(faq.id)} target=\"_blank\" rel=\"noreferrer\">\n {label}\n </a>\n );\n }\n return (\n <div data-testid=\"support-chat-faq\" className=\"mt-1 max-w-[80%]\">\n {content}\n </div>\n );\n};\n\nconst MessageBubble = ({\n message,\n faqArticleUrl,\n onOpenArticle,\n}: {\n message: SupportChatMessage;\n faqArticleUrl?: (articleId: string) => string;\n onOpenArticle?: (article: { id: string; title?: string }) => void;\n}) => {\n const own = isOwnMessage(message);\n return (\n <div\n className={cn(\n 'flex flex-col mb-3 transition-opacity',\n own ? 'items-end' : 'items-start',\n message.pending && 'opacity-60'\n )}\n aria-busy={message.pending || undefined}\n >\n {!own && (\n <Text size=\"xs\" className=\"text-text-500 mb-1\">\n {message.senderName}\n </Text>\n )}\n <div\n className={cn(\n 'max-w-[80%] rounded-2xl whitespace-pre-wrap break-words',\n // Image-only messages show the image without the colored bubble\n message.content\n ? cn(\n 'px-4 py-2',\n own\n ? 'bg-primary-500 text-white rounded-br-md'\n : 'bg-background-100 text-text-900 rounded-bl-md'\n )\n : 'text-text-900'\n )}\n >\n {message.content && (\n <MessageText\n text={message.content}\n onOpenArticle={\n onOpenArticle ? (id) => onOpenArticle({ id }) : undefined\n }\n />\n )}\n {message.attachments.length > 0 && (\n <AttachmentList items={message.attachments} />\n )}\n </div>\n {message.faq && (\n <FaqReference\n faq={message.faq}\n faqArticleUrl={faqArticleUrl}\n onOpenArticle={onOpenArticle}\n />\n )}\n <Text size=\"xs\" className=\"text-text-400 mt-1\">\n {message.pending ? 'Enviando…' : formatTime(message.createdAt)}\n </Text>\n </div>\n );\n};\n\n/**\n * Floating support chat (native support, the Zendesk replacement).\n * Renders nothing when disabled or when the institution uses Zendesk.\n */\nexport const SupportChatWidget = ({\n flags,\n ...props\n}: SupportChatWidgetProps) => {\n const hidden =\n flags?.enabled === false ||\n flags?.chat === false ||\n flags?.supportType === SupportType.ZENDESK;\n\n if (hidden) return null;\n return <SupportChatWidgetContent {...props} />;\n};\n\nconst SupportChatWidgetContent = ({\n adapter,\n channel,\n institutionId,\n user,\n theme,\n onOpen,\n onSend,\n onClose,\n faqArticleUrl,\n}: Omit<SupportChatWidgetProps, 'flags'>) => {\n const [windowState, setWindowState] =\n useState<SupportChatWindowState>('closed');\n const [draft, setDraft] = useState('');\n const [files, setFiles] = useState<File[]>([]);\n const [fileError, setFileError] = useState<string | null>(null);\n const [unread, setUnread] = useState(0);\n // FAQ article open inside the chat (the conversation stays as it was)\n const [article, setArticle] = useState<{ id: string; title?: string } | null>(\n null\n );\n const [expanded, setExpanded] = useState(false);\n const listRef = useRef<HTMLDivElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const seenCount = useRef(0);\n\n const { messages, sending, error, closed, send, updateContact, reset } =\n useSupportChat({\n adapter,\n channel,\n institutionId,\n user,\n });\n const [savedContactRequests, setSavedContactRequests] = useState<string[]>(\n []\n );\n\n // Latest AI request for name/e-mail: the form goes under it, and the\n // composer waits until it is sent\n const contactRequest = updateContact\n ? [...messages].reverse().find((message) => message.contactRequest?.length)\n : undefined;\n const awaitingContact =\n !!contactRequest && !savedContactRequests.includes(contactRequest.id);\n\n const isOpen = windowState === 'open';\n\n const canOpenArticle = !!adapter.getArticle && !!institutionId;\n const loadArticle = useCallback(\n (articleId: string) => {\n if (!adapter.getArticle || !institutionId) {\n return Promise.reject(new Error('Artigo indisponível'));\n }\n // Called on the adapter so a class-based adapter keeps its `this`\n return adapter.getArticle(institutionId, articleId);\n },\n [adapter, institutionId]\n );\n const openArticle = canOpenArticle\n ? (target: { id: string; title?: string }) => {\n setArticle(target);\n setExpanded(true);\n }\n : undefined;\n const closeArticle = () => {\n setArticle(null);\n setExpanded(false);\n };\n const title = theme?.title ?? 'Suporte';\n const left = theme?.position === 'bottom-left';\n const accentStyle = theme?.primaryColor\n ? { backgroundColor: theme.primaryColor }\n : undefined;\n\n // Count the support replies that arrive while the window is not visible\n useEffect(() => {\n const fresh = messages.slice(seenCount.current);\n seenCount.current = messages.length;\n if (isOpen) return;\n const replies = fresh.filter((message) => !isOwnMessage(message)).length;\n if (replies) setUnread((count) => count + replies);\n }, [messages, isOpen]);\n\n useEffect(() => {\n if (!isOpen || article) return;\n setUnread(0);\n listRef.current?.scrollTo?.({ top: listRef.current.scrollHeight });\n }, [isOpen, messages, article]);\n\n const open = () => {\n setWindowState('open');\n onOpen?.();\n };\n\n const close = () => {\n setWindowState('closed');\n onClose?.();\n };\n\n /** Drop a finished conversation so the next message opens a new ticket */\n const startOver = () => {\n setDraft('');\n setFiles([]);\n setFileError(null);\n setUnread(0);\n seenCount.current = 0;\n setSavedContactRequests([]);\n reset();\n };\n\n const submit = async () => {\n const content = draft;\n const pending = files;\n if (!content.trim() && pending.length === 0) return;\n setDraft('');\n setFiles([]);\n setFileError(null);\n const message = await send(content, pending);\n if (message) {\n onSend?.(message);\n } else {\n setDraft(content);\n setFiles(pending);\n }\n };\n\n const handleFiles = (event: ChangeEvent<HTMLInputElement>) => {\n const { accepted, problem } = pickAttachments(\n files,\n Array.from(event.target.files ?? [])\n );\n setFiles(accepted);\n setFileError(problem);\n event.target.value = '';\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n void submit();\n }\n };\n\n return (\n <div\n className={cn(\n 'fixed bottom-4 z-50 flex flex-col gap-3',\n left ? 'left-4 items-start' : 'right-4 items-end'\n )}\n >\n {isOpen && (\n <section\n role=\"dialog\"\n aria-label={title}\n className={cn(\n 'max-w-[calc(100vw-2rem)] max-h-[calc(100vh-6rem)] flex flex-col bg-background rounded-2xl shadow-lg border border-border-100 overflow-hidden transition-[width,height]',\n expanded ? 'w-[720px] h-[615px]' : 'w-[360px] h-[520px]'\n )}\n >\n {article ? (\n <SupportChatArticleView\n articleId={article.id}\n initialTitle={article.title}\n loadArticle={loadArticle}\n expanded={expanded}\n onToggleExpanded={() => setExpanded((value) => !value)}\n onBack={closeArticle}\n externalUrl={faqArticleUrl?.(article.id)}\n accentStyle={accentStyle}\n />\n ) : (\n <>\n <header\n className=\"flex items-center justify-between px-4 py-3 bg-primary-500 text-white\"\n style={accentStyle}\n >\n <Text size=\"md\" weight=\"semibold\" color=\"\">\n {title}\n </Text>\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"raw\"\n aria-label=\"Minimizar\"\n onClick={() => setWindowState('minimized')}\n className=\"p-1 rounded hover:bg-white/20\"\n >\n <MinusIcon size={18} />\n </Button>\n <Button\n variant=\"raw\"\n aria-label=\"Fechar\"\n onClick={close}\n className=\"p-1 rounded hover:bg-white/20\"\n >\n <XIcon size={18} />\n </Button>\n </div>\n </header>\n\n <div ref={listRef} className=\"flex-1 overflow-y-auto px-4 py-3\">\n {messages.length === 0 ? (\n <Text size=\"sm\" className=\"text-text-500 text-center mt-8\">\n Olá! Como podemos te ajudar?\n </Text>\n ) : (\n messages.map((message) => (\n <div key={message.id}>\n <MessageBubble\n message={message}\n faqArticleUrl={faqArticleUrl}\n onOpenArticle={openArticle}\n />\n {updateContact &&\n message.id === contactRequest?.id &&\n message.contactRequest && (\n <SupportChatContactForm\n fields={message.contactRequest}\n accentColor={theme?.primaryColor}\n onSubmit={async (contact) => {\n const ok = await updateContact(contact);\n if (ok) {\n setSavedContactRequests((ids) => [\n ...ids,\n message.id,\n ]);\n }\n return ok;\n }}\n />\n )}\n </div>\n ))\n )}\n </div>\n\n {(fileError || error) && (\n <Text\n size=\"xs\"\n role=\"alert\"\n className=\"px-4 py-2 text-error-700 bg-error-50\"\n >\n {fileError ?? error}\n </Text>\n )}\n\n {closed ? (\n <div className=\"flex flex-col items-center gap-2 px-4 py-3 border-t border-border-100\">\n <Text size=\"sm\" color=\"text-text-500\" className=\"text-center\">\n Este atendimento foi encerrado.\n </Text>\n <Button variant=\"link\" size=\"small\" onClick={startOver}>\n Iniciar nova conversa\n </Button>\n </div>\n ) : (\n <form\n className=\"flex flex-col gap-2 px-3 py-3 border-t border-border-100\"\n onSubmit={(event) => {\n event.preventDefault();\n void submit();\n }}\n >\n {files.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {files.map((file, index) => (\n <PendingAttachment\n key={`${file.name}-${index}`}\n file={file}\n onRemove={() =>\n setFiles((list) =>\n list.filter((_, i) => i !== index)\n )\n }\n />\n ))}\n </div>\n )}\n <div className=\"flex items-end gap-2\">\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={SUPPORT_CHAT_ACCEPT}\n multiple\n hidden\n data-testid=\"support-chat-file-input\"\n onChange={handleFiles}\n />\n <Button\n variant=\"raw\"\n aria-label=\"Anexar arquivo\"\n disabled={\n sending ||\n awaitingContact ||\n files.length >= SUPPORT_CHAT_MAX_ATTACHMENTS\n }\n onClick={() => fileInputRef.current?.click()}\n className=\"p-2 rounded-full text-text-600 hover:bg-background-100 disabled:opacity-50\"\n >\n <PaperclipIcon size={20} />\n </Button>\n <textarea\n aria-label=\"Mensagem\"\n placeholder={\n awaitingContact\n ? 'Preencha seus dados acima'\n : 'Digite sua mensagem'\n }\n disabled={awaitingContact}\n rows={1}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={handleKeyDown}\n className=\"flex-1 resize-none rounded-xl border border-border-300 px-3 py-2 text-sm text-text-900 focus:outline-none focus:border-primary-500 max-h-28\"\n />\n <Button\n variant=\"raw\"\n type=\"submit\"\n aria-label=\"Enviar\"\n disabled={\n sending ||\n awaitingContact ||\n (!draft.trim() && files.length === 0)\n }\n className=\"p-2 rounded-full bg-primary-500 text-white disabled:opacity-50\"\n style={accentStyle}\n >\n <PaperPlaneTiltIcon size={18} />\n </Button>\n </div>\n </form>\n )}\n </>\n )}\n </section>\n )}\n\n {!isOpen && (\n <Button\n variant=\"raw\"\n aria-label={`Abrir ${title.toLowerCase()}`}\n onClick={open}\n className=\"relative w-14 h-14 rounded-full bg-primary-500 text-white shadow-lg flex items-center justify-center hover:brightness-110\"\n style={accentStyle}\n >\n <ChatCircleDotsIcon size={28} />\n {unread > 0 && (\n <Text\n as=\"span\"\n size=\"xs\"\n color=\"text-white\"\n aria-label={formatNewMessagesLabel(unread)}\n className=\"absolute -top-1 -right-1 min-w-5 h-5 px-1 rounded-full bg-error-500 flex items-center justify-center\"\n >\n {unread}\n </Text>\n )}\n </Button>\n )}\n </div>\n );\n};\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n SUPPORT_CHAT_AUTHOR,\n SUPPORT_CHAT_GUEST_SENDER_ID,\n SUPPORT_CHAT_STATUS,\n type SupportChatAdapter,\n type SupportChatContact,\n type SupportChatEvent,\n type SupportChatMessage,\n type SupportChatTicket,\n type SupportChatUser,\n} from '../types/supportChat';\n\nexport interface UseSupportChatConfig {\n adapter: SupportChatAdapter;\n channel: string;\n institutionId?: string;\n user?: SupportChatUser;\n}\n\nexport interface UseSupportChatReturn {\n ticket: SupportChatTicket | null;\n messages: SupportChatMessage[];\n sending: boolean;\n error: string | null;\n /** Ticket resolved: no more messages can be sent */\n closed: boolean;\n /** Send a message (text and/or files); the first one opens the ticket */\n send: (content: string, files?: File[]) => Promise<SupportChatMessage | null>;\n /**\n * Save the contact typed in the form shown under the AI request. Resolves\n * false (and sets `error`) when it fails. Undefined when the adapter cannot\n * save it.\n */\n updateContact?: (contact: SupportChatContact) => Promise<boolean>;\n /** Forget the current conversation */\n reset: () => void;\n}\n\nlet pendingSequence = 0;\n\n/**\n * Message shown right away while it is being sent (optimistic rendering).\n * Images use local object URLs, released when the real message arrives.\n */\nconst buildPendingMessage = (\n content: string,\n files: File[],\n user?: SupportChatUser\n): SupportChatMessage => {\n pendingSequence += 1;\n return {\n id: `pending-${pendingSequence}`,\n senderId: SUPPORT_CHAT_GUEST_SENDER_ID,\n senderName: user?.name || 'Você',\n senderPhoto: null,\n senderRole: user?.email\n ? SUPPORT_CHAT_AUTHOR.USER\n : SUPPORT_CHAT_AUTHOR.GUEST,\n content,\n messageType: 'text',\n createdAt: new Date().toISOString(),\n attachments: files.map((file, index) => ({\n id: `pending-${pendingSequence}-${index}`,\n url: URL.createObjectURL(file),\n fileName: file.name,\n mimeType: file.type,\n size: file.size,\n })),\n pending: true,\n };\n};\n\nconst releasePending = (message: SupportChatMessage) =>\n message.attachments.forEach((item) => URL.revokeObjectURL(item.url));\n\nconst getErrorMessage = (error: unknown) =>\n error instanceof Error ? error.message : 'Não foi possível enviar a mensagem';\n\n/** Add or replace a message, keeping the list ordered by arrival */\nconst upsertMessage = (\n list: SupportChatMessage[],\n message: SupportChatMessage\n): SupportChatMessage[] =>\n list.some((item) => item.id === message.id)\n ? list.map((item) => (item.id === message.id ? message : item))\n : [...list, message];\n\n/**\n * State of a support conversation on top of a SupportChatAdapter.\n * The REST-like calls write; the subscription only applies the events,\n * so a message received both ways is stored once.\n */\nexport const useSupportChat = ({\n adapter,\n channel,\n institutionId,\n user,\n}: UseSupportChatConfig): UseSupportChatReturn => {\n const [ticket, setTicket] = useState<SupportChatTicket | null>(null);\n const [messages, setMessages] = useState<SupportChatMessage[]>([]);\n const [sending, setSending] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const sendingRef = useRef(false);\n\n const ticketId = ticket?.id;\n\n useEffect(() => {\n if (!ticketId) return;\n\n const handleEvent = (event: SupportChatEvent) => {\n switch (event.type) {\n case 'new_message':\n setMessages((list) => upsertMessage(list, event.payload.message));\n break;\n case 'message_updated':\n setMessages((list) =>\n list.map((item) =>\n item.id === event.payload.messageId\n ? { ...item, content: event.payload.content }\n : item\n )\n );\n break;\n case 'ticket_updated': {\n const { ticketId: id, ...rest } = event.payload;\n setTicket((current) =>\n current?.id === id ? { ...current, ...rest } : current\n );\n break;\n }\n case 'error':\n setError(event.payload.message);\n break;\n }\n };\n\n return adapter.subscribe(ticketId, handleEvent);\n }, [adapter, ticketId]);\n\n const send = useCallback(\n async (content: string, files: File[] = []) => {\n const text = content.trim();\n if ((!text && files.length === 0) || sendingRef.current) return null;\n\n sendingRef.current = true;\n setSending(true);\n setError(null);\n\n const pending = buildPendingMessage(text, files, user);\n setMessages((list) => [...list, pending]);\n // Swap the optimistic message for the confirmed ones\n const confirm = (confirmed: SupportChatMessage[]) =>\n setMessages((list) =>\n confirmed.reduce(\n upsertMessage,\n list.filter((item) => item.id !== pending.id)\n )\n );\n\n try {\n if (ticketId) {\n const message = await adapter.sendMessage(ticketId, text, files);\n confirm([message]);\n return message;\n }\n\n const started = await adapter.startConversation({\n message: text,\n channel,\n institutionId,\n requesterName: user?.name,\n email: user?.email,\n files,\n });\n setTicket(started.ticket);\n confirm(started.messages);\n return started.messages[started.messages.length - 1] ?? null;\n } catch (sendError) {\n setMessages((list) => list.filter((item) => item.id !== pending.id));\n setError(getErrorMessage(sendError));\n return null;\n } finally {\n releasePending(pending);\n sendingRef.current = false;\n setSending(false);\n }\n },\n [adapter, channel, institutionId, ticketId, user]\n );\n\n const updateContact = useCallback(\n async (contact: SupportChatContact) => {\n if (!ticketId || !adapter.updateContact) return false;\n setError(null);\n try {\n await adapter.updateContact(ticketId, contact);\n return true;\n } catch (contactError) {\n setError(\n (contactError as { response?: { status?: number } } | null)?.response\n ?.status === 409\n ? 'O e-mail deste atendimento já foi informado'\n : 'Não foi possível salvar seus dados. Tente de novo.'\n );\n return false;\n }\n },\n [adapter, ticketId]\n );\n\n const reset = useCallback(() => {\n setTicket(null);\n setMessages([]);\n setError(null);\n }, []);\n\n return {\n ticket,\n messages,\n sending,\n error,\n closed: ticket?.status === SUPPORT_CHAT_STATUS.FECHADO,\n send,\n updateContact: adapter.updateContact ? updateContact : undefined,\n reset,\n };\n};\n","import { useState, type FormEvent } from 'react';\nimport Text from '../Text/Text';\nimport Button from '../Button/Button';\nimport Input from '../Input/Input';\nimport type {\n SupportChatContact,\n SupportChatContactField,\n} from '../../types/supportChat';\n\nexport interface SupportChatContactFormProps {\n /** Fields the AI asked for, in order (one step each) */\n fields: SupportChatContactField[];\n /** Saves the contact; resolves true when it worked */\n onSubmit: (contact: SupportChatContact) => Promise<boolean>;\n /** Accent color of the theme, for the button */\n accentColor?: string;\n}\n\nconst STEPS: Record<\n SupportChatContactField,\n { label: string; type: string; autoComplete: string; placeholder: string }\n> = {\n name: {\n label: 'Seu nome',\n type: 'text',\n autoComplete: 'name',\n placeholder: 'Como podemos te chamar?',\n },\n email: {\n label: 'Seu e-mail',\n type: 'email',\n autoComplete: 'email',\n placeholder: 'nome@exemplo.com',\n },\n};\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** Problem with a typed value, or null when it can be sent */\nconst validate = (\n field: SupportChatContactField,\n value: string\n): string | null => {\n if (field === 'name') {\n return value.length >= 2 ? null : 'Digite seu nome';\n }\n return EMAIL_PATTERN.test(value) ? null : 'Digite um e-mail válido';\n};\n\n/**\n * Step-by-step form for the contact the AI asks on the first contact (name,\n * then e-mail), shown under the AI message like the Zendesk pre-chat form.\n * The values go to the backend as fields, not as a chat message. Once sent,\n * it shows what was saved.\n */\nexport const SupportChatContactForm = ({\n fields,\n onSubmit,\n accentColor,\n}: SupportChatContactFormProps) => {\n const [step, setStep] = useState(0);\n const [values, setValues] = useState<Record<string, string>>({});\n const [problem, setProblem] = useState<string | null>(null);\n const [saving, setSaving] = useState(false);\n const [saved, setSaved] = useState(false);\n\n const field = fields[step];\n const config = STEPS[field];\n const value = values[field] ?? '';\n const isLast = step === fields.length - 1;\n\n if (saved) {\n return (\n <div\n data-testid=\"support-chat-contact-saved\"\n className=\"mb-3 ml-1 flex max-w-[85%] flex-col gap-1 rounded-2xl border border-border-100 px-4 py-3\"\n >\n {fields.map((item) => (\n <Text key={item} size=\"xs\" color=\"text-text-600\">\n {STEPS[item].label}:{' '}\n <Text as=\"span\" size=\"xs\" weight=\"semibold\" color=\"text-text-900\">\n {values[item]}\n </Text>\n </Text>\n ))}\n </div>\n );\n }\n\n const advance = async (event: FormEvent) => {\n event.preventDefault();\n const typed = field === 'email' ? value.trim().toLowerCase() : value.trim();\n const error = validate(field, typed);\n setProblem(error);\n if (error) return;\n\n const next = { ...values, [field]: typed };\n setValues(next);\n if (!isLast) {\n setStep(step + 1);\n return;\n }\n\n setSaving(true);\n const ok = await onSubmit({\n ...(next.name ? { requesterName: next.name } : {}),\n ...(next.email ? { email: next.email } : {}),\n });\n setSaving(false);\n if (ok) setSaved(true);\n };\n\n const inputId = `support-chat-contact-${field}`;\n\n return (\n <form\n aria-label=\"Seus dados\"\n noValidate\n onSubmit={(event) => void advance(event)}\n className=\"mb-3 ml-1 flex max-w-[85%] flex-col gap-3 rounded-2xl border border-border-100 px-4 py-3\"\n >\n <Input\n id={inputId}\n key={field}\n label={config.label}\n variant=\"rounded\"\n size=\"small\"\n type={config.type}\n autoComplete={config.autoComplete}\n placeholder={config.placeholder}\n value={value}\n autoFocus\n disabled={saving}\n errorMessage={problem ?? undefined}\n onChange={(event) =>\n setValues((current) => ({ ...current, [field]: event.target.value }))\n }\n />\n <div className=\"flex items-center justify-between\">\n <Text size=\"xs\" color=\"text-text-500\">\n {fields.length > 1 ? `${step + 1} de ${fields.length}` : ''}\n </Text>\n <Button\n type=\"submit\"\n size=\"small\"\n disabled={saving}\n style={accentColor ? { backgroundColor: accentColor } : undefined}\n >\n {isLast ? 'Enviar' : 'Avançar'}\n </Button>\n </div>\n </form>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n ArrowLeftIcon,\n ArrowSquareOutIcon,\n ArrowsInSimpleIcon,\n ArrowsOutSimpleIcon,\n} from '@phosphor-icons/react';\nimport Text from '../Text/Text';\nimport Button from '../Button/Button';\nimport { ArticleContent } from '../ArticleContent/ArticleContent';\nimport { embedVideoLinks } from '../../utils/videoEmbed';\nimport type { SupportChatArticle } from '../../types/supportChat';\nimport { cn } from '../../utils/utils';\n\nexport interface SupportChatArticleViewProps {\n /** Article to show */\n articleId: string;\n /** Title known from the chat, shown while the article loads */\n initialTitle?: string;\n /** Loads the article (adapter.getArticle bound to the institution) */\n loadArticle: (articleId: string) => Promise<SupportChatArticle>;\n /** Whether the chat window is in the wide mode */\n expanded: boolean;\n onToggleExpanded: () => void;\n /** Back to the conversation (it is kept as it was) */\n onBack: () => void;\n /** Page of the article in the help center, for \"Abrir em nova aba\" */\n externalUrl?: string;\n /** Header background (theme) */\n accentStyle?: { backgroundColor: string };\n}\n\nconst formatUpdatedAt = (iso: string) =>\n new Date(iso).toLocaleDateString('pt-BR', {\n day: '2-digit',\n month: '2-digit',\n year: 'numeric',\n });\n\n/**\n * FAQ article read inside the support chat, like the Zendesk messenger: the\n * conversation is replaced by the article (no new tab), with back,\n * expand/reduce and open-in-new-tab controls. The big title moves to the\n * header once it scrolls out of view. YouTube/Vimeo links of the article\n * become players.\n */\nexport const SupportChatArticleView = ({\n articleId,\n initialTitle,\n loadArticle,\n expanded,\n onToggleExpanded,\n onBack,\n externalUrl,\n accentStyle,\n}: SupportChatArticleViewProps) => {\n const [article, setArticle] = useState<SupportChatArticle | null>(null);\n const [failed, setFailed] = useState(false);\n const [titleHidden, setTitleHidden] = useState(false);\n const titleRef = useRef<HTMLHeadingElement>(null);\n\n useEffect(() => {\n let active = true;\n setArticle(null);\n setFailed(false);\n loadArticle(articleId)\n .then((loaded) => {\n if (active) setArticle(loaded);\n })\n .catch(() => {\n if (active) setFailed(true);\n });\n return () => {\n active = false;\n };\n }, [articleId, loadArticle]);\n\n // The big title leaves the screen → show it in the header\n useEffect(() => {\n const title = titleRef.current;\n if (!title || typeof IntersectionObserver === 'undefined') {\n return undefined;\n }\n const observer = new IntersectionObserver(([entry]) =>\n setTitleHidden(!entry.isIntersecting)\n );\n observer.observe(title);\n return () => observer.disconnect();\n }, [article]);\n\n const html = useMemo(\n () => (article ? embedVideoLinks(article.body) : ''),\n [article]\n );\n const title = article?.title ?? initialTitle ?? 'Artigo';\n\n return (\n <div\n data-testid=\"support-chat-article\"\n className=\"flex min-h-0 flex-1 flex-col\"\n >\n <header\n className=\"flex items-center gap-1 px-2 py-2 bg-primary-500 text-white\"\n style={accentStyle}\n >\n <Button\n variant=\"raw\"\n aria-label=\"Voltar para a janela de mensagens\"\n onClick={onBack}\n className=\"p-1 rounded hover:bg-white/20\"\n >\n <ArrowLeftIcon size={18} />\n </Button>\n <Text\n as=\"span\"\n size=\"sm\"\n weight=\"semibold\"\n color=\"\"\n aria-hidden={!titleHidden || undefined}\n className={cn(\n 'min-w-0 flex-1 truncate transition-opacity',\n titleHidden ? 'opacity-100' : 'opacity-0'\n )}\n >\n {title}\n </Text>\n <Button\n variant=\"raw\"\n aria-label={expanded ? 'Reduzir artigo' : 'Expandir artigo'}\n onClick={onToggleExpanded}\n className=\"p-1 rounded hover:bg-white/20\"\n >\n {expanded ? (\n <ArrowsInSimpleIcon size={18} />\n ) : (\n <ArrowsOutSimpleIcon size={18} />\n )}\n </Button>\n {externalUrl && (\n <a\n href={externalUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n aria-label=\"Abrir em nova aba\"\n className=\"p-1 rounded hover:bg-white/20\"\n >\n <ArrowSquareOutIcon size={18} />\n </a>\n )}\n </header>\n\n <div className=\"flex-1 overflow-y-auto px-5 py-4\">\n <Text\n as=\"h2\"\n ref={titleRef}\n size=\"xl\"\n weight=\"bold\"\n color=\"text-text-950\"\n >\n {title}\n </Text>\n {article?.updatedAt && (\n <Text size=\"xs\" color=\"text-text-500\" className=\"mt-1\">\n Atualizado em {formatUpdatedAt(article.updatedAt)}\n </Text>\n )}\n\n <div className=\"mt-4\">\n {failed && (\n <Text size=\"sm\" role=\"alert\" color=\"text-error-700\">\n Não foi possível abrir o artigo.\n </Text>\n )}\n {!failed && !article && (\n <Text size=\"sm\" color=\"text-text-500\">\n Carregando…\n </Text>\n )}\n {article && <ArticleContent html={html} />}\n </div>\n </div>\n </div>\n );\n};\n","import { useEffect, useState, type ReactNode } from 'react';\nimport {\n FileIcon,\n FilePdfIcon,\n FileVideoIcon,\n XIcon,\n} from '@phosphor-icons/react';\nimport Text from '../Text/Text';\nimport Button from '../Button/Button';\nimport {\n SUPPORT_CHAT_ATTACHMENT_RULES,\n SUPPORT_CHAT_MAX_ATTACHMENTS,\n type SupportChatAttachment,\n} from '../../types/supportChat';\n\n/** Value of the `accept` attribute of the file picker */\nexport const SUPPORT_CHAT_ACCEPT = Object.keys(\n SUPPORT_CHAT_ATTACHMENT_RULES\n).join(',');\n\nconst isImage = (mimeType?: string) => Boolean(mimeType?.startsWith('image/'));\nconst isVideo = (mimeType?: string) => Boolean(mimeType?.startsWith('video/'));\n\nconst formatSize = (bytes: number) => `${Math.round(bytes / (1024 * 1024))} MB`;\n\n/**\n * Keep the files the backend accepts (type, size and count) and say why the\n * others were left out.\n * @param current - Files already picked\n * @param picked - Files just chosen\n * @returns The accepted files and the last problem found, if any\n */\nexport const pickAttachments = (current: File[], picked: File[]) => {\n const accepted = [...current];\n let problem: string | null = null;\n for (const file of picked) {\n const maxBytes = SUPPORT_CHAT_ATTACHMENT_RULES[file.type];\n if (!maxBytes) {\n problem =\n 'Envie imagens (JPG, PNG, GIF, WEBP), PDF ou vídeo (MP4, WEBM, MOV)';\n } else if (file.size > maxBytes) {\n problem = `${file.name} passa de ${formatSize(maxBytes)}`;\n } else if (accepted.length >= SUPPORT_CHAT_MAX_ATTACHMENTS) {\n problem = `Máximo de ${SUPPORT_CHAT_MAX_ATTACHMENTS} arquivos por mensagem`;\n } else {\n accepted.push(file);\n }\n }\n return { accepted, problem };\n};\n\nconst FileTypeIcon = ({ mimeType }: { mimeType?: string }) => {\n if (mimeType === 'application/pdf') return <FilePdfIcon size={18} />;\n if (isVideo(mimeType)) return <FileVideoIcon size={18} />;\n return <FileIcon size={18} />;\n};\n\n/** Files of a message: images and videos inline, other files as links */\nexport const AttachmentList = ({\n items,\n}: {\n items: SupportChatAttachment[];\n}) => (\n <div className=\"flex flex-wrap gap-2 first:mt-0 mt-1\">\n {items.map((item) => {\n if (isImage(item.mimeType)) {\n return (\n <a key={item.id} href={item.url} target=\"_blank\" rel=\"noreferrer\">\n <img\n src={item.url}\n alt={item.fileName}\n className=\"max-w-[200px] max-h-[200px] rounded-lg object-cover\"\n />\n </a>\n );\n }\n if (isVideo(item.mimeType)) {\n return (\n <video\n key={item.id}\n src={item.url}\n controls\n preload=\"metadata\"\n aria-label={item.fileName}\n className=\"max-w-[240px] max-h-[200px] rounded-lg bg-black\"\n />\n );\n }\n return (\n <a\n key={item.id}\n href={item.url}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"flex items-center gap-1 underline\"\n >\n <FileTypeIcon mimeType={item.mimeType} />\n <Text as=\"span\" size=\"sm\" color=\"\">\n {item.fileName}\n </Text>\n </a>\n );\n })}\n </div>\n);\n\n/** A file picked but not sent yet: thumbnail for images, name for the rest */\nexport const PendingAttachment = ({\n file,\n onRemove,\n}: {\n file: File;\n onRemove: () => void;\n}) => {\n const image = isImage(file.type);\n const [url, setUrl] = useState('');\n useEffect(() => {\n if (!image) return undefined;\n const objectUrl = URL.createObjectURL(file);\n setUrl(objectUrl);\n return () => URL.revokeObjectURL(objectUrl);\n }, [file, image]);\n\n return (\n <div className=\"relative\">\n {image ? (\n url && (\n <img\n src={url}\n alt={file.name}\n className=\"w-14 h-14 rounded-lg object-cover border border-border-100\"\n />\n )\n ) : (\n <div className=\"flex h-14 max-w-[160px] items-center gap-1 rounded-lg border border-border-100 px-2 text-text-700\">\n <FileTypeIcon mimeType={file.type} />\n <Text as=\"span\" size=\"xs\" className=\"truncate\">\n {file.name}\n </Text>\n </div>\n )}\n <Button\n variant=\"raw\"\n aria-label={`Remover ${file.name}`}\n onClick={onRemove}\n className=\"absolute -top-1 -right-1 w-5 h-5 rounded-full bg-text-700 text-white flex items-center justify-center\"\n >\n <XIcon size={12} />\n </Button>\n </div>\n );\n};\n\n/** http(s) addresses inside a message */\nconst URL_PATTERN = /https?:\\/\\/[^\\s<>\"]+/g;\n\n/** Punctuation that ends a sentence, not the address */\nconst TRAILING_PUNCTUATION = /[.,;:!?)\\]]+$/;\n\n/** A FAQ article page: `/artigo/<uuid>` */\nconst ARTICLE_PATH =\n /\\/artigo\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;\n\n/**\n * Message text with its addresses as links. A link to a FAQ article opens in\n * the chat (when `onOpenArticle` is given); the others open a new tab.\n */\nexport const MessageText = ({\n text,\n onOpenArticle,\n}: {\n text: string;\n onOpenArticle?: (articleId: string) => void;\n}) => {\n const parts: ReactNode[] = [];\n let cursor = 0;\n for (const match of text.matchAll(URL_PATTERN)) {\n const raw = match[0];\n const url = raw.replace(TRAILING_PUNCTUATION, '');\n const start = match.index ?? 0;\n if (start > cursor) parts.push(text.slice(cursor, start));\n\n const articleId = onOpenArticle ? ARTICLE_PATH.exec(url)?.[1] : undefined;\n parts.push(\n <a\n key={`${start}-${url}`}\n href={url}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"underline break-all\"\n onClick={\n articleId\n ? (event) => {\n event.preventDefault();\n onOpenArticle?.(articleId);\n }\n : undefined\n }\n >\n {url}\n </a>\n );\n cursor = start + url.length;\n }\n if (cursor < text.length) parts.push(text.slice(cursor));\n\n return (\n <Text size=\"sm\" color=\"\" className=\"whitespace-pre-wrap\">\n {parts}\n </Text>\n );\n};\n","import { useMemo } from 'react';\nimport { SupportChatWidget } from './SupportChatWidget';\nimport { createGuestSupportChatAdapter } from './guestSupportChatAdapter';\nimport { useSupportFeatureFlag } from '../../hooks/useSupportFeatureFlag';\nimport { useAppStore } from '../../store/appStore';\nimport { useAuthStore } from '../../store/authStore';\nimport { SupportType, type SupportApiClient } from '../../types/support';\nimport type {\n SupportChatHttpClient,\n SupportChatTheme,\n} from '../../types/supportChat';\n\nexport interface NativeSupportChatProps {\n /** Client of the backend API (the app axios instance fits as it is) */\n apiClient: SupportApiClient & SupportChatHttpClient;\n /** App the chat runs in, sent with the ticket (e.g. \"aluno\", \"login\") */\n channel: string;\n /**\n * Extra condition of the app to show the chat, on top of the SUPPORT flag\n * (e.g. logged in and with the support module). Default: true.\n */\n enabled?: boolean;\n /**\n * Link of a FAQ article. Default: the FAQ address of the SUPPORT flag\n * (`<faqUrl>/artigo/<id>`).\n */\n faqArticleUrl?: (articleId: string) => string;\n /** Support WebSocket; without it the chat polls the REST API */\n wsUrl?: string;\n theme?: SupportChatTheme;\n}\n\n/**\n * Native support chat ready to drop in an app: reads the SUPPORT feature flag\n * of the institution (from the app store), the logged user (from the auth\n * store) and talks to the support API by itself.\n *\n * Shown only when the institution uses the native support with the internal\n * support and its chat switched on in the backoffice, once the flag is read,\n * and when `enabled`. With Zendesk it renders nothing (the app loads the\n * Zendesk widget). A logged user opens the ticket with their name and e-mail;\n * a visitor gives them to the AI on the first contact.\n *\n * @example\n * <NativeSupportChat apiClient={api} channel=\"aluno\" enabled={hasSupport} />\n */\nexport const NativeSupportChat = ({\n apiClient,\n channel,\n enabled = true,\n faqArticleUrl,\n wsUrl,\n theme,\n}: NativeSupportChatProps) => {\n const { institutionId } = useAppStore();\n const userName = useAuthStore(\n (state) => state.user?.name ?? state.sessionInfo?.userName\n );\n const userEmail = useAuthStore((state) => state.user?.email);\n const flag = useSupportFeatureFlag({ apiClient });\n const adapter = useMemo(\n () => createGuestSupportChatAdapter({ apiClient, wsUrl }),\n [apiClient, wsUrl]\n );\n\n const user =\n userName || userEmail ? { name: userName, email: userEmail } : undefined;\n\n return (\n <SupportChatWidget\n adapter={adapter}\n channel={channel}\n institutionId={institutionId || undefined}\n user={user}\n theme={theme}\n faqArticleUrl={faqArticleUrl ?? flag.faqArticleUrl}\n flags={{\n enabled: enabled && !flag.loading && flag.isNative && flag.chatEnabled,\n supportType: flag.isNative ? SupportType.NATIVE : SupportType.ZENDESK,\n }}\n />\n );\n};\n","import { isSupportTicketOrigin } from '../../types/support';\nimport {\n SUPPORT_CHAT_AUTHOR,\n SUPPORT_CHAT_GUEST_SENDER_ID,\n SUPPORT_CHAT_STATUS,\n type SupportChatAdapter,\n type SupportChatAnswerApi,\n type SupportChatArticle,\n type SupportChatEvent,\n type SupportChatHttpClient,\n type SupportChatMessage,\n} from '../../types/supportChat';\n\nexport interface GuestSupportChatAdapterOptions {\n /** Client of the backend API (e.g. the app axios instance) */\n apiClient: SupportChatHttpClient;\n /**\n * Support WebSocket, including ?channel=support (e.g.\n * wss://bff-hml-chat.analyticaensino.com.br?channel=support).\n * Without it the adapter only polls the REST API.\n */\n wsUrl?: string;\n /** Polling interval while the socket is not connected, in ms */\n pollIntervalMs?: number;\n}\n\nconst GUEST_TOKEN_HEADER = 'x-support-guest-token';\n\nconst senderNameFor = (role: string, requesterName: string) => {\n if (role === SUPPORT_CHAT_AUTHOR.AGENT) return 'Suporte';\n if (role === SUPPORT_CHAT_AUTHOR.AI) return 'Assistente';\n return requesterName;\n};\n\n/** REST answer → widget message */\nexport const fromSupportAnswer = (\n answer: SupportChatAnswerApi,\n requesterName: string\n): SupportChatMessage => ({\n id: answer.id,\n senderId:\n answer.authorType === SUPPORT_CHAT_AUTHOR.GUEST\n ? SUPPORT_CHAT_GUEST_SENDER_ID\n : answer.authorType.toLowerCase(),\n senderName: senderNameFor(answer.authorType, requesterName),\n senderPhoto: null,\n senderRole: answer.authorType,\n content: answer.answer,\n messageType: 'text',\n createdAt: answer.createdAt,\n attachments: answer.attachments ?? [],\n faq:\n answer.faqArticleId && answer.faqArticleTitle\n ? { id: answer.faqArticleId, title: answer.faqArticleTitle }\n : null,\n contactRequest: answer.contactRequest ?? null,\n});\n\n/**\n * Support chat adapter for visitors (no login), on top of the support API:\n * - POST /support/external opens the ticket and returns the guest token;\n * - GET/POST /support/guest/:id/answer read and send, with the token header;\n * - the chat WebSocket (?channel=support&roomId=&token=) pushes the events,\n * and REST polling covers the time the socket is down.\n */\nexport const createGuestSupportChatAdapter = ({\n apiClient,\n wsUrl,\n pollIntervalMs = 3000,\n}: GuestSupportChatAdapterOptions): SupportChatAdapter => {\n const sessions = new Map<string, { token: string; name: string }>();\n /**\n * Ticket opening text written by the adapter (an image-only start still\n * needs a description). It is the ticket description for the agents, not\n * something the visitor typed, so the chat does not show it.\n */\n const hiddenIds = new Set<string>();\n const isVisible = (message: SupportChatMessage) => !hiddenIds.has(message.id);\n\n const headersFor = (ticketId: string) => ({\n [GUEST_TOKEN_HEADER]: sessions.get(ticketId)?.token ?? '',\n });\n const nameFor = (ticketId: string) =>\n sessions.get(ticketId)?.name ?? 'Visitante';\n\n const listMessages = async (ticketId: string) => {\n const { data } = await apiClient.get<{ data: SupportChatAnswerApi[] }>(\n `/support/guest/${ticketId}/answer`,\n { headers: headersFor(ticketId) }\n );\n return data.data\n .map((answer) => fromSupportAnswer(answer, nameFor(ticketId)))\n .filter(isVisible);\n };\n\n /**\n * Upload each file straight to the storage: ask the backend for a signed\n * URL, then PUT the file there. Returns what the message needs.\n */\n const uploadFiles = (ticketId: string, files: File[]) =>\n Promise.all(\n files.map(async (file) => {\n const { data } = await apiClient.post<{\n data: { uploadUrl: string; key: string; contentType: string };\n }>(\n `/support/guest/${ticketId}/upload-url`,\n { fileName: file.name, fileSize: file.size, mimeType: file.type },\n { headers: headersFor(ticketId) }\n );\n const { uploadUrl, key, contentType } = data.data;\n const response = await fetch(uploadUrl, {\n method: 'PUT',\n headers: { 'Content-Type': contentType },\n body: file,\n });\n if (!response.ok) {\n throw new Error(`Não foi possível enviar ${file.name}`);\n }\n return {\n key,\n fileName: file.name,\n mimeType: contentType,\n size: file.size,\n };\n })\n );\n\n const sendMessage = async (\n ticketId: string,\n content: string,\n files: File[] = []\n ) => {\n const attachments = await uploadFiles(ticketId, files);\n const { data } = await apiClient.post<{ data: SupportChatAnswerApi }>(\n `/support/guest/${ticketId}/answer`,\n { answer: content, attachments },\n { headers: headersFor(ticketId) }\n );\n return fromSupportAnswer(data.data, nameFor(ticketId));\n };\n\n return {\n async startConversation({\n message,\n channel,\n institutionId,\n requesterName,\n email,\n files = [],\n }) {\n // The ticket needs a description; an image-only start uses the names\n const description = message || files.map((file) => file.name).join(', ');\n const { data } = await apiClient.post<{\n data: { id: string; guestToken: string };\n }>('/support/external', {\n description,\n ...(institutionId ? { institutionId } : {}),\n ...(requesterName ? { requesterName } : {}),\n ...(email ? { email } : {}),\n // The app the chat runs in, when it is one the API knows\n ...(isSupportTicketOrigin(channel) ? { origin: channel } : {}),\n });\n const ticketId = data.data.id;\n sessions.set(ticketId, {\n token: data.data.guestToken,\n name: requesterName || 'Visitante',\n });\n\n // The first message is saved with the ticket; read it back for its id.\n // /support/external takes no files, so they go as the next message.\n let messages = await listMessages(ticketId);\n if (!message && messages[0]) {\n hiddenIds.add(messages[0].id);\n messages = messages.slice(1);\n }\n if (files.length) messages.push(await sendMessage(ticketId, '', files));\n return {\n ticket: {\n id: ticketId,\n status: SUPPORT_CHAT_STATUS.ABERTO,\n handlerType: null,\n problemType: null,\n },\n messages,\n };\n },\n\n sendMessage,\n\n async getArticle(institutionId, articleId) {\n const { data } = await apiClient.get<{\n data: { article: SupportChatArticle };\n }>(`/help-center/${institutionId}/articles/${articleId}`);\n const { id, title, body, updatedAt } = data.data.article;\n return { id, title, body, updatedAt: updatedAt ?? null };\n },\n\n async updateContact(ticketId, contact) {\n if (!apiClient.put) {\n throw new Error('O cliente da API não envia PUT');\n }\n await apiClient.put(`/support/guest/${ticketId}`, contact, {\n headers: headersFor(ticketId),\n });\n const session = sessions.get(ticketId);\n if (session && contact.requesterName) {\n session.name = contact.requesterName;\n }\n },\n\n listMessages,\n\n subscribe(ticketId, listener) {\n // id → what the chat shows besides the text; a message seen again with\n // more of it (a stale WebSocket history may lack contactRequest/faq) is\n // delivered again so the chat replaces it\n const seen = new Map<string, string>();\n const extrasOf = (message: SupportChatMessage) =>\n JSON.stringify([message.contactRequest ?? null, message.faq ?? null]);\n const NO_EXTRAS = JSON.stringify([null, null]);\n let socket: WebSocket | null = null;\n let pollTimer: ReturnType<typeof setInterval> | null = null;\n let active = true;\n\n const deliver = (message: SupportChatMessage) => {\n if (!isVisible(message)) return;\n const extras = extrasOf(message);\n const previous = seen.get(message.id);\n // Same as before, or a poorer copy: nothing new to show\n if (previous === extras || (previous && extras === NO_EXTRAS)) return;\n seen.set(message.id, extras);\n listener({ type: 'new_message', payload: { message } });\n };\n\n const poll = async () => {\n try {\n (await listMessages(ticketId)).forEach(deliver);\n } catch {\n // Next tick tries again\n }\n };\n const startPolling = () => {\n if (pollTimer || !active) return;\n void poll();\n pollTimer = setInterval(poll, pollIntervalMs);\n };\n const stopPolling = () => {\n if (pollTimer) clearInterval(pollTimer);\n pollTimer = null;\n };\n\n const handleFrame = (raw: string) => {\n let event: { type?: string; payload?: Record<string, unknown> };\n try {\n event = JSON.parse(raw);\n } catch {\n return;\n }\n const payload = event.payload ?? {};\n if (event.type === 'history') {\n (payload.messages as SupportChatMessage[]).forEach((message) =>\n deliver({ ...message, attachments: message.attachments ?? [] })\n );\n } else if (event.type === 'new_message') {\n const message = payload.message as SupportChatMessage;\n deliver({ ...message, attachments: message.attachments ?? [] });\n } else if (\n event.type === 'message_updated' ||\n event.type === 'ticket_updated'\n ) {\n listener(event as SupportChatEvent);\n } else if (event.type === 'error') {\n listener({\n type: 'error',\n payload: {\n message: String(payload.message_text ?? payload.message ?? ''),\n },\n });\n }\n };\n\n const token = sessions.get(ticketId)?.token;\n if (wsUrl && token) {\n const url = new URL(wsUrl);\n url.searchParams.set('channel', 'support');\n url.searchParams.set('roomId', ticketId);\n url.searchParams.set('token', token);\n socket = new WebSocket(url.toString());\n socket.onopen = () => {\n stopPolling();\n socket?.send(JSON.stringify({ type: 'getInitialData' }));\n // The REST API is the source of truth: a WebSocket deployed apart\n // from it may send a history without contactRequest/faq\n void poll();\n };\n socket.onmessage = (event) => handleFrame(String(event.data));\n socket.onclose = startPolling;\n socket.onerror = startPolling;\n } else {\n startPolling();\n }\n\n return () => {\n active = false;\n stopPolling();\n if (socket) {\n socket.onclose = null;\n socket.close();\n }\n };\n },\n };\n};\n","import {\n SUPPORT_CHAT_AUTHOR,\n SUPPORT_CHAT_GUEST_SENDER_ID,\n SUPPORT_CHAT_HANDLER,\n SUPPORT_CHAT_STATUS,\n type SupportChatAdapter,\n type SupportChatEvent,\n type SupportChatMessage,\n type SupportChatTicket,\n} from '../../types/supportChat';\n\nexport interface MockSupportChatAdapterOptions {\n /** Latency of every call, in ms (default 300) */\n delayMs?: number;\n /** Delay of the automatic agent reply, in ms (default 1500) */\n replyDelayMs?: number;\n /** Text of the agent reply; null disables it */\n replyText?: string | null;\n}\n\nconst DEFAULT_REPLY =\n 'Olá! Recebemos sua mensagem e já vamos te ajudar. Este é um atendimento de demonstração.';\n\n/**\n * In-memory adapter for the support chat widget. It mirrors the backend\n * contract (statuses, authors, events) so the widget can be built and tested\n * before the real API + WebSocket adapter exists.\n */\nexport const createMockSupportChatAdapter = ({\n delayMs = 300,\n replyDelayMs = 1500,\n replyText = DEFAULT_REPLY,\n}: MockSupportChatAdapterOptions = {}): SupportChatAdapter => {\n const tickets = new Map<string, SupportChatTicket>();\n const messages = new Map<string, SupportChatMessage[]>();\n const requesters = new Map<string, { name: string; logged: boolean }>();\n const listeners = new Map<string, Set<(event: SupportChatEvent) => void>>();\n let sequence = 0;\n\n const wait = () => new Promise((resolve) => setTimeout(resolve, delayMs));\n const nextId = (prefix: string) => `${prefix}-${++sequence}`;\n\n const emit = (ticketId: string, event: SupportChatEvent) => {\n listeners.get(ticketId)?.forEach((listener) => listener(event));\n };\n\n const append = (ticketId: string, message: SupportChatMessage) => {\n messages.set(ticketId, [...(messages.get(ticketId) ?? []), message]);\n };\n\n const toAttachments = (files: File[]) =>\n files.map((file) => ({\n id: nextId('file'),\n url: URL.createObjectURL(file),\n fileName: file.name,\n mimeType: file.type,\n size: file.size,\n }));\n\n const buildRequesterMessage = (\n ticketId: string,\n content: string,\n files: File[] = []\n ): SupportChatMessage => {\n const requester = requesters.get(ticketId);\n return {\n id: nextId('msg'),\n senderId: requester?.logged ? 'user' : SUPPORT_CHAT_GUEST_SENDER_ID,\n senderName: requester?.name ?? 'Visitante',\n senderPhoto: null,\n senderRole: requester?.logged\n ? SUPPORT_CHAT_AUTHOR.USER\n : SUPPORT_CHAT_AUTHOR.GUEST,\n content,\n messageType: 'text',\n createdAt: new Date().toISOString(),\n attachments: toAttachments(files),\n };\n };\n\n const scheduleReply = (ticketId: string) => {\n if (replyText === null) return;\n setTimeout(() => {\n const ticket = tickets.get(ticketId);\n if (!ticket) return;\n\n const reply: SupportChatMessage = {\n id: nextId('msg'),\n senderId: 'agent',\n senderName: 'Suporte',\n senderPhoto: null,\n senderRole: SUPPORT_CHAT_AUTHOR.AGENT,\n content: replyText,\n messageType: 'text',\n createdAt: new Date().toISOString(),\n attachments: [],\n };\n append(ticketId, reply);\n\n const updated: SupportChatTicket = {\n ...ticket,\n status: SUPPORT_CHAT_STATUS.EM_ANDAMENTO,\n handlerType: SUPPORT_CHAT_HANDLER.HUMANO,\n };\n tickets.set(ticketId, updated);\n\n emit(ticketId, { type: 'new_message', payload: { message: reply } });\n emit(ticketId, {\n type: 'ticket_updated',\n payload: {\n ticketId,\n status: updated.status,\n handlerType: updated.handlerType,\n problemType: updated.problemType,\n },\n });\n }, replyDelayMs);\n };\n\n return {\n async getArticle(_institutionId, articleId) {\n await wait();\n return {\n id: articleId,\n title: 'Como gerar e localizar simulados',\n body:\n '<p>Monte um simulado com as matérias que quiser e acompanhe o resultado. 📝</p>' +\n '<h2>Passo a passo</h2>' +\n '<ol><li><p>No menu, abra <strong>Simulados</strong>.</p></li>' +\n '<li><p>Clique em <strong>Gerar simulado</strong> e escolha as áreas.</p></li>' +\n '<li><p>Responda e veja o resultado em <strong>Histórico</strong>.</p></li></ol>' +\n '<p><a href=\"https://www.youtube.com/watch?v=dQw4w9WgXcQ\">Assista ao vídeo do passo a passo</a></p>' +\n '<blockquote><p>💡 Dica: refaça as questões que errou.</p></blockquote>',\n updatedAt: '2026-10-21T12:00:00.000Z',\n };\n },\n\n async startConversation({ message, requesterName, email, files }) {\n await wait();\n const ticket: SupportChatTicket = {\n id: nextId('ticket'),\n status: SUPPORT_CHAT_STATUS.ABERTO,\n handlerType: null,\n problemType: null,\n };\n tickets.set(ticket.id, ticket);\n requesters.set(ticket.id, {\n name: requesterName || 'Visitante',\n logged: Boolean(email),\n });\n\n const first = buildRequesterMessage(ticket.id, message, files);\n append(ticket.id, first);\n scheduleReply(ticket.id);\n return { ticket, messages: [first] };\n },\n\n async sendMessage(ticketId, content, files) {\n await wait();\n const ticket = tickets.get(ticketId);\n if (!ticket) throw new Error('Atendimento não encontrado');\n if (ticket.status === SUPPORT_CHAT_STATUS.FECHADO) {\n throw new Error('Este atendimento já foi encerrado');\n }\n\n const message = buildRequesterMessage(ticketId, content, files);\n append(ticketId, message);\n scheduleReply(ticketId);\n return message;\n },\n\n async listMessages(ticketId) {\n await wait();\n return [...(messages.get(ticketId) ?? [])];\n },\n\n subscribe(ticketId, listener) {\n const set = listeners.get(ticketId) ?? new Set();\n set.add(listener);\n listeners.set(ticketId, set);\n return () => {\n set.delete(listener);\n };\n },\n };\n};\n"]}
@@ -48,6 +48,15 @@ var getCategoryText = (category) => {
48
48
  return category;
49
49
  }
50
50
  };
51
+ var SUPPORT_TICKET_ORIGIN = {
52
+ LOGIN: "login",
53
+ STUDENT: "aluno",
54
+ TEACHER: "professor",
55
+ MANAGER: "gestor",
56
+ FAQ: "faq",
57
+ APP: "app"
58
+ };
59
+ var isSupportTicketOrigin = (value) => Object.values(SUPPORT_TICKET_ORIGIN).includes(value);
51
60
  var mapApiStatusToInternal = (apiStatus) => {
52
61
  switch (apiStatus) {
53
62
  case "ABERTO":
@@ -84,8 +93,10 @@ export {
84
93
  getStatusBadgeAction,
85
94
  getStatusText,
86
95
  getCategoryText,
96
+ SUPPORT_TICKET_ORIGIN,
97
+ isSupportTicketOrigin,
87
98
  mapApiStatusToInternal,
88
99
  mapInternalStatusToApi,
89
100
  SupportType
90
101
  };
91
- //# sourceMappingURL=chunk-OMBS4ACE.mjs.map
102
+ //# sourceMappingURL=chunk-MVT7IVMY.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types/support.ts"],"sourcesContent":["export enum SupportStatus {\n ABERTO = 'aberto',\n RESPONDIDO = 'respondido',\n ENCERRADO = 'encerrado',\n}\n\nexport enum SupportCategory {\n ACESSO = 'acesso',\n TECNICO = 'tecnico',\n OUTROS = 'outros',\n}\n\nexport type TicketStatus = SupportStatus;\nexport type ProblemType = SupportCategory | null;\nexport type TabType = 'criar-pedido' | 'historico';\n\nexport interface SupportResponse {\n id: string;\n receivedAt: string;\n message: string;\n}\n\nexport interface SupportTicket {\n id: string;\n title: string;\n status: TicketStatus;\n createdAt: string;\n category: ProblemType;\n description?: string;\n response?: SupportResponse;\n}\n\nexport const getStatusBadgeAction = (\n status: SupportStatus\n): 'success' | 'error' | 'warning' | 'info' => {\n switch (status) {\n case SupportStatus.ABERTO:\n return 'success';\n case SupportStatus.RESPONDIDO:\n return 'warning';\n case SupportStatus.ENCERRADO:\n return 'info';\n default:\n return 'info';\n }\n};\n\nexport const getStatusText = (status: SupportStatus): string => {\n switch (status) {\n case SupportStatus.ABERTO:\n return 'Aberto';\n case SupportStatus.RESPONDIDO:\n return 'Respondido';\n case SupportStatus.ENCERRADO:\n return 'Encerrado';\n default:\n return status;\n }\n};\n\nexport const getCategoryText = (category: SupportCategory | null): string => {\n if (!category) return '';\n switch (category) {\n case SupportCategory.ACESSO:\n return 'Acesso';\n case SupportCategory.TECNICO:\n return 'Técnico';\n case SupportCategory.OUTROS:\n return 'Outros';\n default:\n return category;\n }\n};\n\n// API Types\n/**\n * App a ticket is opened from, saved on the ticket (`origin`). Same values the\n * apps pass as the support chat `channel`.\n */\nexport const SUPPORT_TICKET_ORIGIN = {\n LOGIN: 'login',\n STUDENT: 'aluno',\n TEACHER: 'professor',\n MANAGER: 'gestor',\n FAQ: 'faq',\n APP: 'app',\n} as const;\n\nexport type SupportTicketOrigin =\n (typeof SUPPORT_TICKET_ORIGIN)[keyof typeof SUPPORT_TICKET_ORIGIN];\n\n/**\n * Whether a value is a ticket origin the API accepts.\n * @param value - e.g. the support chat `channel`\n */\nexport const isSupportTicketOrigin = (\n value: unknown\n): value is SupportTicketOrigin =>\n Object.values(SUPPORT_TICKET_ORIGIN).includes(value as SupportTicketOrigin);\n\nexport interface CreateSupportTicketRequest {\n subject: string;\n description: string;\n type: string;\n /** App the ticket is opened from */\n origin?: SupportTicketOrigin;\n}\n\nexport interface CreateSupportTicketResponse {\n message: string;\n data: {\n id: string;\n ownerId: string;\n type: string;\n email: string;\n subject: string;\n description: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n };\n}\n\nexport interface SupportTicketAPI {\n id: string;\n ownerId: string;\n type: string;\n email: string;\n subject: string;\n description: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface SupportPagination {\n page: number;\n limit: number;\n total: number;\n totalPages: number;\n hasNext: boolean;\n hasPrev: boolean;\n}\n\nexport interface GetSupportTicketsResponse {\n message: string;\n data: {\n support: SupportTicketAPI[];\n pagination: SupportPagination;\n };\n}\n\nexport interface SupportAnswerAPI {\n id: string;\n userId: string;\n supportId: string;\n answer: string;\n read: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface GetSupportAnswersResponse {\n message: string;\n data: SupportAnswerAPI[];\n}\n\nexport interface SubmitSupportAnswerRequest {\n userId: string;\n supportId: string;\n answer: string;\n}\n\nexport interface SubmitSupportAnswerResponse {\n message: string;\n data: SupportAnswerAPI;\n}\n\n// Mapping functions for API status to internal status\nexport const mapApiStatusToInternal = (apiStatus: string): SupportStatus => {\n switch (apiStatus) {\n case 'ABERTO':\n return SupportStatus.ABERTO;\n case 'PENDENTE':\n return SupportStatus.RESPONDIDO;\n case 'FECHADO':\n return SupportStatus.ENCERRADO;\n default:\n return SupportStatus.ABERTO;\n }\n};\n\nexport const mapInternalStatusToApi = (\n internalStatus: SupportStatus\n): string => {\n switch (internalStatus) {\n case SupportStatus.ABERTO:\n return 'ABERTO';\n case SupportStatus.RESPONDIDO:\n return 'PENDENTE';\n case SupportStatus.ENCERRADO:\n return 'FECHADO';\n default:\n return 'ABERTO';\n }\n};\n\n// API Client interface for dependency injection\nexport interface SupportApiClient {\n get: <T>(url: string) => Promise<{ data: T }>;\n post: <T>(url: string, data?: unknown) => Promise<{ data: T }>;\n patch: <T>(url: string, data?: unknown) => Promise<{ data: T }>;\n}\n\n// Feature Flag Support Types\nexport enum SupportType {\n NATIVE = 'NATIVE',\n ZENDESK = 'ZENDESK',\n}\n\n/**\n * Chaves do Zendesk por instituição, guardadas na `version` da flag SUPPORT.\n *\n * São três porque o Zendesk emite uma chave por canal, e web e mobile não\n * compartilham canal: `zendeskKey` é o Web Widget (UUID) e as duas\n * `zendeskChannelKey*` são o SDK mobile (blob base64, um por plataforma). Os\n * fronts web só usam a primeira; as outras existem no tipo porque a mesma\n * flag alimenta o app.\n *\n * Todas são opcionais: a flag pode estar em NATIVE, ou em ZENDESK com a chave\n * ainda não preenchida no backoffice. Sem chave o widget não carrega — não há\n * fallback para uma conta global.\n */\nexport interface SupportZendeskKeys {\n zendeskKey?: string;\n zendeskChannelKeyIos?: string;\n zendeskChannelKeyAndroid?: string;\n}\n\nexport interface SupportFeatureFlags {\n institutionId: string;\n page: string;\n version: {\n supportType: SupportType;\n /** Address of the institution FAQ, e.g. https://ajuda.escola.com.br */\n faqUrl?: string;\n /** \"Suporte interno ativo\" in the backoffice; absent means on */\n enabled?: boolean;\n /** Native chat widget switch in the backoffice; absent means on */\n chat?: boolean;\n } & SupportZendeskKeys;\n}\n"],"mappings":";AAAO,IAAK,gBAAL,kBAAKA,mBAAL;AACL,EAAAA,eAAA,YAAS;AACT,EAAAA,eAAA,gBAAa;AACb,EAAAA,eAAA,eAAY;AAHF,SAAAA;AAAA,GAAA;AAML,IAAK,kBAAL,kBAAKC,qBAAL;AACL,EAAAA,iBAAA,YAAS;AACT,EAAAA,iBAAA,aAAU;AACV,EAAAA,iBAAA,YAAS;AAHC,SAAAA;AAAA,GAAA;AA0BL,IAAM,uBAAuB,CAClC,WAC6C;AAC7C,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEO,IAAM,gBAAgB,CAAC,WAAkC;AAC9D,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEO,IAAM,kBAAkB,CAAC,aAA6C;AAC3E,MAAI,CAAC,SAAU,QAAO;AACtB,UAAQ,UAAU;AAAA,IAChB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAOO,IAAM,wBAAwB;AAAA,EACnC,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,KAAK;AAAA,EACL,KAAK;AACP;AASO,IAAM,wBAAwB,CACnC,UAEA,OAAO,OAAO,qBAAqB,EAAE,SAAS,KAA4B;AAiFrE,IAAM,yBAAyB,CAAC,cAAqC;AAC1E,UAAQ,WAAW;AAAA,IACjB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEO,IAAM,yBAAyB,CACpC,mBACW;AACX,UAAQ,gBAAgB;AAAA,IACtB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAUO,IAAK,cAAL,kBAAKC,iBAAL;AACL,EAAAA,aAAA,YAAS;AACT,EAAAA,aAAA,aAAU;AAFA,SAAAA;AAAA,GAAA;","names":["SupportStatus","SupportCategory","SupportType"]}
@@ -21,6 +21,9 @@ import {
21
21
  SUPPORT_CHAT_MAX_ATTACHMENTS,
22
22
  SUPPORT_CHAT_STATUS
23
23
  } from "./chunk-7ZB3YHF2.mjs";
24
+ import {
25
+ isSupportTicketOrigin
26
+ } from "./chunk-MVT7IVMY.mjs";
24
27
  import {
25
28
  Input_default
26
29
  } from "./chunk-ZRBIE5ZM.mjs";
@@ -1112,6 +1115,7 @@ var createGuestSupportChatAdapter = ({
1112
1115
  return {
1113
1116
  async startConversation({
1114
1117
  message,
1118
+ channel,
1115
1119
  institutionId,
1116
1120
  requesterName,
1117
1121
  email,
@@ -1122,7 +1126,9 @@ var createGuestSupportChatAdapter = ({
1122
1126
  description,
1123
1127
  ...institutionId ? { institutionId } : {},
1124
1128
  ...requesterName ? { requesterName } : {},
1125
- ...email ? { email } : {}
1129
+ ...email ? { email } : {},
1130
+ // The app the chat runs in, when it is one the API knows
1131
+ ...isSupportTicketOrigin(channel) ? { origin: channel } : {}
1126
1132
  });
1127
1133
  const ticketId = data.data.id;
1128
1134
  sessions.set(ticketId, {
@@ -1427,4 +1433,4 @@ export {
1427
1433
  NativeSupportChat,
1428
1434
  createMockSupportChatAdapter
1429
1435
  };
1430
- //# sourceMappingURL=chunk-KBWT73XY.mjs.map
1436
+ //# sourceMappingURL=chunk-OES54VU6.mjs.map