@flamingo-stack/openframe-frontend-core 0.0.719 → 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 (166) hide show
  1. package/dist/{chunk-4JIGEK7C.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-E2V2EH2G.cjs → chunk-5XDCCQ4A.cjs} +15 -15
  5. package/dist/{chunk-HCFNEKO3.cjs → chunk-6BLN5WDS.cjs} +41 -41
  6. package/dist/{chunk-D6PMVWDI.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-AJHSQ5XK.js → chunk-7HYEQKN6.js} +6 -6
  10. package/dist/{chunk-2JLP2X2M.js → chunk-7IOU6BJE.js} +12 -11
  11. package/dist/{chunk-7BREFV7G.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-TELAANY6.js → chunk-B5LNX4GZ.js} +2 -2
  16. package/dist/{chunk-SWAVO7BB.cjs → chunk-DHNJSQGX.cjs} +91 -91
  17. package/dist/{chunk-LOZLHG2U.cjs → chunk-DOPOHO6H.cjs} +20 -20
  18. package/dist/{chunk-NQVS4G7G.cjs → chunk-DYDDAA6O.cjs} +519 -518
  19. package/dist/{chunk-DFEPUYTF.cjs → chunk-EVMM3JE6.cjs} +44 -46
  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-TDZ76NCB.cjs → chunk-HFEYEPOG.cjs} +27 -27
  24. package/dist/{chunk-LE5WKGJV.cjs → chunk-HTS46FFY.cjs} +3 -3
  25. package/dist/{chunk-H5VIMBRN.cjs → chunk-IL3ZWNM2.cjs} +4 -4
  26. package/dist/{chunk-PSVSEN4J.cjs → chunk-IMTY3GRR.cjs} +2 -0
  27. package/dist/{chunk-WS7WRR5Z.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-FVQJBII6.js → chunk-LO3KCLEF.js} +1 -1
  31. package/dist/{chunk-MA4Q5RBP.cjs → chunk-NYSMDM4J.cjs} +2 -2
  32. package/dist/{chunk-5YAGX5DY.js → chunk-O3MKJU6A.js} +6 -6
  33. package/dist/{chunk-H5V7GRN5.js → chunk-PSMJYZMV.js} +4 -6
  34. package/dist/{chunk-JH2SIWPQ.js → chunk-PXAKSPLZ.js} +1586 -1263
  35. package/dist/{chunk-CU6NWO2M.js → chunk-Q5327DXK.js} +13 -17
  36. package/dist/{chunk-NYWESHOQ.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-EAHOB64X.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-4D6NMPOO.js → chunk-V6B2FBZK.js} +5 -5
  44. package/dist/{chunk-5U3KWQXP.js → chunk-VBHGJFA3.js} +10 -10
  45. package/dist/{chunk-LCLDFMDF.cjs → chunk-VF2CJXZ2.cjs} +15 -15
  46. package/dist/{chunk-7BCY77W6.js → chunk-VYVPII4W.js} +8 -10
  47. package/dist/{chunk-TTEFN6VO.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-Y4MB25KW.js → chunk-YK3X74SF.js} +1 -1
  51. package/dist/{chunk-ZKNZ2CXW.cjs → chunk-ZHD3F2TH.cjs} +3 -3
  52. package/dist/{chunk-KBJJQFYU.js → chunk-ZPHOQDAE.js} +1 -1
  53. package/dist/{cloud-tenants-DNBPNQNP.js → cloud-tenants-RKQRSDNE.js} +9 -9
  54. package/dist/{cloud-tenants-FBUO66GW.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/help-center-pages/index.cjs +104 -105
  91. package/dist/components/help-center-pages/index.js +28 -29
  92. package/dist/components/help-center-pages/trust-center-page.d.ts +4 -1
  93. package/dist/components/help-center-pages/trust-center-page.d.ts.map +1 -1
  94. package/dist/components/index.cjs +109 -197
  95. package/dist/components/index.js +228 -316
  96. package/dist/components/meeting-scheduler/index.cjs +50 -51
  97. package/dist/components/meeting-scheduler/index.js +12 -13
  98. package/dist/components/navigation/index.cjs +13 -13
  99. package/dist/components/navigation/index.js +12 -12
  100. package/dist/components/navigation/mingo-ai-button.d.ts +1 -1
  101. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  102. package/dist/components/onboarding-guides/index.cjs +13 -14
  103. package/dist/components/onboarding-guides/index.js +12 -13
  104. package/dist/components/product-demo/index.cjs +7 -7
  105. package/dist/components/product-demo/index.js +7 -7
  106. package/dist/components/related-content/index.cjs +5 -7
  107. package/dist/components/related-content/index.js +4 -6
  108. package/dist/components/tickets/index.cjs +15 -15
  109. package/dist/components/tickets/index.js +14 -14
  110. package/dist/components/ui/index.cjs +26 -10
  111. package/dist/components/ui/index.js +25 -9
  112. package/dist/contexts/assistant-runtime-context.d.ts +74 -0
  113. package/dist/contexts/assistant-runtime-context.d.ts.map +1 -0
  114. package/dist/contexts/index.cjs +7 -1
  115. package/dist/contexts/index.d.ts +1 -0
  116. package/dist/contexts/index.d.ts.map +1 -1
  117. package/dist/contexts/index.js +6 -0
  118. package/dist/{devices-5A5TY4JD.cjs → devices-7Q2EUM7B.cjs} +11 -11
  119. package/dist/{devices-PLDAEUSL.js → devices-U3Y4IIW4.js} +9 -9
  120. package/dist/hooks/index.cjs +3 -4
  121. package/dist/hooks/index.js +2 -3
  122. package/dist/index.cjs +34 -14
  123. package/dist/index.js +33 -13
  124. package/dist/{knowledge-G6Q5KRWV.js → knowledge-NST53EXE.js} +9 -9
  125. package/dist/{knowledge-FKTJ723Z.cjs → knowledge-UYIZ74MA.cjs} +11 -11
  126. package/dist/{logs-CJCXUBU6.cjs → logs-HTH7X7KV.cjs} +18 -18
  127. package/dist/{logs-QMV6XIC4.js → logs-JVTRG232.js} +9 -9
  128. package/dist/{policies-G6D7AX6F.js → policies-DTXTRK56.js} +9 -9
  129. package/dist/{policies-QMRTK3CY.cjs → policies-MXK2HB4W.cjs} +14 -14
  130. package/dist/{remote-session-DBD6OSTX.js → remote-session-LAJZKQBN.js} +3 -3
  131. package/dist/{remote-session-2JVI7RVA.cjs → remote-session-SKKF4PBV.cjs} +5 -5
  132. package/dist/{software-update-6BQM6BKH.js → software-update-6RHMPM3W.js} +9 -9
  133. package/dist/{software-update-PNZMRDBT.cjs → software-update-P6BSM6HX.cjs} +14 -14
  134. package/dist/utils/index.cjs +26 -2
  135. package/dist/utils/index.d.ts +1 -0
  136. package/dist/utils/index.d.ts.map +1 -1
  137. package/dist/utils/index.js +25 -3
  138. package/dist/utils/list-url.d.ts.map +1 -1
  139. package/dist/utils/source-icons.d.ts.map +1 -1
  140. package/dist/utils/trial.d.ts +18 -0
  141. package/dist/utils/trial.d.ts.map +1 -0
  142. package/package.json +1 -1
  143. package/src/components/chat/ask-prompts.tsx +25 -3
  144. package/src/components/chat/assistant-ask-prompts.tsx +78 -0
  145. package/src/components/chat/entity-cards/dispatch.tsx +38 -4
  146. package/src/components/chat/hooks/index.ts +12 -0
  147. package/src/components/chat/hooks/use-ask-prompts.ts +188 -0
  148. package/src/components/chat/index.ts +1 -0
  149. package/src/components/chat/utils/icon-library.tsx +6 -0
  150. package/src/components/faq/faq-ask-card.tsx +115 -0
  151. package/src/components/faq/faq-document-page.tsx +6 -1
  152. package/src/components/faq/faq-section.tsx +27 -5
  153. package/src/components/faq/index.ts +1 -0
  154. package/src/components/features/auth/auth-benefits-panel.tsx +5 -4
  155. package/src/components/help-center-pages/trust-center-page.tsx +5 -2
  156. package/src/components/navigation/mingo-ai-button.tsx +27 -6
  157. package/src/contexts/assistant-runtime-context.tsx +84 -0
  158. package/src/contexts/index.ts +9 -0
  159. package/src/utils/index.ts +1 -0
  160. package/src/utils/list-url.ts +4 -0
  161. package/src/utils/source-icons.ts +10 -2
  162. package/src/utils/trial.ts +19 -0
  163. package/dist/chunk-6X3O67OL.cjs +0 -89
  164. package/dist/chunk-AUMKE74M.cjs +0 -16
  165. package/dist/chunk-G3GRYDMS.js +0 -16
  166. package/dist/chunk-RR6PY34K.js +0 -89
@@ -39,3 +39,15 @@ export * from './use-embedded-chat';
39
39
  // `ChatRuntime.endpoints`, so hub vs. embedders share one image-proxy
40
40
  // resolver.
41
41
  export * from './use-proxied-image-url';
42
+ export {
43
+ ASK_PROMPTS_DEFAULT_COUNT,
44
+ assistantAvailable,
45
+ buildAskPromptsUrl,
46
+ useAskPrompts,
47
+ useAssistantOpen,
48
+ useShownAskPromptIds,
49
+ useShownAskPrompts,
50
+ type AskPromptsResponse,
51
+ type UseAskPromptsOptions,
52
+ type UseAskPromptsResult,
53
+ } from './use-ask-prompts';
@@ -0,0 +1,188 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useId, useLayoutEffect, useSyncExternalStore } from 'react';
4
+ import {
5
+ useAssistantRuntime,
6
+ type AssistantOpenRequest,
7
+ type AssistantRuntime,
8
+ } from '../../../contexts/assistant-runtime-context';
9
+ import { useSelfFetch } from '../../../hooks/use-self-fetch';
10
+ import { openAskAi } from '../../navigation/mingo-ai-button';
11
+ import type { AskPrompt } from '../ask-prompts';
12
+
13
+ /** The answer of a host's questions endpoint (`AssistantRuntime.askPromptsUrl`). */
14
+ export interface AskPromptsResponse {
15
+ prompts: AskPrompt[];
16
+ }
17
+
18
+ /** How many questions an "ask" surface offers when nothing states a number. */
19
+ export const ASK_PROMPTS_DEFAULT_COUNT = 3;
20
+
21
+ /** The questions URL: the host's endpoint with the surface's count, topic and exclusions. */
22
+ export function buildAskPromptsUrl(
23
+ base: string,
24
+ { count, topic, exclude = [] }: { count: number; topic?: string; exclude?: readonly string[] },
25
+ ): string {
26
+ const params = new URLSearchParams({ count: String(count) });
27
+ if (topic) params.set('section', topic);
28
+ if (exclude.length > 0) params.set('exclude', exclude.join(','));
29
+ return `${base}${base.includes('?') ? '&' : '?'}${params.toString()}`;
30
+ }
31
+
32
+ /** An "ask" surface has something to open: a runtime is mounted and its chat is there. */
33
+ export function assistantAvailable(assistant: AssistantRuntime | null): assistant is AssistantRuntime {
34
+ return !!assistant?.available;
35
+ }
36
+
37
+ export interface UseAskPromptsOptions {
38
+ /**
39
+ * What the surface is about: ANY string the host's endpoint knows (sent as
40
+ * `section`). Questions written for it are picked first. Absent: general ones.
41
+ */
42
+ topic?: string;
43
+ count?: number;
44
+ /** Ids of questions never to pick (the ones the page already shows). */
45
+ exclude?: readonly string[];
46
+ /** False: nothing is fetched yet (the surface is not ready to pick). Default true. */
47
+ enabled?: boolean;
48
+ }
49
+
50
+ export interface UseAskPromptsResult {
51
+ prompts: AskPrompt[];
52
+ /** True until the pick has landed (or failed). False where nothing is fetched. */
53
+ isLoading: boolean;
54
+ /** The host has a questions endpoint and a chat to ask: questions can exist here. */
55
+ supported: boolean;
56
+ }
57
+
58
+ /**
59
+ * The questions an "ask" surface offers, picked by the HOST for this visit
60
+ * (`AssistantRuntime.askPromptsUrl`). One pick per mount: read once, never
61
+ * cached, so a visitor keeps the same questions while the page is open and
62
+ * gets a new pick next time. A failed read leaves the list empty. No chat, or
63
+ * no endpoint: nothing is fetched and the list is empty.
64
+ */
65
+ export function useAskPrompts({
66
+ topic,
67
+ count = ASK_PROMPTS_DEFAULT_COUNT,
68
+ exclude,
69
+ enabled = true,
70
+ }: UseAskPromptsOptions = {}): UseAskPromptsResult {
71
+ const assistant = useAssistantRuntime();
72
+ const base = assistantAvailable(assistant) ? assistant.askPromptsUrl : undefined;
73
+ const url = base && enabled ? buildAskPromptsUrl(base, { count, topic, exclude }) : null;
74
+ const { data, isLoading } = useSelfFetch<AskPromptsResponse>(url);
75
+ return { prompts: data?.prompts ?? [], isLoading: !!base && (!enabled || isLoading), supported: !!base };
76
+ }
77
+
78
+ /**
79
+ * How an "ask" surface opens the chat and asks it: its own `onOpen`, else the
80
+ * nearest assistant runtime's `open`, else the `ask-ai:open` event of the
81
+ * runtime's source (the mounted `EmbeddableChat`). The surface's topic rides
82
+ * every request, so a host with several chats can tell them apart.
83
+ */
84
+ export function useAssistantOpen(
85
+ topic?: string,
86
+ onOpen?: (request: AssistantOpenRequest) => void,
87
+ ): (request: { prompt?: string }) => void {
88
+ const assistant = useAssistantRuntime();
89
+ const runtimeOpen = assistant?.open;
90
+ const source = assistant?.source;
91
+ return useCallback(
92
+ (request: { prompt?: string }) => {
93
+ const full: AssistantOpenRequest = { ...request, topic };
94
+ if (onOpen) onOpen(full);
95
+ else if (runtimeOpen) runtimeOpen(full);
96
+ else openAskAi(source, request);
97
+ },
98
+ [onOpen, runtimeOpen, source, topic],
99
+ );
100
+ }
101
+
102
+ // ─── The questions the page already shows ────────────────────────────────────
103
+ // Every surface that shows questions says which ones (`useShownAskPrompts`), so
104
+ // a later surface (the FAQ's card) never repeats one, with no wiring by the
105
+ // host. One list per QUESTIONS ENDPOINT (the runtime's `askPromptsUrl`): two
106
+ // assistants on one page read different questions, and never leave out each
107
+ // other's ids. A module store, read through React's external-store hook.
108
+ // `null` for a surface = it is still picking.
109
+
110
+ const NO_IDS: readonly string[] = [];
111
+
112
+ /** One endpoint's list: which questions each surface shows, and who is listening. */
113
+ interface ShownList {
114
+ set(surface: string, ids: readonly string[] | null): void;
115
+ remove(surface: string): void;
116
+ subscribe(listener: () => void): () => void;
117
+ read(): readonly string[] | null;
118
+ }
119
+
120
+ function createShownList(): ShownList {
121
+ const bySurface = new Map<string, readonly string[] | null>();
122
+ const listeners = new Set<() => void>();
123
+ let snapshot: readonly string[] | null = NO_IDS;
124
+ const publish = (): void => {
125
+ const lists = [...bySurface.values()];
126
+ const next = lists.some(list => list === null) ? null : lists.flatMap(list => list ?? []);
127
+ const same = next === null ? snapshot === null : snapshot !== null && next.join(',') === snapshot.join(',');
128
+ if (same) return;
129
+ snapshot = next === null ? null : next.length > 0 ? next : NO_IDS;
130
+ for (const listener of listeners) listener();
131
+ };
132
+ return {
133
+ set(surface, ids) {
134
+ bySurface.set(surface, ids);
135
+ publish();
136
+ },
137
+ remove(surface) {
138
+ bySurface.delete(surface);
139
+ publish();
140
+ },
141
+ subscribe(listener) {
142
+ listeners.add(listener);
143
+ return () => listeners.delete(listener);
144
+ },
145
+ read: () => snapshot,
146
+ };
147
+ }
148
+
149
+ const shownLists = new Map<string, ShownList>();
150
+
151
+ /** The list a surface belongs to: the questions endpoint of its assistant runtime. */
152
+ function useShownList(): ShownList {
153
+ const scope = useAssistantRuntime()?.askPromptsUrl ?? '';
154
+ let list = shownLists.get(scope);
155
+ if (!list) {
156
+ list = createShownList();
157
+ shownLists.set(scope, list);
158
+ }
159
+ return list;
160
+ }
161
+
162
+ /**
163
+ * Say which questions this surface shows. `null`: it is still picking, so a
164
+ * surface that must not repeat them waits. Pass `undefined` to say nothing
165
+ * (the surface shows no questions here).
166
+ */
167
+ export function useShownAskPrompts(ids: readonly string[] | null | undefined): void {
168
+ const surface = useId();
169
+ const list = useShownList();
170
+ const key = ids === undefined ? undefined : ids === null ? null : ids.join(',');
171
+ // A layout effect: the list holds this surface before any reader's own effects run.
172
+ useLayoutEffect(() => {
173
+ if (key === undefined) return undefined;
174
+ list.set(surface, key === null ? null : key ? key.split(',') : NO_IDS);
175
+ return () => list.remove(surface);
176
+ }, [list, surface, key]);
177
+ }
178
+
179
+ /** The ids of the questions the page's other surfaces of the same assistant show; `null` while one is still picking. */
180
+ export function useShownAskPromptIds(): readonly string[] | null {
181
+ const list = useShownList();
182
+ return useSyncExternalStore(list.subscribe, list.read, () => NO_IDS);
183
+ }
184
+
185
+ /** Tests only: forget every surface. */
186
+ export function resetShownAskPrompts(): void {
187
+ shownLists.clear();
188
+ }
@@ -24,6 +24,7 @@ export * from './chat-message-skeleton';
24
24
 
25
25
  export * from './quick-action-chip';
26
26
  export * from './ask-prompts';
27
+ export * from './assistant-ask-prompts';
27
28
  export * from './quick-action-marquee';
28
29
  export * from './quick-action-wall';
29
30
  export * from './chat-ticket-list';
@@ -91,6 +91,11 @@ const OpenmspLogoIcon: IconComponent = ({ size = 16, className }) => (
91
91
  />
92
92
  );
93
93
  const FlamingoLogoIcon = sizedLogo(FlamingoLogo);
94
+ // The same mark as a GLYPH: it takes the colour of the slot it sits in, like every icon beside it (a source
95
+ // chip, a command row, a card), where the brand pink would be the one coloured icon in the list.
96
+ const FlamingoLogoGreyIcon: IconComponent = ({ size = 16, className, color }) => (
97
+ <FlamingoLogo className={className} fill={color ?? 'currentColor'} style={{ width: size, height: size }} />
98
+ );
94
99
  // The two community marks size themselves through a `size` prop, the resolver's own contract.
95
100
  // Major League GitHub draws its mark in the theme's text colour on its own site
96
101
  // (its header, hero and footer), never in an accent: so does every glyph of it.
@@ -174,6 +179,7 @@ export const ICON_ALIASES: Record<string, IconComponent> = {
174
179
  'tmcg-logo': TmcgLogoIcon,
175
180
  flamingo: FlamingoLogoIcon,
176
181
  'flamingo-logo': FlamingoLogoIcon,
182
+ 'flamingo-logo-grey': FlamingoLogoGreyIcon,
177
183
  megaphone: Megaphone,
178
184
  bell: Bell,
179
185
  info: Info,
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+
3
+ import { useMemo, useState } from 'react';
4
+ import {
5
+ AssistantRuntimeContext,
6
+ useAssistantRuntime,
7
+ type AssistantOpenRequest,
8
+ type AssistantRuntime,
9
+ } from '../../contexts/assistant-runtime-context';
10
+ import { useInView } from '../../hooks/ui/use-in-view';
11
+ import { AskCard } from '../chat/ask-prompts';
12
+ import {
13
+ assistantAvailable,
14
+ ASK_PROMPTS_DEFAULT_COUNT,
15
+ useAskPrompts,
16
+ useAssistantOpen,
17
+ useShownAskPromptIds,
18
+ } from '../chat/hooks/use-ask-prompts';
19
+
20
+ /** The card beside a FAQ, as a host may shape it. Every field is optional. */
21
+ export interface FaqAskOptions {
22
+ /**
23
+ * What this FAQ is about: ANY string the host's questions endpoint knows (it
24
+ * is sent as `section`, and reported with every click). Questions written
25
+ * for the topic are picked first. Whoever renders the FAQ passes it. Absent:
26
+ * the entity the FAQ is attached to (`FaqSection`'s `entityType`). With
27
+ * neither, the FAQ has no card: the lib names no topic of its own.
28
+ */
29
+ topic?: string;
30
+ /**
31
+ * Ids of questions never to offer. Absent: the ones the page's other "ask"
32
+ * surfaces show (`AssistantAskPrompts` rows say so themselves), which the
33
+ * card waits for. `null`: wait.
34
+ */
35
+ exclude?: readonly string[] | null;
36
+ /** How many questions the card offers. Absent: the runtime's, else three. */
37
+ count?: number;
38
+ /** The card's wording. `{assistant}` is replaced by the assistant's name. Absent: the runtime's, else the lib's. */
39
+ title?: string;
40
+ description?: string;
41
+ /**
42
+ * How the chat is opened and asked from THIS card. Absent: the assistant
43
+ * runtime's `open`, else the `ask-ai:open` event of its source. A FAQ that
44
+ * sits beside its own chat (an embedded one) passes that chat's opener.
45
+ */
46
+ onOpen?: (request: AssistantOpenRequest) => void;
47
+ }
48
+
49
+ /** The lib's own wording, when neither the host nor the runtime states it. */
50
+ const DEFAULT_ASK_CARD = {
51
+ title: 'Still deciding?',
52
+ description: '{assistant} answers from our docs and customer stories.',
53
+ } as const;
54
+
55
+ /** How far below the viewport the card is when it picks its questions. */
56
+ const PICK_ROOT_MARGIN = '600px';
57
+
58
+ const fillAssistant = (text: string, name: string) => text.split('{assistant}').join(name);
59
+
60
+ /** True when a FAQ under this runtime shows the card: a chat to open, and a name for the card's words. */
61
+ export function faqAskCardShown(assistant: AssistantRuntime | null): assistant is AssistantRuntime {
62
+ return assistantAvailable(assistant) && !!assistant.name;
63
+ }
64
+
65
+ /**
66
+ * The "ask the assistant" card of a FAQ: the assistant's identity and the
67
+ * chat's opener from the nearest assistant runtime, its questions from the
68
+ * host's endpoint. It picks ONCE, when the card first comes near the viewport:
69
+ * by then the page above has said which questions it shows itself, and a
70
+ * visitor who never reaches the FAQ costs no request. The card is the same
71
+ * height before and after the questions arrive. A host with no questions
72
+ * endpoint gets the launcher with no questions.
73
+ */
74
+ export function FaqAskCard({ topic, exclude, count, title, description, onOpen }: FaqAskOptions & { topic: string }) {
75
+ const assistant = useAssistantRuntime();
76
+ const { ref, inView } = useInView<HTMLDivElement>({ rootMargin: PICK_ROOT_MARGIN });
77
+ const [reached, setReached] = useState(false);
78
+ if (inView && !reached) setReached(true);
79
+
80
+ const shownElsewhere = useShownAskPromptIds();
81
+ const excluded = exclude === undefined ? shownElsewhere : exclude;
82
+ const slots = count ?? assistant?.askCard?.count ?? ASK_PROMPTS_DEFAULT_COUNT;
83
+ const { prompts, isLoading, supported } = useAskPrompts({
84
+ topic,
85
+ count: slots,
86
+ exclude: excluded ?? undefined,
87
+ enabled: reached && excluded !== null,
88
+ });
89
+ const open = useAssistantOpen(topic, onOpen);
90
+ // The card's launcher opens the same chat as its questions, with the card's topic: it reads the runtime.
91
+ const scoped = useMemo(() => (assistant ? { ...assistant, open } : null), [assistant, open]);
92
+
93
+ if (!faqAskCardShown(assistant) || !assistant.name) return null;
94
+ const name = assistant.name;
95
+ const cardDescription = description ?? assistant.askCard?.description ?? DEFAULT_ASK_CARD.description;
96
+ return (
97
+ <AssistantRuntimeContext.Provider value={scoped}>
98
+ <div ref={ref}>
99
+ <AskCard
100
+ title={fillAssistant(title ?? assistant.askCard?.title ?? DEFAULT_ASK_CARD.title, name)}
101
+ description={cardDescription ? fillAssistant(cardDescription, name) : undefined}
102
+ prompts={prompts}
103
+ // Fixed slots, one question per row: chip skeletons until the pick lands, the same box after.
104
+ count={supported ? slots : 0}
105
+ loading={isLoading}
106
+ source={assistant.source}
107
+ label={name}
108
+ icon={assistant.icon}
109
+ onOpen={open}
110
+ onAsk={prompt => assistant.onAsk?.({ promptId: prompt.id, topic })}
111
+ />
112
+ </div>
113
+ </AssistantRuntimeContext.Provider>
114
+ );
115
+ }
@@ -23,7 +23,7 @@
23
23
  import { useRouter } from '../../embed-shims/next-navigation';
24
24
  import type { Faq } from '../../types/faq';
25
25
  import { PageShell, PageLayout } from '../ui';
26
- import { FaqSection } from './faq-section';
26
+ import { FaqSection, type FaqSectionProps } from './faq-section';
27
27
  import type { FaqSchemaOptions } from './json-ld';
28
28
 
29
29
  export interface FaqDocumentPageProps {
@@ -54,6 +54,9 @@ export interface FaqDocumentPageProps {
54
54
  entityId?: number | string;
55
55
  /** Minimum FAQ count before the section renders (forwarded to `FaqSection`). */
56
56
  minResults?: number;
57
+ /** The "ask the assistant" card beside the questions (forwarded to `FaqSection`): `false` switches it off, an
58
+ * object states its topic, wording and the chat it opens. */
59
+ ask?: FaqSectionProps['ask'];
57
60
  }
58
61
 
59
62
  export function FaqDocumentPage({
@@ -67,6 +70,7 @@ export function FaqDocumentPage({
67
70
  entityType,
68
71
  entityId,
69
72
  minResults,
73
+ ask,
70
74
  shell = true,
71
75
  }: FaqDocumentPageProps) {
72
76
  const router = useRouter();
@@ -93,6 +97,7 @@ export function FaqDocumentPage({
93
97
  entityType={entityType}
94
98
  entityId={entityId}
95
99
  minResults={minResults}
100
+ ask={ask}
96
101
  />
97
102
  </PageLayout>
98
103
  );
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type React from 'react';
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { useAssistantRuntime } from '../../contexts/assistant-runtime-context';
5
6
  import Link from '../../embed-shims/next-link';
6
7
  import { useScrollToHash } from '../../hooks/use-scroll-to-hash';
7
8
  import { useSelfFetch } from '../../hooks/use-self-fetch';
@@ -13,6 +14,7 @@ import { navigateSamePageHash, STICKY_HEADER_OFFSET_PX } from '../../utils/same-
13
14
  import { buildSuggestionUrl } from '../../utils/suggestion-url';
14
15
  import { FaqAccordion, type FaqItem } from '../faq-accordion';
15
16
  import { SECTION_HEADING_CLASS } from '../layout/page-heading';
17
+ import { FaqAskCard, faqAskCardShown, type FaqAskOptions } from './faq-ask-card';
16
18
  import { buildFaqJsonLdFromFaqs, type FaqSchemaOptions } from './json-ld';
17
19
 
18
20
  export interface FaqSectionProps {
@@ -48,12 +50,24 @@ export interface FaqSectionProps {
48
50
  * ('' = same-origin relative). */
49
51
  apiBaseUrl?: string;
50
52
  /**
51
- * A block beside the questions (an "ask the assistant" card). From the
52
- * content `lg` step the heading, the category nav and this block form a left
53
- * column and the questions the right one; below it the block follows the
54
- * questions. Absent: the single-column layout, unchanged.
53
+ * A block beside the questions, in place of the "ask the assistant" card.
54
+ * From the content `lg` step the heading, the category nav and this block
55
+ * form a left column and the questions the right one; below it the block
56
+ * follows the questions.
55
57
  */
56
58
  aside?: React.ReactNode;
59
+ /**
60
+ * The "ask the assistant" card beside the questions. Shown wherever the
61
+ * host's assistant runtime says a chat is there to open
62
+ * (`AssistantRuntimeContext`: its name, glyph, questions endpoint and
63
+ * opener) AND the FAQ has a topic: `ask.topic` (any string the host's
64
+ * questions endpoint knows), else the `entityType` the FAQ is attached to.
65
+ * No runtime or no topic: the FAQ is the single column it always was; the
66
+ * lib names no topic of its own. `false` switches the card off; the object
67
+ * also states the questions to leave out, the wording and the chat it opens
68
+ * (`onOpen`). An `aside` takes the card's place.
69
+ */
70
+ ask?: false | FaqAskOptions;
57
71
  }
58
72
 
59
73
  const DEFAULT_HEADING_TEXT = 'Frequently Asked Questions';
@@ -392,8 +406,16 @@ export function FaqSection({
392
406
  className,
393
407
  minResults,
394
408
  apiBaseUrl = '',
395
- aside,
409
+ aside: asideProp,
410
+ ask,
396
411
  }: FaqSectionProps) {
412
+ const assistant = useAssistantRuntime();
413
+ // The host's own block wins; else the card, where a chat is there to open and the FAQ has a topic
414
+ // (an empty topic is no topic, as the questions request reads it).
415
+ const askTopic = ask === false ? undefined : ask?.topic || entityType;
416
+ const aside =
417
+ asideProp ??
418
+ (ask !== false && askTopic && faqAskCardShown(assistant) ? <FaqAskCard {...ask} topic={askTopic} /> : undefined);
397
419
  const url = buildFaqsUrl(entityType, entityId, minResults, apiBaseUrl);
398
420
  // Memoized — useSelfFetch re-syncs on [initialData]; a fresh per-render
399
421
  // wrapper object would setState-loop under re-rendering parents.
@@ -11,3 +11,4 @@
11
11
  // block and exposed via "./components/faq/json-ld" in package.json#exports.)
12
12
  export { FaqSection, type FaqSectionProps } from './faq-section';
13
13
  export { FaqDocumentPage, type FaqDocumentPageProps } from './faq-document-page';
14
+ export { FaqAskCard, type FaqAskOptions } from './faq-ask-card';
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { ReactNode } from 'react';
4
4
  import { cn } from '../../../utils/cn';
5
+ import { TRIAL_LABELS } from '../../../utils/trial';
5
6
  import { OpenFrameLogo } from '../../icons';
6
7
  import { FileOffIcon } from '../../icons-v2-generated/documents/file-off-icon';
7
8
  import { CreditCardXmarkIcon } from '../../icons-v2-generated/finance/credit-card-xmark-icon';
@@ -29,10 +30,10 @@ const DEFAULT_DESCRIPTION =
29
30
  'All your core ops in one place - built for MSPs who are done duct-taping tools together. Unified stack, AI-ready, no vendor tax. Just solid software that lets you run lean and fast.';
30
31
 
31
32
  const DEFAULT_BENEFITS: AuthBenefit[] = [
32
- { icon: <CreditCardXmarkIcon />, label: 'No card required' },
33
- { icon: <XmarkAltIcon />, label: 'Cancel Anytime' },
34
- { icon: <FlaskVialIcon />, label: '14 day free trial' },
35
- { icon: <FileOffIcon />, label: 'No Contract' },
33
+ { icon: <CreditCardXmarkIcon />, label: TRIAL_LABELS.noCard },
34
+ { icon: <XmarkAltIcon />, label: TRIAL_LABELS.cancel },
35
+ { icon: <FlaskVialIcon />, label: TRIAL_LABELS.length },
36
+ { icon: <FileOffIcon />, label: TRIAL_LABELS.noContract },
36
37
  ];
37
38
 
38
39
  /**
@@ -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(
@@ -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
+ }