@flamingo-stack/openframe-frontend-core 0.0.725 → 0.0.728

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 (107) hide show
  1. package/dist/{chunk-JVHP7Y4S.js → chunk-2F4DO7DS.js} +2 -2
  2. package/dist/{chunk-4ZZ4TNCY.cjs → chunk-32QAIKXR.cjs} +9 -9
  3. package/dist/{chunk-AI3CLDZO.cjs → chunk-33QHEG7K.cjs} +289 -285
  4. package/dist/{chunk-QV4GO3GW.cjs → chunk-4ICMFNCJ.cjs} +29 -56
  5. package/dist/{chunk-YPEILGVZ.cjs → chunk-5K3QUH7C.cjs} +3 -3
  6. package/dist/{chunk-5FVWDDX6.js → chunk-66XG5RFJ.js} +2 -2
  7. package/dist/{chunk-ZSXZE4FJ.cjs → chunk-7N6M77PI.cjs} +3 -3
  8. package/dist/{chunk-2VOZ37BL.cjs → chunk-7VYBSCCL.cjs} +35 -35
  9. package/dist/{chunk-7V5KFBY2.cjs → chunk-APJCBQ7X.cjs} +16 -16
  10. package/dist/{chunk-RU6BTELI.cjs → chunk-CELBOIKC.cjs} +538 -491
  11. package/dist/{chunk-SEVKFMKR.cjs → chunk-CI7KWY6L.cjs} +3 -3
  12. package/dist/{chunk-OBMWSUBK.js → chunk-EM3WCK2K.js} +5 -5
  13. package/dist/{chunk-ASHRHL4W.js → chunk-GEUUDNCI.js} +5 -5
  14. package/dist/{chunk-ZZRUKEN2.js → chunk-GINCQZMM.js} +3 -3
  15. package/dist/{chunk-AX4F35C2.js → chunk-HP4C2DFL.js} +1 -1
  16. package/dist/{chunk-ASIXPRHV.js → chunk-HTQJSW3Y.js} +3 -3
  17. package/dist/{chunk-SE4O4HCD.cjs → chunk-OKEJY2GC.cjs} +8 -8
  18. package/dist/{chunk-B7234BE2.cjs → chunk-OYU7QNLQ.cjs} +11 -11
  19. package/dist/{chunk-PUADVXJD.js → chunk-QL4RUHKU.js} +1 -1
  20. package/dist/{chunk-LYQYLPPV.cjs → chunk-RMXLHYEG.cjs} +26 -26
  21. package/dist/{chunk-MGTZ2BKY.js → chunk-SKEW5NMJ.js} +22 -49
  22. package/dist/{chunk-772242QR.cjs → chunk-SSXQYS5K.cjs} +65 -65
  23. package/dist/{chunk-F56GNKX2.js → chunk-TYMLEI2I.js} +2 -2
  24. package/dist/{chunk-MLFS5G3V.js → chunk-UF3NFAQK.js} +315 -268
  25. package/dist/{chunk-2SO3OJFY.cjs → chunk-UJZ5Y3RK.cjs} +33 -33
  26. package/dist/{chunk-W2DXHR2R.js → chunk-URYLQ4MC.js} +15 -11
  27. package/dist/{chunk-F77J34W2.js → chunk-VGRQK7YS.js} +1 -1
  28. package/dist/{chunk-IOTAUWWF.js → chunk-XYSKJ7GA.js} +1 -1
  29. package/dist/{chunk-LR6DJY4Z.cjs → chunk-YREYU6DY.cjs} +9 -9
  30. package/dist/{chunk-JAKG6BY7.js → chunk-ZDGK2TNJ.js} +3 -3
  31. package/dist/{cloud-tenants-SMULXU54.cjs → cloud-tenants-XP3L6PWO.cjs} +3 -3
  32. package/dist/{cloud-tenants-US63E2HV.js → cloud-tenants-XVVFCOK2.js} +2 -2
  33. package/dist/components/case-studies/index.cjs +14 -14
  34. package/dist/components/case-studies/index.js +3 -3
  35. package/dist/components/chat/assistant-ask-prompts.d.ts +34 -14
  36. package/dist/components/chat/assistant-ask-prompts.d.ts.map +1 -1
  37. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  38. package/dist/components/chat/hooks/index.d.ts +1 -1
  39. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  40. package/dist/components/chat/hooks/use-ask-prompts.d.ts +34 -7
  41. package/dist/components/chat/hooks/use-ask-prompts.d.ts.map +1 -1
  42. package/dist/components/chat/index.cjs +2 -4
  43. package/dist/components/chat/index.js +3 -5
  44. package/dist/components/contact/index.cjs +4 -4
  45. package/dist/components/contact/index.js +3 -3
  46. package/dist/components/docs/index.cjs +7 -7
  47. package/dist/components/docs/index.js +6 -6
  48. package/dist/components/embeds/index.cjs +4 -4
  49. package/dist/components/embeds/index.js +3 -3
  50. package/dist/components/faq/faq-ask-card.d.ts +8 -11
  51. package/dist/components/faq/faq-ask-card.d.ts.map +1 -1
  52. package/dist/components/faq/index.cjs +4 -4
  53. package/dist/components/faq/index.js +3 -3
  54. package/dist/components/features/auth/auth-benefits-panel.d.ts +3 -1
  55. package/dist/components/features/auth/auth-benefits-panel.d.ts.map +1 -1
  56. package/dist/components/features/auth/auth-shell.d.ts +3 -1
  57. package/dist/components/features/auth/auth-shell.d.ts.map +1 -1
  58. package/dist/components/features/index.cjs +3 -3
  59. package/dist/components/features/index.js +2 -2
  60. package/dist/components/help-center-pages/index.cjs +71 -71
  61. package/dist/components/help-center-pages/index.js +13 -13
  62. package/dist/components/index.cjs +35 -37
  63. package/dist/components/index.js +14 -16
  64. package/dist/components/meeting-scheduler/index.cjs +19 -19
  65. package/dist/components/meeting-scheduler/index.js +2 -2
  66. package/dist/components/navigation/index.cjs +6 -6
  67. package/dist/components/navigation/index.js +5 -5
  68. package/dist/components/onboarding-guides/index.cjs +5 -5
  69. package/dist/components/onboarding-guides/index.js +4 -4
  70. package/dist/components/product-demo/index.cjs +6 -6
  71. package/dist/components/product-demo/index.js +6 -6
  72. package/dist/components/tickets/index.cjs +7 -7
  73. package/dist/components/tickets/index.js +6 -6
  74. package/dist/components/ui/index.cjs +3 -5
  75. package/dist/components/ui/index.js +4 -6
  76. package/dist/{devices-6ZURNFRB.cjs → devices-MMHLY67A.cjs} +4 -4
  77. package/dist/{devices-JWHGWKLK.js → devices-S7PJJI66.js} +2 -2
  78. package/dist/index.cjs +7 -7
  79. package/dist/index.js +14 -14
  80. package/dist/{knowledge-CY66JZ54.cjs → knowledge-LJNFAJNE.cjs} +4 -4
  81. package/dist/{knowledge-TEDPLCFX.js → knowledge-WW3LOQKN.js} +2 -2
  82. package/dist/{logs-MO357RE7.cjs → logs-F4ZVNUKQ.cjs} +11 -11
  83. package/dist/{logs-S46THPPD.js → logs-I4DMHQDA.js} +2 -2
  84. package/dist/{policies-GSH4K5YP.cjs → policies-KZXFXGD7.cjs} +7 -7
  85. package/dist/{policies-BUXT7DKF.js → policies-RX55V46V.js} +2 -2
  86. package/dist/{software-update-2ETT35NM.cjs → software-update-4A242VDB.cjs} +6 -6
  87. package/dist/{software-update-YKS6KLX2.js → software-update-VI7FQYR4.js} +2 -2
  88. package/dist/utils/index.cjs +25 -24
  89. package/dist/utils/index.d.ts +1 -1
  90. package/dist/utils/index.d.ts.map +1 -1
  91. package/dist/utils/index.js +23 -23
  92. package/dist/utils/source-grouping.d.ts +15 -5
  93. package/dist/utils/source-grouping.d.ts.map +1 -1
  94. package/dist/utils/trial.d.ts +17 -9
  95. package/dist/utils/trial.d.ts.map +1 -1
  96. package/package.json +1 -1
  97. package/src/components/chat/assistant-ask-prompts.tsx +84 -39
  98. package/src/components/chat/entity-cards/dispatch.tsx +47 -58
  99. package/src/components/chat/hooks/index.ts +3 -2
  100. package/src/components/chat/hooks/use-ask-prompts.ts +140 -56
  101. package/src/components/faq/faq-ask-card.tsx +22 -67
  102. package/src/components/features/auth/auth-benefits-panel.tsx +15 -8
  103. package/src/components/features/auth/auth-shell.tsx +4 -2
  104. package/src/utils/index.ts +1 -1
  105. package/src/utils/source-grouping.ts +24 -14
  106. package/src/utils/source-icons.ts +16 -16
  107. package/src/utils/trial.ts +16 -10
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useId, useLayoutEffect, useSyncExternalStore } from 'react';
3
+ import { useCallback, useId, useLayoutEffect, useMemo, useState, useSyncExternalStore } from 'react';
4
4
  import {
5
5
  useAssistantRuntime,
6
6
  type AssistantOpenRequest,
@@ -71,8 +71,10 @@ export function useAskPrompts({
71
71
  const assistant = useAssistantRuntime();
72
72
  const base = assistantAvailable(assistant) ? assistant.askPromptsUrl : undefined;
73
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 };
74
+ const { data, dataUrl, error } = useSelfFetch<AskPromptsResponse>(url);
75
+ // Landed for THIS url (or failed): never the frame between a url appearing and its fetch starting.
76
+ const landed = url !== null && (dataUrl === url || error);
77
+ return { prompts: landed ? (data?.prompts ?? []) : [], isLoading: !!base && !landed, supported: !!base };
76
78
  }
77
79
 
78
80
  /**
@@ -99,90 +101,172 @@ export function useAssistantOpen(
99
101
  );
100
102
  }
101
103
 
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.
104
+ // ─── One pick at a time, and never a question twice ──────────────────────────
105
+ // Every surface that offers questions (a row in a section, the FAQ's card)
106
+ // takes a TURN before it picks, in the order the surfaces came to need one:
107
+ // document order for the ones a page mounts together, later for one that waits
108
+ // to be reached (the FAQ's card). A surface picks once every surface ahead of
109
+ // it has settled, and leaves out every question the page already shows. So no
110
+ // two surfaces of a page can show the same question, whatever their topics,
111
+ // with no wiring by the host. One queue per QUESTIONS ENDPOINT (the runtime's
112
+ // `askPromptsUrl`): two assistants on one page read different questions, and
113
+ // never wait for or leave out each other's. A module store, read through
114
+ // React's external-store hook.
109
115
 
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
+ /** One endpoint's queue: each surface's turn and the questions it settled on (`null`: not yet). */
117
+ interface AskQueue {
118
+ enter(surface: string): void;
119
+ settle(surface: string, ids: readonly string[] | null): void;
120
+ leave(surface: string): void;
116
121
  subscribe(listener: () => void): () => void;
117
- read(): readonly string[] | null;
122
+ /**
123
+ * What `surface` must leave out, as a comma-joined list ('' for nothing),
124
+ * once it may pick; `null` while it has no turn or a surface ahead of it is
125
+ * still picking. A string, so React can compare two readings.
126
+ */
127
+ turnOf(surface: string): string | null;
118
128
  }
119
129
 
120
- function createShownList(): ShownList {
121
- const bySurface = new Map<string, readonly string[] | null>();
130
+ function createAskQueue(): AskQueue {
131
+ const entries = new Map<string, { turn: number; ids: readonly string[] | null }>();
122
132
  const listeners = new Set<() => void>();
123
- let snapshot: readonly string[] | null = NO_IDS;
133
+ let next = 0;
124
134
  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
135
  for (const listener of listeners) listener();
131
136
  };
132
137
  return {
133
- set(surface, ids) {
134
- bySurface.set(surface, ids);
138
+ enter(surface) {
139
+ if (entries.has(surface)) return;
140
+ entries.set(surface, { turn: next++, ids: null });
135
141
  publish();
136
142
  },
137
- remove(surface) {
138
- bySurface.delete(surface);
143
+ settle(surface, ids) {
144
+ const entry = entries.get(surface);
145
+ if (!entry || (entry.ids?.join(',') ?? null) === (ids?.join(',') ?? null)) return;
146
+ entry.ids = ids;
139
147
  publish();
140
148
  },
149
+ leave(surface) {
150
+ if (entries.delete(surface)) publish();
151
+ },
141
152
  subscribe(listener) {
142
153
  listeners.add(listener);
143
154
  return () => listeners.delete(listener);
144
155
  },
145
- read: () => snapshot,
156
+ turnOf(surface) {
157
+ const own = entries.get(surface);
158
+ if (!own) return null;
159
+ const shown: string[] = [];
160
+ for (const [other, entry] of entries) {
161
+ if (other === surface) continue;
162
+ if (entry.ids === null) {
163
+ if (entry.turn < own.turn) return null;
164
+ continue;
165
+ }
166
+ shown.push(...entry.ids);
167
+ }
168
+ return shown.join(',');
169
+ },
146
170
  };
147
171
  }
148
172
 
149
- const shownLists = new Map<string, ShownList>();
173
+ const askQueues = new Map<string, AskQueue>();
150
174
 
151
- /** The list a surface belongs to: the questions endpoint of its assistant runtime. */
152
- function useShownList(): ShownList {
175
+ /** The queue a surface belongs to: the questions endpoint of its assistant runtime. */
176
+ function useAskQueue(): AskQueue {
153
177
  const scope = useAssistantRuntime()?.askPromptsUrl ?? '';
154
- let list = shownLists.get(scope);
155
- if (!list) {
156
- list = createShownList();
157
- shownLists.set(scope, list);
178
+ let queue = askQueues.get(scope);
179
+ if (!queue) {
180
+ queue = createAskQueue();
181
+ askQueues.set(scope, queue);
158
182
  }
159
- return list;
183
+ return queue;
184
+ }
185
+
186
+ export interface UseAskSurfaceOptions {
187
+ /** What the surface is about (see `UseAskPromptsOptions.topic`). */
188
+ topic?: string;
189
+ count?: number;
190
+ /**
191
+ * Ids of questions never to offer. Absent: the ones the page's other
192
+ * surfaces show, which this one waits for. A list: exactly those, picked at
193
+ * once with no wait (the surfaces after it still leave its questions out).
194
+ * `null`: the surface waits and never picks.
195
+ */
196
+ exclude?: readonly string[] | null;
197
+ /** False: the surface is not ready to pick yet (it takes its turn when it is). Default true. */
198
+ enabled?: boolean;
199
+ /** How the chat is opened from THIS surface. Absent: the assistant runtime's opener. */
200
+ onOpen?: (request: AssistantOpenRequest) => void;
201
+ }
202
+
203
+ export interface UseAskSurfaceResult extends UseAskPromptsResult {
204
+ /** The nearest assistant runtime; `null` with none mounted. */
205
+ assistant: AssistantRuntime | null;
206
+ /** The same runtime with THIS surface's opener: what the surface's launcher reads. */
207
+ scoped: AssistantRuntime | null;
208
+ /** Opens the chat (and asks, with a prompt), carrying the surface's topic. */
209
+ open: (request: { prompt?: string }) => void;
210
+ /** Reports a question that was asked (the runtime's `onAsk`, with the topic). */
211
+ reportAsk: (prompt: AskPrompt) => void;
160
212
  }
161
213
 
162
214
  /**
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).
215
+ * THE wiring of an "ask" surface, whatever it looks like: the assistant
216
+ * runtime, the surface's questions (picked on its turn, never one the page
217
+ * already shows), the opener and the click report. `AssistantAskPrompts` is its
218
+ * one component; nothing else picks questions for a page.
166
219
  */
167
- export function useShownAskPrompts(ids: readonly string[] | null | undefined): void {
220
+ export function useAskSurface({
221
+ topic,
222
+ count = ASK_PROMPTS_DEFAULT_COUNT,
223
+ exclude,
224
+ enabled = true,
225
+ onOpen,
226
+ }: UseAskSurfaceOptions = {}): UseAskSurfaceResult {
227
+ const assistant = useAssistantRuntime();
168
228
  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.
229
+ const queue = useAskQueue();
230
+ const picks = assistantAvailable(assistant) && !!assistant.askPromptsUrl && enabled && exclude !== null;
231
+
232
+ // A layout effect: surfaces a page mounts together take their turns in document order.
172
233
  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
- }
234
+ if (!picks) return undefined;
235
+ queue.enter(surface);
236
+ return () => queue.leave(surface);
237
+ }, [queue, surface, picks]);
238
+
239
+ const turn = useSyncExternalStore(
240
+ queue.subscribe,
241
+ () => queue.turnOf(surface),
242
+ () => null,
243
+ );
244
+ // ONE pick per mount: what the page showed when this surface's turn came is what it leaves out, for good.
245
+ const [shownAtTurn, setShownAtTurn] = useState<string | null>(null);
246
+ if (picks && turn !== null && shownAtTurn === null) setShownAtTurn(turn);
247
+ const leftOut = useMemo(() => exclude ?? (shownAtTurn ? shownAtTurn.split(',') : undefined), [exclude, shownAtTurn]);
248
+
249
+ const { prompts, isLoading, supported } = useAskPrompts({
250
+ topic,
251
+ count,
252
+ exclude: leftOut,
253
+ // Told exactly what to leave out, a surface needs nobody's pick: it never waits for a turn.
254
+ enabled: picks && (exclude !== undefined || shownAtTurn !== null),
255
+ });
256
+ const settled = picks && !isLoading ? prompts.map(prompt => prompt.id).join(',') : null;
257
+ useLayoutEffect(() => {
258
+ queue.settle(surface, settled === null ? null : settled ? settled.split(',') : []);
259
+ }, [queue, surface, settled]);
178
260
 
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);
261
+ const open = useAssistantOpen(topic, onOpen);
262
+ const scoped = useMemo(() => (assistant ? { ...assistant, open } : null), [assistant, open]);
263
+ const onAsk = assistant?.onAsk;
264
+ const reportAsk = useCallback((prompt: AskPrompt) => onAsk?.({ promptId: prompt.id, topic }), [onAsk, topic]);
265
+ // A surface that waits (not reached, or told to) is loading wherever questions can exist.
266
+ return { assistant, scoped, prompts, isLoading: supported && (!picks || isLoading), supported, open, reportAsk };
183
267
  }
184
268
 
185
269
  /** Tests only: forget every surface. */
186
- export function resetShownAskPrompts(): void {
187
- shownLists.clear();
270
+ export function resetAskSurfaces(): void {
271
+ askQueues.clear();
188
272
  }
@@ -1,21 +1,10 @@
1
1
  'use client';
2
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';
3
+ import { useState } from 'react';
4
+ import type { AssistantOpenRequest, AssistantRuntime } from '../../contexts/assistant-runtime-context';
10
5
  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';
6
+ import { AssistantAskPrompts } from '../chat/assistant-ask-prompts';
7
+ import { assistantAvailable } from '../chat/hooks/use-ask-prompts';
19
8
 
20
9
  /** The card beside a FAQ, as a host may shape it. Every field is optional. */
21
10
  export interface FaqAskOptions {
@@ -29,8 +18,7 @@ export interface FaqAskOptions {
29
18
  topic?: string;
30
19
  /**
31
20
  * 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.
21
+ * surfaces show, which the card waits for. `null`: wait.
34
22
  */
35
23
  exclude?: readonly string[] | null;
36
24
  /** How many questions the card offers. Absent: the runtime's, else three. */
@@ -46,70 +34,37 @@ export interface FaqAskOptions {
46
34
  onOpen?: (request: AssistantOpenRequest) => void;
47
35
  }
48
36
 
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
37
  /** How far below the viewport the card is when it picks its questions. */
56
38
  const PICK_ROOT_MARGIN = '600px';
57
39
 
58
- const fillAssistant = (text: string, name: string) => text.split('{assistant}').join(name);
59
-
60
40
  /** True when a FAQ under this runtime shows the card: a chat to open, and a name for the card's words. */
61
41
  export function faqAskCardShown(assistant: AssistantRuntime | null): assistant is AssistantRuntime {
62
42
  return assistantAvailable(assistant) && !!assistant.name;
63
43
  }
64
44
 
65
45
  /**
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.
46
+ * The "ask the assistant" card of a FAQ: THE ask surface (`AssistantAskPrompts`)
47
+ * as a card, told when to pick. It picks ONCE, when the card first comes near
48
+ * the viewport: a visitor who never reaches the FAQ costs no request, and the
49
+ * card then leaves out every question the page above already shows. The card
50
+ * is the same height before and after the questions arrive.
73
51
  */
74
52
  export function FaqAskCard({ topic, exclude, count, title, description, onOpen }: FaqAskOptions & { topic: string }) {
75
- const assistant = useAssistantRuntime();
76
53
  const { ref, inView } = useInView<HTMLDivElement>({ rootMargin: PICK_ROOT_MARGIN });
77
54
  const [reached, setReached] = useState(false);
78
55
  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
56
  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>
57
+ <div ref={ref}>
58
+ <AssistantAskPrompts
59
+ variant="card"
60
+ topic={topic}
61
+ count={count}
62
+ exclude={exclude}
63
+ enabled={reached}
64
+ title={title}
65
+ description={description}
66
+ onOpen={onOpen}
67
+ />
68
+ </div>
114
69
  );
115
70
  }
@@ -2,7 +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
+ import { trialLabels } from '../../../utils/trial';
6
6
  import { OpenFrameLogo } from '../../icons';
7
7
  import { FileOffIcon } from '../../icons-v2-generated/documents/file-off-icon';
8
8
  import { CreditCardXmarkIcon } from '../../icons-v2-generated/finance/credit-card-xmark-icon';
@@ -19,6 +19,8 @@ export interface AuthBenefitsPanelProps {
19
19
  title?: string;
20
20
  description?: string;
21
21
  benefits?: AuthBenefit[];
22
+ /** The free trial's length in days, as the host read it from the billing plan. Without it the length is not stated. */
23
+ trialDays?: number | null;
22
24
  learnMoreLabel?: string;
23
25
  learnMoreUrl?: string;
24
26
  className?: string;
@@ -29,12 +31,16 @@ const DEFAULT_TITLE = 'The All-in-One Open Platform for MSPs';
29
31
  const DEFAULT_DESCRIPTION =
30
32
  '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.';
31
33
 
32
- const DEFAULT_BENEFITS: AuthBenefit[] = [
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 },
37
- ];
34
+ /** The default chips: the trial's terms, and its length when the host knows it. */
35
+ function defaultBenefits(trialDays: number | null | undefined): AuthBenefit[] {
36
+ const labels = trialLabels(trialDays);
37
+ return [
38
+ { icon: <CreditCardXmarkIcon />, label: labels.noCard },
39
+ { icon: <XmarkAltIcon />, label: labels.cancel },
40
+ ...(labels.length ? [{ icon: <FlaskVialIcon />, label: labels.length }] : []),
41
+ { icon: <FileOffIcon />, label: labels.noContract },
42
+ ];
43
+ }
38
44
 
39
45
  /**
40
46
  * Marketing panel shown alongside the auth forms — title, blurb, benefit chips
@@ -43,7 +49,8 @@ const DEFAULT_BENEFITS: AuthBenefit[] = [
43
49
  export function AuthBenefitsPanel({
44
50
  title = DEFAULT_TITLE,
45
51
  description = DEFAULT_DESCRIPTION,
46
- benefits = DEFAULT_BENEFITS,
52
+ trialDays,
53
+ benefits = defaultBenefits(trialDays),
47
54
  learnMoreLabel = 'Learn More About OpenFrame',
48
55
  learnMoreUrl = 'https://www.flamingo.run/',
49
56
  className,
@@ -12,6 +12,8 @@ export interface AuthShellProps {
12
12
  children: ReactNode;
13
13
  /** Marketing panel. Defaults to <AuthBenefitsPanel />. */
14
14
  benefits?: ReactNode;
15
+ /** The free trial's length in days, from the host's billing plan; the default panel states it when given. */
16
+ trialDays?: number | null;
15
17
  /** Pinned to the bottom-left of the form column on desktop only (e.g. "Back to Login"). */
16
18
  footer?: ReactNode;
17
19
  className?: string;
@@ -28,8 +30,8 @@ export interface AuthShellProps {
28
30
  * `of-auth-shell` on the root is a stable hook for shell/consumer CSS (the
29
31
  * native mobile shell pads it by the top safe-area inset).
30
32
  */
31
- export function AuthShell({ tabs, children, benefits, footer, className }: AuthShellProps) {
32
- const benefitsNode = benefits ?? <AuthBenefitsPanel />;
33
+ export function AuthShell({ tabs, children, benefits, trialDays, footer, className }: AuthShellProps) {
34
+ const benefitsNode = benefits ?? <AuthBenefitsPanel trialDays={trialDays} />;
33
35
 
34
36
  return (
35
37
  <div
@@ -19,7 +19,7 @@ export {
19
19
  export { formatDate, formatNumber, formatPrice, formatBytes, formatUrlForDisplay } from './format';
20
20
  export * from './ai-providers';
21
21
  export * from './model-token-rates';
22
- export { TRIAL_DAYS, TRIAL_LABELS } from './trial';
22
+ export { TRIAL_TERMS, trialLabels, trialLengthLabel } from './trial';
23
23
  export {
24
24
  VENDOR_CLASSIFICATIONS,
25
25
  OPENFRAME_CLASSIFICATIONS,
@@ -28,14 +28,23 @@ import { defaultDocumentTypeForTableId, getSourceLabel } from './source-icons';
28
28
  /**
29
29
  * Doc-table documentTypes: whole documents with their own viewer (markdown =
30
30
  * product docs, data_room_doc = data room). They carry an in-app `path`, an
31
- * embedder keys `docPlatformTargets` by them, and they NEVER group: one chip
32
- * per document. The hub's per-source contract test pins this list to its
31
+ * embedder keys `docPlatformTargets` by them, and a server never folds them
32
+ * into one citation (`groupsByTable`); the strip still draws them as one chip
33
+ * per table. The hub's per-source contract test pins this list to its
33
34
  * doc-table configs.
34
35
  */
35
36
  export const DOC_TABLE_TYPES = ['markdown', 'data_room_doc'] as const;
36
37
 
37
- /** THE rule: rows of a knowledge table share one chip unless they are whole
38
- * documents. Decided by `documentType`, the one fact every producer has. */
38
+ /**
39
+ * Which tables a SERVER folds into one citation: rows of a knowledge table
40
+ * share one number (and reach the model as one table) unless they are whole
41
+ * documents, which each keep their own number and are read in full. Decided by
42
+ * `documentType`, the one fact every producer has.
43
+ *
44
+ * This is not the strip's rule. The strip (`groupSourcesByTable`) draws ONE chip
45
+ * per table for every source, documents included: a document keeps its own
46
+ * citation number inside its table's chip.
47
+ */
39
48
  export function groupsByTable(documentType: string | null | undefined): boolean {
40
49
  return !(DOC_TABLE_TYPES as readonly string[]).includes(documentType ?? '');
41
50
  }
@@ -98,16 +107,16 @@ function typeOf(source: TableSource): string {
98
107
  return source.documentType || defaultDocumentTypeForTableId(source.sourceRepo) || '';
99
108
  }
100
109
 
101
- /** A source that belongs to its table's chip: it names its table and is not a
102
- * whole document. Both shapes qualify, a flat row AND a chip the server
103
- * already grouped, so one table is never drawn twice. Naming the table is the
104
- * ONLY requirement: a row with no `id`, or no `documentType`, still joins (as
105
- * the hub's server-side chips do). Leaving it out would split one table
106
- * across a group AND a stray chip. What it lacks only limits its dropdown
107
- * row: no `id` or no type means Open-only, no Ask. */
110
+ /** A source that belongs to its table's chip: it names its table. Whole
111
+ * documents join their table's chip like every other record (each keeps its
112
+ * own citation number as a dropdown row). Both shapes qualify, a flat row AND
113
+ * a chip the server already grouped, so one table is never drawn twice.
114
+ * Naming the table is the ONLY requirement: a row with no `id`, or no
115
+ * `documentType`, still joins (as the hub's server-side chips do). Leaving it
116
+ * out would split one table across a group AND a stray chip. What it lacks
117
+ * only limits its dropdown row: no `id` or no type means Open-only, no Ask. */
108
118
  function joinsTableChip(source: ChatSource): source is TableSource {
109
- if (!source.sourceRepo) return false;
110
- return groupsByTable(typeOf({ ...source, sourceRepo: source.sourceRepo }));
119
+ return !!source.sourceRepo;
111
120
  }
112
121
 
113
122
  /** A source's dropdown rows: a grouped chip's own items, or the flat row itself. */
@@ -154,7 +163,8 @@ function uniqueRows(rows: GroupedSourceRow[]): GroupedSourceRow[] {
154
163
  * table's only source. When flat rows of the same table arrive beside it (a
155
164
  * hub-injected chip plus MCP-cited rows in one answer), they merge into it:
156
165
  * its rows first, its chip-level link kept, the count restated. Whole
157
- * documents and rows naming no table pass through as their own chips.
166
+ * documents group under their table too, each row keeping its own citation
167
+ * number; only a row naming no table passes through as its own chip.
158
168
  */
159
169
  export function groupSourcesByTable(sources: ChatSource[]): ChatSource[] {
160
170
  const membersByTable = new Map<string, TableSource[]>();
@@ -64,11 +64,11 @@ export const SOURCE_ICON_NAMES: Record<string, string> = {
64
64
 
65
65
  // Financials
66
66
  'investor-updates': 'mail',
67
- 'financial-kpis': 'activity',
68
- 'financial-cap-table': 'table',
69
- 'financial-pnl': 'trending-up',
70
- 'financial-balance-sheet': 'dollar-sign',
71
- 'financial-cash-flow': 'banknote',
67
+ 'financial-kpis': 'chart-bar-01-ver',
68
+ 'financial-cap-table': 'chart-pie',
69
+ 'financial-pnl': 'money-bill-dollar',
70
+ 'financial-balance-sheet': 'bank',
71
+ 'financial-cash-flow': 'coins-exchange-currency',
72
72
 
73
73
  // ClickUp
74
74
  'clickup-roadmap': 'clickup',
@@ -92,24 +92,24 @@ export const SOURCE_ICON_NAMES: Record<string, string> = {
92
92
  'slack-messages': 'slack',
93
93
 
94
94
  // People-hub employee content
95
- 'what-i-shipped': 'package',
96
- 'how-i-work': 'book-open',
97
- 'ai-prompts': 'message-square',
95
+ 'what-i-shipped': 'rocket-02',
96
+ 'how-i-work': 'clipboard-list',
97
+ 'ai-prompts': 'chat-quote',
98
98
 
99
99
  // Product-hub internal
100
100
  'design-docs': 'file-text',
101
101
  'openframe-tenants': 'openframe',
102
- 'prospect-calls': 'phone',
102
+ 'prospect-calls': 'call',
103
103
 
104
104
  // Code intelligence
105
- 'code-rules': 'check-square',
106
- 'code-graph-repos': 'github',
105
+ 'code-rules': 'bracket-curly-check',
106
+ 'code-graph-repos': 'coding-branch',
107
107
  'code-graph-deployments': 'package',
108
- 'code-graph-files': 'file-text',
109
- 'code-symbols': 'box',
110
- 'code-duplicates': 'search',
111
- 'code-impact': 'activity',
112
- 'change-sets': 'coding-branch',
108
+ 'code-graph-files': 'file-code',
109
+ 'code-symbols': 'code-square',
110
+ 'code-duplicates': 'copy-01',
111
+ 'code-impact': 'coding-pull-request',
112
+ 'change-sets': 'coding-merge',
113
113
  };
114
114
 
115
115
  /** Lookup an icon name by RagTableConfig.id. Returns undefined when
@@ -1,19 +1,25 @@
1
1
  /**
2
- * The free trial's facts and the words every surface says them with: the
3
- * sign-up panel here, and the host site's trial buttons, trust rows and pricing
4
- * copy. Stated once, so a trial that changes length changes everywhere.
2
+ * The words every surface states the free trial with: the sign-up panel here,
3
+ * and the host site's trial buttons, trust rows and pricing copy.
4
+ *
5
+ * The trial's LENGTH is not stated here. It is the billing plan's, so a host
6
+ * passes the number it read from its server (`trialLabels(days)`); with no
7
+ * number the length is left out, never guessed.
5
8
  *
6
9
  * Server-safe (no React, no DOM).
7
10
  */
8
11
 
9
- /** How long the free trial runs, in days. */
10
- export const TRIAL_DAYS = 14;
11
-
12
- /** The reassurance labels shown beside a trial button. */
13
- export const TRIAL_LABELS = {
14
- /** "14 day free trial" */
15
- length: `${TRIAL_DAYS} day free trial`,
12
+ /** The reassurance labels that hold whatever the trial's length is. */
13
+ export const TRIAL_TERMS = {
16
14
  noCard: 'No card required',
17
15
  cancel: 'Cancel Anytime',
18
16
  noContract: 'No Contract',
19
17
  } as const;
18
+
19
+ /** "30 day free trial", from the length the billing plan states. A string is a template slot (`{trialDays}`). */
20
+ export const trialLengthLabel = (days: number | string): string => `${days} day free trial`;
21
+
22
+ /** The labels shown beside a trial button. `length` is null while the plan's length is not known. */
23
+ export function trialLabels(days: number | string | null | undefined) {
24
+ return { ...TRIAL_TERMS, length: days === null || days === undefined || days === '' ? null : trialLengthLabel(days) };
25
+ }