@flamingo-stack/openframe-frontend-core 0.0.718 → 0.0.720

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 (175) hide show
  1. package/dist/{chunk-MIQLUUVF.cjs → chunk-2C72EWQR.cjs} +93 -29
  2. package/dist/{chunk-IWCPXJEE.js → chunk-3HZ6XSZC.js} +2 -4
  3. package/dist/{chunk-ZXCZYMN4.cjs → chunk-4SIZCHVZ.cjs} +13 -15
  4. package/dist/{chunk-BOTM373R.cjs → chunk-5XDCCQ4A.cjs} +15 -15
  5. package/dist/{chunk-DXCBOZVT.cjs → chunk-6BLN5WDS.cjs} +41 -41
  6. package/dist/{chunk-DCD5WZUF.js → chunk-6W46LVK4.js} +5 -5
  7. package/dist/{chunk-JHQ334WS.js → chunk-742BGXNX.js} +2 -0
  8. package/dist/{chunk-AECTRMGQ.cjs → chunk-7GRPR7XA.cjs} +13 -1
  9. package/dist/{chunk-45F7PXUI.js → chunk-7HYEQKN6.js} +6 -6
  10. package/dist/{chunk-N2PF5CFX.js → chunk-7IOU6BJE.js} +12 -11
  11. package/dist/{chunk-BWIN42ZA.cjs → chunk-7LPR4RZ3.cjs} +34 -36
  12. package/dist/chunk-AFTRPEXO.js +13 -0
  13. package/dist/{chunk-JPD4NXH6.js → chunk-AZRI4XBP.js} +1 -1
  14. package/dist/{chunk-4QFWGLZO.js → chunk-B2N4YX66.js} +1 -1
  15. package/dist/{chunk-QXZCLQNV.js → chunk-B5LNX4GZ.js} +2 -2
  16. package/dist/{chunk-GN4A7PMB.cjs → chunk-DHNJSQGX.cjs} +91 -91
  17. package/dist/{chunk-NDZ5ZXOQ.cjs → chunk-DOPOHO6H.cjs} +20 -20
  18. package/dist/{chunk-Z3CVSPO3.cjs → chunk-DYDDAA6O.cjs} +519 -518
  19. package/dist/{chunk-2RKFMUNW.cjs → chunk-EVMM3JE6.cjs} +64 -63
  20. package/dist/chunk-EWPR7662.cjs +13 -0
  21. package/dist/{chunk-AGVXKD4K.js → chunk-FSQQZFVG.js} +12 -0
  22. package/dist/{chunk-TTVOLEUS.js → chunk-GANIUMRU.js} +89 -2
  23. package/dist/{chunk-S2FUFMLW.cjs → chunk-HFEYEPOG.cjs} +27 -27
  24. package/dist/{chunk-LE5WKGJV.cjs → chunk-HTS46FFY.cjs} +3 -3
  25. package/dist/{chunk-6A43ZDWV.cjs → chunk-IL3ZWNM2.cjs} +4 -4
  26. package/dist/{chunk-PSVSEN4J.cjs → chunk-IMTY3GRR.cjs} +2 -0
  27. package/dist/{chunk-YC5COB5V.cjs → chunk-ISCZ6UNF.cjs} +85 -89
  28. package/dist/{chunk-XGHGGBM3.js → chunk-K2BYVPGR.js} +3 -3
  29. package/dist/{chunk-HWVUR735.cjs → chunk-KSGMZHVV.cjs} +2 -2
  30. package/dist/{chunk-SGFTTJKO.js → chunk-LO3KCLEF.js} +1 -1
  31. package/dist/{chunk-MA4Q5RBP.cjs → chunk-NYSMDM4J.cjs} +2 -2
  32. package/dist/{chunk-67QBMN4R.js → chunk-O3MKJU6A.js} +6 -6
  33. package/dist/{chunk-APJZBDQN.js → chunk-PSMJYZMV.js} +27 -26
  34. package/dist/{chunk-JH2SIWPQ.js → chunk-PXAKSPLZ.js} +1586 -1263
  35. package/dist/{chunk-TBCZCRQ5.js → chunk-Q5327DXK.js} +13 -17
  36. package/dist/{chunk-EZC2F5OT.cjs → chunk-QF464PPL.cjs} +39 -39
  37. package/dist/{chunk-UWQWMGJ7.cjs → chunk-RDP5UZXQ.cjs} +888 -565
  38. package/dist/{chunk-ETYLOZOB.cjs → chunk-RFUOIQSG.cjs} +100 -13
  39. package/dist/{chunk-DC4RTYWK.cjs → chunk-RZQGIWK3.cjs} +3 -3
  40. package/dist/{chunk-PQ6HBLZ7.js → chunk-TORERAXD.js} +105 -41
  41. package/dist/{chunk-6YPTUHM7.js → chunk-URW23QUM.js} +1 -1
  42. package/dist/{chunk-QBCDSTLE.cjs → chunk-UWJMDFDO.cjs} +18 -18
  43. package/dist/{chunk-YEMCCYIF.js → chunk-V6B2FBZK.js} +5 -5
  44. package/dist/{chunk-JIWMBIVZ.js → chunk-VBHGJFA3.js} +10 -10
  45. package/dist/{chunk-Z5UWW3Z2.cjs → chunk-VF2CJXZ2.cjs} +81 -65
  46. package/dist/{chunk-DEZDMTBM.js → chunk-VYVPII4W.js} +8 -10
  47. package/dist/{chunk-MM7MCIXL.js → chunk-VYZCKY5K.js} +9 -9
  48. package/dist/{chunk-E7YCPY7K.cjs → chunk-XSM46FHD.cjs} +3 -3
  49. package/dist/{chunk-NZ5BHDFZ.js → chunk-XW3Q623Q.js} +1 -1
  50. package/dist/{chunk-LOYPRJWA.js → chunk-YK3X74SF.js} +67 -51
  51. package/dist/{chunk-WALCXC7J.cjs → chunk-ZHD3F2TH.cjs} +3 -3
  52. package/dist/{chunk-KBJJQFYU.js → chunk-ZPHOQDAE.js} +1 -1
  53. package/dist/{cloud-tenants-TAKOWXLD.js → cloud-tenants-RKQRSDNE.js} +9 -9
  54. package/dist/{cloud-tenants-PFDF7O5U.cjs → cloud-tenants-UQZASXUB.cjs} +10 -10
  55. package/dist/components/case-studies/index.cjs +23 -23
  56. package/dist/components/case-studies/index.js +10 -10
  57. package/dist/components/chat/ask-prompts.d.ts +10 -2
  58. package/dist/components/chat/ask-prompts.d.ts.map +1 -1
  59. package/dist/components/chat/assistant-ask-prompts.d.ts +27 -0
  60. package/dist/components/chat/assistant-ask-prompts.d.ts.map +1 -0
  61. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  62. package/dist/components/chat/hooks/index.d.ts +1 -0
  63. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  64. package/dist/components/chat/hooks/use-ask-prompts.d.ts +63 -0
  65. package/dist/components/chat/hooks/use-ask-prompts.d.ts.map +1 -0
  66. package/dist/components/chat/index.cjs +23 -6
  67. package/dist/components/chat/index.d.ts +1 -0
  68. package/dist/components/chat/index.d.ts.map +1 -1
  69. package/dist/components/chat/index.js +22 -5
  70. package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
  71. package/dist/components/contact/index.cjs +11 -11
  72. package/dist/components/contact/index.js +10 -10
  73. package/dist/components/docs/index.cjs +14 -14
  74. package/dist/components/docs/index.js +13 -13
  75. package/dist/components/embeds/index.cjs +11 -11
  76. package/dist/components/embeds/index.js +10 -10
  77. package/dist/components/faq/faq-ask-card.d.ts +44 -0
  78. package/dist/components/faq/faq-ask-card.d.ts.map +1 -0
  79. package/dist/components/faq/faq-document-page.d.ts +5 -1
  80. package/dist/components/faq/faq-document-page.d.ts.map +1 -1
  81. package/dist/components/faq/faq-section.d.ts +18 -5
  82. package/dist/components/faq/faq-section.d.ts.map +1 -1
  83. package/dist/components/faq/index.cjs +13 -12
  84. package/dist/components/faq/index.d.ts +1 -0
  85. package/dist/components/faq/index.d.ts.map +1 -1
  86. package/dist/components/faq/index.js +12 -11
  87. package/dist/components/features/auth/auth-benefits-panel.d.ts.map +1 -1
  88. package/dist/components/features/index.cjs +10 -10
  89. package/dist/components/features/index.js +9 -9
  90. package/dist/components/features/remote-session/remote-desktop-view.d.ts +6 -2
  91. package/dist/components/features/remote-session/remote-desktop-view.d.ts.map +1 -1
  92. package/dist/components/help-center-pages/index.cjs +104 -105
  93. package/dist/components/help-center-pages/index.js +28 -29
  94. package/dist/components/help-center-pages/trust-center-page.d.ts +4 -1
  95. package/dist/components/help-center-pages/trust-center-page.d.ts.map +1 -1
  96. package/dist/components/index.cjs +109 -197
  97. package/dist/components/index.js +228 -316
  98. package/dist/components/meeting-scheduler/index.cjs +50 -51
  99. package/dist/components/meeting-scheduler/index.js +12 -13
  100. package/dist/components/navigation/index.cjs +13 -13
  101. package/dist/components/navigation/index.js +12 -12
  102. package/dist/components/navigation/mingo-ai-button.d.ts +1 -1
  103. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  104. package/dist/components/onboarding-guides/index.cjs +13 -14
  105. package/dist/components/onboarding-guides/index.js +12 -13
  106. package/dist/components/product-demo/index.cjs +7 -7
  107. package/dist/components/product-demo/index.js +7 -7
  108. package/dist/components/related-content/index.cjs +5 -7
  109. package/dist/components/related-content/index.js +4 -6
  110. package/dist/components/related-content/related-content-section.d.ts +6 -6
  111. package/dist/components/related-content/related-content-section.d.ts.map +1 -1
  112. package/dist/components/tickets/index.cjs +15 -15
  113. package/dist/components/tickets/index.js +14 -14
  114. package/dist/components/ui/index.cjs +26 -10
  115. package/dist/components/ui/index.js +25 -9
  116. package/dist/contexts/assistant-runtime-context.d.ts +74 -0
  117. package/dist/contexts/assistant-runtime-context.d.ts.map +1 -0
  118. package/dist/contexts/index.cjs +7 -1
  119. package/dist/contexts/index.d.ts +1 -0
  120. package/dist/contexts/index.d.ts.map +1 -1
  121. package/dist/contexts/index.js +6 -0
  122. package/dist/{devices-KRCGZMGA.cjs → devices-7Q2EUM7B.cjs} +11 -11
  123. package/dist/{devices-LHEX7O5T.js → devices-U3Y4IIW4.js} +9 -9
  124. package/dist/hooks/index.cjs +3 -4
  125. package/dist/hooks/index.js +2 -3
  126. package/dist/hooks/use-self-fetch.d.ts +3 -0
  127. package/dist/hooks/use-self-fetch.d.ts.map +1 -1
  128. package/dist/index.cjs +34 -14
  129. package/dist/index.js +33 -13
  130. package/dist/{knowledge-RZSFNR33.js → knowledge-NST53EXE.js} +9 -9
  131. package/dist/{knowledge-KLVPVEYZ.cjs → knowledge-UYIZ74MA.cjs} +11 -11
  132. package/dist/{logs-OX3XTVL2.cjs → logs-HTH7X7KV.cjs} +18 -18
  133. package/dist/{logs-E5DOV5JO.js → logs-JVTRG232.js} +9 -9
  134. package/dist/{policies-MC2S4S5G.js → policies-DTXTRK56.js} +9 -9
  135. package/dist/{policies-FOSCIZGK.cjs → policies-MXK2HB4W.cjs} +14 -14
  136. package/dist/{remote-session-JQ2BPIE3.js → remote-session-LAJZKQBN.js} +3 -3
  137. package/dist/{remote-session-DRFE5XZE.cjs → remote-session-SKKF4PBV.cjs} +5 -5
  138. package/dist/{software-update-D4OEIH65.js → software-update-6RHMPM3W.js} +9 -9
  139. package/dist/{software-update-HOBSTWVO.cjs → software-update-P6BSM6HX.cjs} +14 -14
  140. package/dist/utils/index.cjs +26 -2
  141. package/dist/utils/index.d.ts +1 -0
  142. package/dist/utils/index.d.ts.map +1 -1
  143. package/dist/utils/index.js +25 -3
  144. package/dist/utils/list-url.d.ts.map +1 -1
  145. package/dist/utils/source-icons.d.ts.map +1 -1
  146. package/dist/utils/trial.d.ts +18 -0
  147. package/dist/utils/trial.d.ts.map +1 -0
  148. package/package.json +1 -1
  149. package/src/components/chat/ask-prompts.tsx +25 -3
  150. package/src/components/chat/assistant-ask-prompts.tsx +78 -0
  151. package/src/components/chat/entity-cards/dispatch.tsx +38 -4
  152. package/src/components/chat/hooks/index.ts +12 -0
  153. package/src/components/chat/hooks/use-ask-prompts.ts +188 -0
  154. package/src/components/chat/index.ts +1 -0
  155. package/src/components/chat/utils/icon-library.tsx +6 -0
  156. package/src/components/faq/faq-ask-card.tsx +115 -0
  157. package/src/components/faq/faq-document-page.tsx +6 -1
  158. package/src/components/faq/faq-section.tsx +27 -5
  159. package/src/components/faq/index.ts +1 -0
  160. package/src/components/features/auth/auth-benefits-panel.tsx +5 -4
  161. package/src/components/features/remote-session/remote-desktop-view.tsx +86 -63
  162. package/src/components/help-center-pages/trust-center-page.tsx +5 -2
  163. package/src/components/navigation/mingo-ai-button.tsx +27 -6
  164. package/src/components/related-content/related-content-section.tsx +46 -47
  165. package/src/contexts/assistant-runtime-context.tsx +84 -0
  166. package/src/contexts/index.ts +9 -0
  167. package/src/hooks/use-self-fetch.ts +7 -0
  168. package/src/utils/index.ts +1 -0
  169. package/src/utils/list-url.ts +4 -0
  170. package/src/utils/source-icons.ts +10 -2
  171. package/src/utils/trial.ts +19 -0
  172. package/dist/chunk-AUMKE74M.cjs +0 -16
  173. package/dist/chunk-G3GRYDMS.js +0 -16
  174. package/dist/chunk-GEDY7HQI.cjs +0 -85
  175. package/dist/chunk-YBQQNDC7.js +0 -85
@@ -4,6 +4,7 @@ import type { ReactNode } from 'react';
4
4
  import {
5
5
  ChatOffIcon,
6
6
  ChatTextIcon,
7
+ Chevron02DownIcon,
7
8
  Collapse02Icon,
8
9
  Expand02Icon,
9
10
  MonitorIcon,
@@ -19,8 +20,12 @@ import { RemoteDesktopFullscreenToolbar } from './remote-desktop-fullscreen-tool
19
20
  /** The page chrome both the view and its skeleton render in. */
20
21
  const PAGE_CLASS = 'h-full overflow-hidden px-[var(--spacing-system-l)] pb-[var(--spacing-system-l)]';
21
22
 
23
+ /** The header card: the device row, and under it the display row when there is one to pick. */
22
24
  const CONTROLS_BAR_CLASS =
23
- 'flex flex-shrink-0 items-center justify-between gap-[var(--spacing-system-mf)] rounded-md border border-ods-border bg-ods-card px-[var(--spacing-system-mf)] py-[var(--spacing-system-xs)]';
25
+ 'flex flex-shrink-0 flex-col overflow-hidden rounded-md border border-ods-border bg-ods-card';
26
+
27
+ const CONTROLS_ROW_CLASS =
28
+ 'flex items-center justify-between gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)] py-[var(--spacing-system-xs)]';
24
29
 
25
30
  export interface RemoteDesktopViewProps {
26
31
  /** First line of the device header. */
@@ -36,9 +41,13 @@ export interface RemoteDesktopViewProps {
36
41
  fullscreen?: boolean;
37
42
  onEnterFullscreen: () => void;
38
43
  onExitFullscreen: () => void;
39
- /** The display switcher's menu; the switcher is hidden while this is empty (a single display). */
44
+ /**
45
+ * The display switcher's menu, one entry per separate display. The switcher is
46
+ * the full-width row under the device row (Figma 2155:109503), and it is left
47
+ * out while this is empty: one display, or an agent that reports none.
48
+ */
40
49
  displayMenuGroups?: ActionsMenuGroup[];
41
- /** The switcher's label, e.g. "Display All" or "Display 2". */
50
+ /** The switcher's label, e.g. "Display 2", or "Display" before the agent has named one. */
42
51
  currentDisplayLabel: string;
43
52
  actionsMenuGroups: ActionsMenuGroup[];
44
53
  onOpenSettings: () => void;
@@ -86,61 +95,73 @@ export function RemoteDesktopView({
86
95
  }: RemoteDesktopViewProps) {
87
96
  const controlsBar = (
88
97
  <div className={CONTROLS_BAR_CLASS}>
89
- <div className="flex min-w-0 items-center gap-[var(--spacing-system-mf)]">
90
- <div className="flex-shrink-0 rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xsf)]">
91
- <MonitorIcon className="h-4 w-4 text-ods-text-primary" />
92
- </div>
93
- <div className="flex min-w-0 flex-col">
94
- <TruncateText>{deviceName}</TruncateText>
95
- <TruncateText
96
- variant="h6"
97
- tone="secondary"
98
- >{`Desktop • ${organizationName || 'Unknown Customer'}`}</TruncateText>
98
+ <div className={CONTROLS_ROW_CLASS}>
99
+ <div className="flex min-w-0 items-center gap-[var(--spacing-system-mf)]">
100
+ <div className="flex-shrink-0 rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xsf)]">
101
+ <MonitorIcon className="h-4 w-4 text-ods-text-primary" />
102
+ </div>
103
+ <div className="flex min-w-0 flex-col">
104
+ <TruncateText>{deviceName}</TruncateText>
105
+ <TruncateText
106
+ variant="h6"
107
+ tone="secondary"
108
+ >{`Desktop • ${organizationName || 'Unknown Customer'}`}</TruncateText>
109
+ </div>
99
110
  </div>
100
- </div>
101
- <div className="flex flex-shrink-0 items-center gap-[var(--spacing-system-xs)]">
102
- {displayMenuGroups.length > 0 && (
103
- <ActionsMenuDropdown
104
- groups={displayMenuGroups}
105
- customTrigger={
106
- <Button variant="outline" leftIcon={<MonitorIcon className="h-4 w-4 md:h-6 md:w-6" />}>
107
- {/* A narrow page keeps the icon and drops the words: the bar never pushes the device's name out. */}
108
- <span className="sr-only content-md:not-sr-only">{currentDisplayLabel}</span>
109
- </Button>
110
- }
111
+ <div className="flex flex-shrink-0 items-center gap-[var(--spacing-system-xs)]">
112
+ {onToggleChat && (
113
+ <Button
114
+ variant="outline"
115
+ onClick={onToggleChat}
116
+ leftIcon={
117
+ chatOpen ? (
118
+ <ChatOffIcon className="h-4 w-4 md:h-6 md:w-6" />
119
+ ) : (
120
+ <ChatTextIcon className="h-4 w-4 md:h-6 md:w-6" />
121
+ )
122
+ }
123
+ >
124
+ {/* A narrow page keeps the icon and drops the words: the bar never pushes the device's name out. */}
125
+ <span className="sr-only content-md:not-sr-only">{chatOpen ? 'Close Chat' : 'Open Chat'}</span>
126
+ </Button>
127
+ )}
128
+ <ActionsMenuDropdown groups={actionsMenuGroups} triggerAriaLabel="Actions" />
129
+ <Button
130
+ variant="outline"
131
+ size="icon"
132
+ aria-label="Settings"
133
+ onClick={onOpenSettings}
134
+ leftIcon={<Settings01Icon />}
111
135
  />
112
- )}
113
- {onToggleChat && (
114
136
  <Button
115
137
  variant="outline"
116
- onClick={onToggleChat}
117
- leftIcon={
118
- chatOpen ? (
119
- <ChatOffIcon className="h-4 w-4 md:h-6 md:w-6" />
120
- ) : (
121
- <ChatTextIcon className="h-4 w-4 md:h-6 md:w-6" />
122
- )
123
- }
124
- >
125
- <span className="sr-only content-md:not-sr-only">{chatOpen ? 'Close Chat' : 'Open Chat'}</span>
126
- </Button>
127
- )}
128
- <ActionsMenuDropdown groups={actionsMenuGroups} triggerAriaLabel="Actions" />
129
- <Button
130
- variant="outline"
131
- size="icon"
132
- aria-label="Settings"
133
- onClick={onOpenSettings}
134
- leftIcon={<Settings01Icon />}
135
- />
136
- <Button
137
- variant="outline"
138
- size="icon"
139
- aria-label={fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
140
- onClick={fullscreen ? onExitFullscreen : onEnterFullscreen}
141
- leftIcon={fullscreen ? <Collapse02Icon /> : <Expand02Icon />}
142
- />
138
+ size="icon"
139
+ aria-label={fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
140
+ onClick={fullscreen ? onExitFullscreen : onEnterFullscreen}
141
+ leftIcon={fullscreen ? <Collapse02Icon /> : <Expand02Icon />}
142
+ />
143
+ </div>
143
144
  </div>
145
+ {/* The monitor selector is a row of its own under the device row, the full
146
+ width of the card (Figma 2155:109503): the label never competes with the
147
+ device's name for the header's width. Hidden with one display or none. */}
148
+ {displayMenuGroups.length > 0 && (
149
+ <ActionsMenuDropdown
150
+ groups={displayMenuGroups}
151
+ align="start"
152
+ customTrigger={
153
+ <button
154
+ type="button"
155
+ aria-label="Switch display"
156
+ className="flex w-full items-center gap-[var(--spacing-system-xs)] border-t border-ods-border p-[var(--spacing-system-sf)] text-left text-ods-text-primary outline-none transition-colors hover:bg-ods-bg-hover focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-focus"
157
+ >
158
+ <MonitorIcon className="h-6 w-6 shrink-0" />
159
+ <span className="min-w-0 flex-1 truncate text-h4">{currentDisplayLabel}</span>
160
+ <Chevron02DownIcon className="h-6 w-6 shrink-0" />
161
+ </button>
162
+ }
163
+ />
164
+ )}
144
165
  </div>
145
166
  );
146
167
 
@@ -189,17 +210,19 @@ export function RemoteDesktopViewSkeleton({ onBack }: Pick<RemoteDesktopViewProp
189
210
  return (
190
211
  <PageLayout className={PAGE_CLASS} backButton={onBack ? { label: 'Back', onClick: onBack } : undefined}>
191
212
  <div className={CONTROLS_BAR_CLASS}>
192
- <div className="flex min-w-0 items-center gap-[var(--spacing-system-mf)]">
193
- <Skeleton className="h-9 w-9 flex-shrink-0 rounded-md" />
194
- <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
195
- <Skeleton className="h-5 w-48" />
196
- <Skeleton className="h-4 w-36" />
213
+ <div className={CONTROLS_ROW_CLASS}>
214
+ <div className="flex min-w-0 items-center gap-[var(--spacing-system-mf)]">
215
+ <Skeleton className="h-9 w-9 flex-shrink-0 rounded-md" />
216
+ <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
217
+ <Skeleton className="h-5 w-48" />
218
+ <Skeleton className="h-4 w-36" />
219
+ </div>
220
+ </div>
221
+ <div className="flex flex-shrink-0 items-center gap-[var(--spacing-system-xs)]">
222
+ <Skeleton className="h-11 w-11 rounded-lg md:h-12 md:w-12" />
223
+ <Skeleton className="h-11 w-11 rounded-lg md:h-12 md:w-12" />
224
+ <Skeleton className="h-11 w-11 rounded-lg md:h-12 md:w-12" />
197
225
  </div>
198
- </div>
199
- <div className="flex flex-shrink-0 items-center gap-[var(--spacing-system-xs)]">
200
- <Skeleton className="h-11 w-11 rounded-lg md:h-12 md:w-12" />
201
- <Skeleton className="h-11 w-11 rounded-lg md:h-12 md:w-12" />
202
- <Skeleton className="h-11 w-11 rounded-lg md:h-12 md:w-12" />
203
226
  </div>
204
227
  </div>
205
228
 
@@ -45,7 +45,7 @@ import {
45
45
  } from '../../types/trust-center';
46
46
  import { STICKY_HEADER_OFFSET_PX } from '../../utils/same-page-hash-nav';
47
47
  import { useScrollSpy } from '../docs/use-scroll-spy';
48
- import { FaqSection } from '../faq/faq-section';
48
+ import { FaqSection, type FaqSectionProps } from '../faq/faq-section';
49
49
  import { PageShell } from '../layout/article-detail-layout';
50
50
  import { PageLayout } from '../layout/page-layout';
51
51
  import { StickySectionNav } from '../navigation/sticky-section-nav';
@@ -83,6 +83,8 @@ export interface TrustCenterPageProps {
83
83
  title?: string;
84
84
  /** Page subtitle. Default the brand-neutral `TRUST_CENTER_TAGLINE` (a host adds its brand here). */
85
85
  subtitle?: string;
86
+ /** The FAQ section's "ask the assistant" card (`FaqSection`'s `ask`): the host states its topic. Absent: no card. */
87
+ ask?: FaqSectionProps['ask'];
86
88
  }
87
89
 
88
90
  /** One section's page content. Its `h2` is the `TRUST_CENTER_SECTIONS` label — never a literal here. */
@@ -149,6 +151,7 @@ export function TrustCenterPage({
149
151
  backButton = false,
150
152
  title = TRUST_CENTER_TITLE,
151
153
  subtitle = TRUST_CENTER_TAGLINE,
154
+ ask,
152
155
  }: TrustCenterPageProps) {
153
156
  const { data, isLoading, error, reload } = useSelfFetch<TrustCenterPublic>(endpoint, {
154
157
  initialData,
@@ -232,7 +235,7 @@ export function TrustCenterPage({
232
235
  lead: TRUST_SECTION_LEADS.subprocessors,
233
236
  render: d => <SubprocessorsSection subprocessors={d.subprocessors} />,
234
237
  },
235
- faq: { render: d => <FaqSection initialFaqs={d.faqs} heading={null} /> },
238
+ faq: { render: d => <FaqSection initialFaqs={d.faqs} heading={null} ask={ask} /> },
236
239
  contact: { render: () => <ContactSection onContact={() => openRequest(null)} /> },
237
240
  };
238
241
 
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import type React from 'react';
4
- import { useEffect } from 'react';
4
+ import { useEffect, useRef } from 'react';
5
+ import { useAssistantRuntime } from '../../contexts/assistant-runtime-context';
5
6
  import { useVisitorOs } from '../../hooks/ui/use-visitor-os';
6
7
  import { cn } from '../../utils';
7
8
  import { shortcutLabel, usesCommandKey } from '../../utils/visitor-os';
@@ -78,8 +79,8 @@ export function openAskAi(source?: string, options?: { prompt?: string }): void
78
79
  */
79
80
  export function MingoAiButton({
80
81
  source,
81
- icon,
82
- label = 'Mingo AI',
82
+ icon: iconProp,
83
+ label: labelProp,
83
84
  shortcutHint = false,
84
85
  variant = 'inline',
85
86
  className,
@@ -93,6 +94,22 @@ export function MingoAiButton({
93
94
  const inPage = variant === 'button';
94
95
 
95
96
  const commandKey = visitor.known && usesCommandKey(visitor.os);
97
+ // The launcher is the assistant's: its name, glyph and the chat it opens are
98
+ // the assistant runtime's (the host's server-resolved identity and its own
99
+ // opener), so no launcher types a name or assumes which chat exists. A prop
100
+ // still wins (a header given its own). The lib holds no name of its own:
101
+ // with none from either, there is no launcher to render (below). With no
102
+ // runtime opener the open is the `ask-ai:open` event of `source`.
103
+ const assistant = useAssistantRuntime();
104
+ const label = labelProp ?? assistant?.name;
105
+ const icon = iconProp ?? assistant?.icon;
106
+ const runtimeOpen = assistant?.open;
107
+ const eventSource = source ?? assistant?.source;
108
+ // The latest opener, for the shortcut listener: a host's inline callback must not re-bind it every render.
109
+ const openRef = useRef<() => void>(() => undefined);
110
+ useEffect(() => {
111
+ openRef.current = () => (runtimeOpen ? runtimeOpen({}) : openAskAi(eventSource));
112
+ });
96
113
 
97
114
  // The shortcut the key cap shows opens the same chat the click opens.
98
115
  useEffect(() => {
@@ -105,11 +122,14 @@ export function MingoAiButton({
105
122
  const modifier = commandKey ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
106
123
  if (event.key.toLowerCase() !== 'k' || !modifier) return;
107
124
  event.preventDefault();
108
- openAskAi(source);
125
+ openRef.current();
109
126
  };
110
127
  window.addEventListener('keydown', onKeyDown);
111
128
  return () => window.removeEventListener('keydown', onKeyDown);
112
- }, [shortcutHint, source, commandKey]);
129
+ }, [shortcutHint, commandKey]);
130
+
131
+ // No name from the host or the runtime: nothing to launch under. The lib invents none.
132
+ if (!label) return null;
113
133
 
114
134
  return (
115
135
  <Button
@@ -121,7 +141,8 @@ export function MingoAiButton({
121
141
  aria-label={label}
122
142
  aria-keyshortcuts={shortcutHint && visitor.known ? (commandKey ? 'Meta+K' : 'Control+K') : undefined}
123
143
  onClick={e => {
124
- openAskAi(source);
144
+ if (runtimeOpen) runtimeOpen({});
145
+ else openAskAi(eventSource);
125
146
  onClick?.(e);
126
147
  }}
127
148
  className={cn(
@@ -28,12 +28,12 @@
28
28
  * `renderSkeletonForType` so the placeholder height matches the loaded card
29
29
  * exactly (zero layout shift on resolve).
30
30
  *
31
- * One API call per content type via the shared list-URL builder
32
- * (`buildListUrl` — injectable; defaults to the lib's byte-parity-tested
33
- * builder prefixed with `apiBaseUrl`). Fetching uses `useSelfFetch` (plain
34
- * fetch, NO react-query) so third-party embedders need no QueryClientProvider;
35
- * cards are imported via DEEP module paths (not the chat barrel) so this
36
- * chunk never reaches `@tanstack/react-query`.
31
+ * One API call per page of a content type, carrying only that page's ids, via
32
+ * the shared list-URL builder (`buildListUrl` — injectable; defaults to the
33
+ * lib's byte-parity-tested builder prefixed with `apiBaseUrl`). Fetching uses
34
+ * `useSelfFetch` (plain fetch, NO react-query) so third-party embedders need
35
+ * no QueryClientProvider; cards are imported via DEEP module paths (not the
36
+ * chat barrel) so this chunk never reaches `@tanstack/react-query`.
37
37
  *
38
38
  * The per-type card + skeleton dispatch lives in `./card-registry`
39
39
  * (`RELATED_CARD_REGISTRY`): ONE entry per content type, each carrying that
@@ -49,7 +49,7 @@
49
49
  */
50
50
 
51
51
  import type React from 'react';
52
- import { useEffect, useMemo, useRef, useState } from 'react';
52
+ import { useMemo, useState } from 'react';
53
53
  import { useSelfFetch } from '../../hooks/use-self-fetch';
54
54
  import type { ContentRef, ContentRefWithReason } from '../../types/content-ref';
55
55
  import {
@@ -246,23 +246,29 @@ function CardForType({
246
246
  }
247
247
 
248
248
  // =============================================================================
249
- // Fetch all items for a type in ONE server-sorted call, via the injectable
250
- // list-URL builder. `useSelfFetch` (URL = cache key) replaces the hub's old
251
- // react-query usage: `enabled` ≙ `url === null`, `!res.ok`/network error ≙
252
- // `error → items null → group renders nothing`. Accepted deltas vs
253
- // react-query: no retry/backoff, no focus refetch, no cross-mount cache.
249
+ // Fetch ONE page of a group — only the ids on screen go into the list URL.
250
+ // Pages already fetched are kept by url, so paging back renders without a
251
+ // request. `useSelfFetch` replaces the hub's old react-query usage: `enabled`
252
+ // ≙ `url === null` (no list API → `items: null`), `!res.ok`/network error ≙
253
+ // an empty page. Accepted deltas vs react-query: no retry/backoff, no focus
254
+ // refetch, no cross-mount cache.
254
255
  // =============================================================================
255
256
 
256
257
  function useGroupItems(type: string, refs: ContentRef[], buildUrl: (type: string, ids: string[]) => string | null) {
257
258
  const ids = refs.map(r => r.id);
258
259
  const url = ids.length > 0 ? buildUrl(type, ids) : null;
259
- const { data, isLoading } = useSelfFetch<unknown>(url);
260
- const items = data != null ? extractItems(data) : null;
261
- return { items, isLoading };
260
+ const [pages, setPages] = useState<ReadonlyMap<string, unknown[]>>(() => new Map());
261
+ const cached = url !== null ? pages.get(url) : undefined;
262
+ const { data, dataUrl, error } = useSelfFetch<unknown>(cached ? null : url);
263
+ if (data != null && dataUrl !== null && !pages.has(dataUrl)) {
264
+ setPages(new Map(pages).set(dataUrl, extractItems(data)));
265
+ }
266
+ if (url === null) return { items: null, isLoading: false };
267
+ return { items: cached ?? (error ? [] : null), isLoading: !cached && !error };
262
268
  }
263
269
 
264
270
  // =============================================================================
265
- // Per-group renderer — one API call, server-sorted, then render cards via the
271
+ // Per-group renderer — one API call per page, then render cards via the
266
272
  // dispatcher with per-type skeletons + per-type layout from CONTENT_REF_GROUPS.
267
273
  // =============================================================================
268
274
 
@@ -325,41 +331,33 @@ function ContentGroup({
325
331
  * its heading too — no orphaned titles. */
326
332
  heading: React.ReactNode;
327
333
  }) {
328
- const { items, isLoading } = useGroupItems(type, refs, buildUrl);
329
334
  const config = resolveGroupConfig(type);
330
335
  const isListLayout = config.layout === 'list';
331
336
  const cardSize = config.gridSize;
332
337
 
333
338
  // Per-group pagination for big groups (author pages): GROUP_PAGE_SIZE items
334
- // per page with the standard Pagination control below the group. Client-side
335
- // slicing — useGroupItems already fetched every row in one batched call, so
336
- // page flips are instant. Hooks live above every early return (file
337
- // convention). Page is clamped so a shrinking refs array (suggestion
338
- // refetch) can never strand the view past the last page, and RESET when the
339
- // ref set actually changes (shrink→grow must not return to a stale page).
339
+ // per page with the standard Pagination control below the group, each page
340
+ // fetched on its own. Back to page 1 whenever the ref set changes (a
341
+ // suggestion refetch can shrink it) — reset while rendering, so the stale
342
+ // page is never requested.
340
343
  const [page, setPage] = useState(1);
341
344
  const refsKey = refs.map(r => r.id).join('|');
342
- const prevRefsKeyRef = useRef(refsKey);
343
- useEffect(() => {
344
- if (prevRefsKeyRef.current !== refsKey) {
345
- prevRefsKeyRef.current = refsKey;
346
- setPage(1);
347
- }
348
- }, [refsKey]);
345
+ const [pagedRefsKey, setPagedRefsKey] = useState(refsKey);
346
+ if (pagedRefsKey !== refsKey) {
347
+ setPagedRefsKey(refsKey);
348
+ setPage(1);
349
+ }
349
350
  const totalGroupPages = pageCount(refs.length, GROUP_PAGE_SIZE);
350
- const safePage = Math.min(page, totalGroupPages);
351
- const visibleGroupRefs =
352
- refs.length > GROUP_PAGE_SIZE ? refs.slice((safePage - 1) * GROUP_PAGE_SIZE, safePage * GROUP_PAGE_SIZE) : refs;
351
+ const pageRefs = refs.slice((page - 1) * GROUP_PAGE_SIZE, page * GROUP_PAGE_SIZE);
352
+ const { items, isLoading } = useGroupItems(type, pageRefs, buildUrl);
353
353
  const groupPagination =
354
- totalGroupPages > 1 ? (
355
- <Pagination currentPage={safePage} totalPages={totalGroupPages} onPageChange={setPage} />
356
- ) : null;
357
-
358
- // Skeleton gate: `isLoading && !items` — SSR HTML and the client's first
359
- // paint render identical skeletons (useSelfFetch starts isLoading=true on
360
- // both sides), and once items exist they are never replaced by skeletons.
361
- if (isLoading && !items) {
362
- const skeletons = visibleGroupRefs.map(r => (
354
+ totalGroupPages > 1 ? <Pagination currentPage={page} totalPages={totalGroupPages} onPageChange={setPage} /> : null;
355
+
356
+ // Skeletons until this page's rows land — SSR HTML and the client's first
357
+ // paint render identical skeletons (useSelfFetch starts loading on both
358
+ // sides). The pager stays in place while an unfetched page loads.
359
+ if (isLoading) {
360
+ const skeletons = pageRefs.map(r => (
363
361
  <div key={r.id}>{renderSkeletonForType(type, cardSize, adminCampaignCard)}</div>
364
362
  ));
365
363
  return (
@@ -370,11 +368,12 @@ function ContentGroup({
370
368
  ) : (
371
369
  <div className={gridClassFor(columns)}>{skeletons}</div>
372
370
  )}
371
+ {groupPagination}
373
372
  </div>
374
373
  );
375
374
  }
376
375
 
377
- if (!items || items.length === 0) return null;
376
+ if (!items) return null;
378
377
 
379
378
  // Index fetched rows by id, then render in REF order — refs carry the
380
379
  // intended sequence (suggestion mode: the engine's tier order, so
@@ -393,7 +392,7 @@ function ContentGroup({
393
392
  };
394
393
  const itemById = new Map(items.map(it => [rowKey(it), it]));
395
394
 
396
- const cards = visibleGroupRefs
395
+ const cards = pageRefs
397
396
  .map(contentRef => {
398
397
  const itemId = String(contentRef.id);
399
398
  const item = itemById.get(itemId);
@@ -426,9 +425,9 @@ function ContentGroup({
426
425
  .filter(Boolean);
427
426
 
428
427
  if (cards.length === 0) {
429
- // Current PAGE resolved zero cards (rows deleted between the ref fetch
430
- // and the group fetch, or a stricter list-API gate dropped them). When a
431
- // pager exists the user must keep the controls to navigate back —
428
+ // Current PAGE resolved zero cards (its fetch failed, rows were deleted
429
+ // between the ref fetch and the page fetch, or a stricter list-API gate
430
+ // dropped them). When a pager exists the user must keep the controls —
432
431
  // dropping the whole group would strand them. A genuinely empty group
433
432
  // (no pager) still vanishes with its heading.
434
433
  if (groupPagination) {
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Assistant runtime context: what a PAGE needs to offer the host's chat
5
+ * assistant ("Ask Mingo"): whether a chat is there to open, the assistant's
6
+ * name and glyph, where its questions are read from, and HOW the chat is
7
+ * opened and asked.
8
+ *
9
+ * A sibling of `ChatRuntimeContext` (which configures the chat panel itself)
10
+ * and `EndpointsRuntimeContext`. It is its own context because its values
11
+ * change with the visitor (signed in or not, chat mounted or not), and a new
12
+ * `ChatRuntime` identity re-renders the whole chat tree.
13
+ *
14
+ * The NEAREST provider wins, so a page region that carries its own chat (an
15
+ * embedded chat beside the content) mounts a provider whose `open` opens THAT
16
+ * chat: every "ask" surface under it (`FaqSection`'s card, `AskPrompts`,
17
+ * `AskCard`) then opens and fills it, with no prop on any of them.
18
+ *
19
+ * No provider, or `available: false`: the "ask" surfaces render nothing.
20
+ */
21
+
22
+ import { createContext, useContext, type ReactNode } from 'react';
23
+
24
+ /** What an "ask" surface asks the chat to do. */
25
+ export interface AssistantOpenRequest {
26
+ /** A question to send once the chat is open. Absent: the chat only opens. */
27
+ prompt?: string;
28
+ /** The topic of the surface that asked (a FAQ's topic, a page section). */
29
+ topic?: string;
30
+ }
31
+
32
+ /** What an "ask" surface reports after a question was sent (analytics). */
33
+ export interface AssistantAskEvent {
34
+ /** The id of the question that was asked. */
35
+ promptId: string;
36
+ /** The topic of the surface that asked. */
37
+ topic?: string;
38
+ }
39
+
40
+ /** The wording of the card beside a FAQ. `{assistant}` is replaced by the assistant's name. */
41
+ export interface AssistantAskCardCopy {
42
+ title?: string;
43
+ description?: string;
44
+ /** How many questions the card offers. */
45
+ count?: number;
46
+ }
47
+
48
+ export interface AssistantRuntime {
49
+ /** A chat is mounted for this visitor: an "ask" click has something to open. */
50
+ available: boolean;
51
+ /** The assistant's configured name. */
52
+ name?: string | null;
53
+ /** The assistant's configured glyph. Absent: the packaged Mingo mark. */
54
+ icon?: ReactNode;
55
+ /** The chat source the default `open` addresses its `ask-ai:open` event to. */
56
+ source?: string;
57
+ /**
58
+ * GET endpoint the questions are read from:
59
+ * `<url>?count=<n>&section=<topic>&exclude=<id,id>` → `{ prompts: AskPrompt[] }`
60
+ * `section` is ANY string: the host decides what topics exist (the hub: the
61
+ * `section` attribute of a quick action, set in chat config), so a new topic
62
+ * needs no change here. Hub: '/api/quick-actions/questions'. Absent: an "ask"
63
+ * surface shows its launcher with no questions.
64
+ */
65
+ askPromptsUrl?: string;
66
+ /**
67
+ * Open the chat and, with `prompt`, ask it. Absent: the `ask-ai:open` event
68
+ * of `source` (`openAskAi`), which the mounted `EmbeddableChat` listens for.
69
+ * A host whose chat is opened another way (an embedded chat, a different
70
+ * panel) supplies its own.
71
+ */
72
+ open?: (request: AssistantOpenRequest) => void;
73
+ /** After a question was sent (analytics). */
74
+ onAsk?: (event: AssistantAskEvent) => void;
75
+ /** The wording of the card beside a FAQ. Absent: the lib's own. */
76
+ askCard?: AssistantAskCardCopy;
77
+ }
78
+
79
+ export const AssistantRuntimeContext = createContext<AssistantRuntime | null>(null);
80
+
81
+ /** The nearest assistant runtime, or null when no provider is mounted. */
82
+ export function useAssistantRuntime(): AssistantRuntime | null {
83
+ return useContext(AssistantRuntimeContext);
84
+ }
@@ -30,4 +30,13 @@ export {
30
30
 
31
31
  export { ChatRuntimeContext, useChatRuntime, useRequiredChatRuntime, type ChatRuntime } from './chat-runtime-context';
32
32
 
33
+ export {
34
+ AssistantRuntimeContext,
35
+ useAssistantRuntime,
36
+ type AssistantAskCardCopy,
37
+ type AssistantAskEvent,
38
+ type AssistantOpenRequest,
39
+ type AssistantRuntime,
40
+ } from './assistant-runtime-context';
41
+
33
42
  export { useOuterOrDefault } from './use-outer-or-default';
@@ -5,6 +5,9 @@ import { contentFetch } from '../utils/embed-content-fetch';
5
5
 
6
6
  export interface UseSelfFetchResult<T> {
7
7
  data: T | null;
8
+ /** The url `data` came from (`null` until something lands). While a new url
9
+ * loads, `data` still holds the previous one — compare before trusting it. */
10
+ dataUrl: string | null;
8
11
  /** Imperatively patch the fetched data (e.g. optimistic vote updates). */
9
12
  setData: Dispatch<SetStateAction<T | null>>;
10
13
  isLoading: boolean;
@@ -48,6 +51,7 @@ export function useSelfFetch<T>(
48
51
  const initialData = options?.initialData;
49
52
  const revalidateOnVisibleAfterMs = options?.revalidateOnVisibleAfterMs;
50
53
  const [data, setData] = useState<T | null>(initialData ?? null);
54
+ const [dataUrl, setDataUrl] = useState<string | null>(initialData !== undefined ? url : null);
51
55
  const [isLoading, setIsLoading] = useState(initialData === undefined && url !== null);
52
56
  const [error, setError] = useState(false);
53
57
  const [reloadKey, setReloadKey] = useState(0);
@@ -78,6 +82,7 @@ export function useSelfFetch<T>(
78
82
  // `initialData` is `undefined`.
79
83
  if (syncedProps.initialData !== initialData && initialData !== undefined) {
80
84
  setData(initialData);
85
+ setDataUrl(url);
81
86
  }
82
87
  // Fetching just got disabled — nothing will land to clear the spinner.
83
88
  if (syncedProps.url !== url && url === null) {
@@ -101,6 +106,7 @@ export function useSelfFetch<T>(
101
106
  const json = (await res.json()) as T;
102
107
  if (!cancelled) {
103
108
  setData(json);
109
+ setDataUrl(url);
104
110
  dataUrlRef.current = url; // remember the url we now hold data for
105
111
  dataAtRef.current = Date.now();
106
112
  }
@@ -149,6 +155,7 @@ export function useSelfFetch<T>(
149
155
 
150
156
  return {
151
157
  data,
158
+ dataUrl,
152
159
  setData,
153
160
  isLoading,
154
161
  error,
@@ -17,6 +17,7 @@ export {
17
17
  // source of truth). Re-exported here so existing callers that pull from
18
18
  // the barrel keep working without changing imports.
19
19
  export { formatDate, formatNumber, formatPrice, formatBytes } from './format';
20
+ export { TRIAL_DAYS, TRIAL_LABELS } from './trial';
20
21
  export {
21
22
  VENDOR_CLASSIFICATIONS,
22
23
  OPENFRAME_CLASSIFICATIONS,
@@ -116,6 +116,10 @@ const BUILDERS: Record<string, (ids: string[], base: string) => string> = {
116
116
  design_doc: (ids, b) => `${b}/api/design-docs?ids=${ids.join(',')}`,
117
117
  openframe_tenant: (ids, b) => `${b}/api/openframe-tenants?ids=${ids.join(',')}`,
118
118
  prospect_call: (ids, b) => `${b}/api/prospect-calls?ids=${ids.join(',')}`,
119
+ // The public website's own pages (live source; ChatRef-shaped items like the objects above).
120
+ site_page: (ids, b) => `${b}/api/site-pages?ids=${ids.join(',')}`,
121
+ // The OpenMSP vendor directory (indexed source).
122
+ vendor: (ids, b) => `${b}/api/vendors/cards?ids=${ids.join(',')}`,
119
123
  // Code intelligence — per-object card hydration, same `handleEntityCardList`
120
124
  // shape as the internal objects above.
121
125
  code_rule: (ids, b) => `${b}/api/code-rules/cards?ids=${ids.join(',')}`,