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

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 (95) hide show
  1. package/dist/{chunk-F56GNKX2.js → chunk-33AFAVBF.js} +2 -2
  2. package/dist/{chunk-7V5KFBY2.cjs → chunk-3D2SCG2E.cjs} +16 -16
  3. package/dist/{chunk-B7234BE2.cjs → chunk-3N3QA47Y.cjs} +11 -11
  4. package/dist/{chunk-OBMWSUBK.js → chunk-3Q7ZNY4B.js} +5 -5
  5. package/dist/{chunk-JVHP7Y4S.js → chunk-4FQ5ZICE.js} +2 -2
  6. package/dist/{chunk-AI3CLDZO.cjs → chunk-AM2RHT3H.cjs} +280 -280
  7. package/dist/{chunk-PUADVXJD.js → chunk-AU4WWLFP.js} +1 -1
  8. package/dist/{chunk-RU6BTELI.cjs → chunk-BKTV3VOD.cjs} +533 -488
  9. package/dist/{chunk-2VOZ37BL.cjs → chunk-BMOEUIFI.cjs} +35 -35
  10. package/dist/{chunk-IOTAUWWF.js → chunk-C6Y22J32.js} +1 -1
  11. package/dist/{chunk-ZSXZE4FJ.cjs → chunk-CGL3TJ2A.cjs} +3 -3
  12. package/dist/{chunk-MLFS5G3V.js → chunk-CIAAU44V.js} +307 -262
  13. package/dist/{chunk-4ZZ4TNCY.cjs → chunk-CNBGSWLN.cjs} +9 -9
  14. package/dist/{chunk-AX4F35C2.js → chunk-FIKAJKPE.js} +1 -1
  15. package/dist/{chunk-SEVKFMKR.cjs → chunk-FLCYWEEB.cjs} +3 -3
  16. package/dist/{chunk-ASHRHL4W.js → chunk-GKJFSE72.js} +5 -5
  17. package/dist/{chunk-ASIXPRHV.js → chunk-HVMH67Z7.js} +3 -3
  18. package/dist/{chunk-F77J34W2.js → chunk-J274GOBJ.js} +1 -1
  19. package/dist/{chunk-LYQYLPPV.cjs → chunk-MAGVU2HZ.cjs} +26 -26
  20. package/dist/{chunk-QV4GO3GW.cjs → chunk-MGVT5RT6.cjs} +29 -56
  21. package/dist/{chunk-W2DXHR2R.js → chunk-N27CSEDR.js} +1 -1
  22. package/dist/{chunk-YPEILGVZ.cjs → chunk-N2PHC6RX.cjs} +3 -3
  23. package/dist/{chunk-5FVWDDX6.js → chunk-NQRFGXDG.js} +2 -2
  24. package/dist/{chunk-772242QR.cjs → chunk-QJVG5V46.cjs} +65 -65
  25. package/dist/{chunk-LR6DJY4Z.cjs → chunk-RZFTEUV4.cjs} +9 -9
  26. package/dist/{chunk-SE4O4HCD.cjs → chunk-U6HOC7JD.cjs} +8 -8
  27. package/dist/{chunk-ZZRUKEN2.js → chunk-URDJXB75.js} +3 -3
  28. package/dist/{chunk-MGTZ2BKY.js → chunk-VTXIY33G.js} +22 -49
  29. package/dist/{chunk-2SO3OJFY.cjs → chunk-WWJOQGSH.cjs} +33 -33
  30. package/dist/{chunk-JAKG6BY7.js → chunk-WXL5G7WC.js} +3 -3
  31. package/dist/{cloud-tenants-US63E2HV.js → cloud-tenants-65EUPGVC.js} +2 -2
  32. package/dist/{cloud-tenants-SMULXU54.cjs → cloud-tenants-PT5QPWWU.cjs} +3 -3
  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/index.cjs +3 -3
  55. package/dist/components/features/index.js +2 -2
  56. package/dist/components/help-center-pages/index.cjs +71 -71
  57. package/dist/components/help-center-pages/index.js +13 -13
  58. package/dist/components/index.cjs +35 -37
  59. package/dist/components/index.js +14 -16
  60. package/dist/components/meeting-scheduler/index.cjs +19 -19
  61. package/dist/components/meeting-scheduler/index.js +2 -2
  62. package/dist/components/navigation/index.cjs +6 -6
  63. package/dist/components/navigation/index.js +5 -5
  64. package/dist/components/onboarding-guides/index.cjs +5 -5
  65. package/dist/components/onboarding-guides/index.js +4 -4
  66. package/dist/components/product-demo/index.cjs +6 -6
  67. package/dist/components/product-demo/index.js +6 -6
  68. package/dist/components/tickets/index.cjs +7 -7
  69. package/dist/components/tickets/index.js +6 -6
  70. package/dist/components/ui/index.cjs +3 -5
  71. package/dist/components/ui/index.js +4 -6
  72. package/dist/{devices-JWHGWKLK.js → devices-J253LQNQ.js} +2 -2
  73. package/dist/{devices-6ZURNFRB.cjs → devices-ZPVLATZA.cjs} +4 -4
  74. package/dist/index.cjs +7 -9
  75. package/dist/index.js +8 -10
  76. package/dist/{knowledge-CY66JZ54.cjs → knowledge-3FAF5T7Q.cjs} +4 -4
  77. package/dist/{knowledge-TEDPLCFX.js → knowledge-IMTMYURI.js} +2 -2
  78. package/dist/{logs-S46THPPD.js → logs-55GOSGVE.js} +2 -2
  79. package/dist/{logs-MO357RE7.cjs → logs-UL7BQEXP.cjs} +11 -11
  80. package/dist/{policies-BUXT7DKF.js → policies-KRDF563O.js} +2 -2
  81. package/dist/{policies-GSH4K5YP.cjs → policies-O5F2XSCY.cjs} +7 -7
  82. package/dist/{software-update-YKS6KLX2.js → software-update-NXSC4OVM.js} +2 -2
  83. package/dist/{software-update-2ETT35NM.cjs → software-update-R2ASCRWP.cjs} +6 -6
  84. package/dist/utils/index.cjs +17 -18
  85. package/dist/utils/index.js +17 -18
  86. package/dist/utils/source-grouping.d.ts +15 -5
  87. package/dist/utils/source-grouping.d.ts.map +1 -1
  88. package/package.json +1 -1
  89. package/src/components/chat/assistant-ask-prompts.tsx +84 -39
  90. package/src/components/chat/entity-cards/dispatch.tsx +47 -58
  91. package/src/components/chat/hooks/index.ts +3 -2
  92. package/src/components/chat/hooks/use-ask-prompts.ts +140 -56
  93. package/src/components/faq/faq-ask-card.tsx +22 -67
  94. package/src/utils/source-grouping.ts +24 -14
  95. package/src/utils/source-icons.ts +16 -16
@@ -1,74 +1,119 @@
1
1
  'use client';
2
2
 
3
- import { useMemo } from 'react';
4
3
  import {
5
4
  AssistantRuntimeContext,
6
5
  useAssistantRuntime,
7
6
  type AssistantOpenRequest,
8
7
  } from '../../contexts/assistant-runtime-context';
9
- import { AskPrompts, type AskPromptsProps } from './ask-prompts';
10
- import {
11
- assistantAvailable,
12
- ASK_PROMPTS_DEFAULT_COUNT,
13
- useAskPrompts,
14
- useAssistantOpen,
15
- useShownAskPrompts,
16
- } from './hooks/use-ask-prompts';
8
+ import { AskCard, AskPrompts, type AskPromptsProps } from './ask-prompts';
9
+ import { assistantAvailable, ASK_PROMPTS_DEFAULT_COUNT, useAskSurface } from './hooks/use-ask-prompts';
10
+
11
+ /** The lib's own wording of the card, when neither the host nor the runtime states it. */
12
+ const DEFAULT_ASK_CARD = {
13
+ title: 'Still deciding?',
14
+ description: '{assistant} answers from our docs and customer stories.',
15
+ } as const;
16
+
17
+ const fillAssistant = (text: string, name: string) => text.split('{assistant}').join(name);
17
18
 
18
19
  export interface AssistantAskPromptsProps extends Pick<AskPromptsProps, 'align' | 'fadeColor' | 'className'> {
19
- /** What this row is about: any string the host's questions endpoint knows. Reported with every click. */
20
+ /**
21
+ * How the surface looks. `row` (default): the launcher, then the questions,
22
+ * for a page section. `card`: the "still deciding?" card beside a list of
23
+ * questions (a FAQ), with a title, a line of description and the launcher.
24
+ */
25
+ variant?: 'row' | 'card';
26
+ /** What this surface is about: any string the host's questions endpoint knows. Reported with every click. */
20
27
  topic?: string;
21
- /** How many questions the row offers: the same number of slots on every load. */
28
+ /** How many questions it offers: the same number of slots on every load. Absent: three (a card: the runtime's, else three). */
22
29
  count?: number;
30
+ /**
31
+ * Ids of questions never to offer. Absent: the ones the page's other "ask"
32
+ * surfaces show, which this one waits for. `null`: wait.
33
+ */
34
+ exclude?: readonly string[] | null;
35
+ /** False: the surface does not pick yet (its host is waiting for it to be reached). Default true. */
36
+ enabled?: boolean;
23
37
  /** The launcher's name. Absent: the assistant's configured name, else the launcher's default. */
24
38
  label?: string;
25
- /** Something else the row waits for (the host is still reading the name it will show). */
39
+ /** Something else the surface waits for (the host is still reading the name it will show). */
26
40
  loading?: boolean;
27
- /** How the chat is opened from THIS row. Absent: the assistant runtime's opener. */
41
+ /** A card's wording. `{assistant}` is replaced by the assistant's name. Absent: the runtime's, else the lib's. */
42
+ title?: string;
43
+ description?: string;
44
+ /** How the chat is opened from THIS surface. Absent: the assistant runtime's opener. */
28
45
  onOpen?: (request: AssistantOpenRequest) => void;
29
46
  }
30
47
 
31
48
  /**
32
- * A row of questions for the host's assistant, wired by itself: the launcher
33
- * with the assistant's name and glyph, then the questions the host picked for
34
- * this row's `topic`, each opening the chat and asking it. Everything comes
35
- * from the nearest assistant runtime (`AssistantRuntimeContext`): whether a
36
- * chat is there, the identity, the questions endpoint and the opener. While
37
- * the pick is on its way the row is the SAME block (a chip skeleton in every
38
- * slot), so nothing moves when the questions land. The row tells the page
39
- * which questions it shows, so the FAQ's card never repeats one. Nothing
40
- * renders with no chat, or once the pick has settled empty.
49
+ * THE "ask the assistant" surface, wired by itself: a row of questions in a
50
+ * page section, or the card beside a FAQ (`variant`). Everything comes from
51
+ * the nearest assistant runtime (`AssistantRuntimeContext`) through ONE hook,
52
+ * `useAskSurface`: whether a chat is there, the identity, the questions the
53
+ * host picked for this surface's `topic`, the opener and the click report.
54
+ * Surfaces of one page pick one after another and never show a question
55
+ * another already shows, so two of them may share a topic. While a pick is on
56
+ * its way the surface is the SAME block (a chip skeleton in every slot), so
57
+ * nothing moves when the questions land.
58
+ *
59
+ * Nothing renders with no chat. A row also renders nothing with no questions
60
+ * endpoint, or once its pick has settled empty; a card needs the assistant's
61
+ * name for its words, and with no endpoint is the launcher alone.
41
62
  */
42
63
  export function AssistantAskPrompts({
64
+ variant = 'row',
43
65
  topic,
44
- count = ASK_PROMPTS_DEFAULT_COUNT,
66
+ count,
67
+ exclude,
68
+ enabled,
45
69
  label,
46
70
  loading = false,
71
+ title,
72
+ description,
47
73
  onOpen,
48
74
  align,
49
75
  fadeColor,
50
76
  className,
51
77
  }: AssistantAskPromptsProps) {
78
+ const card = variant === 'card';
52
79
  const assistant = useAssistantRuntime();
53
- const { prompts, isLoading, supported } = useAskPrompts({ topic, count });
54
- const open = useAssistantOpen(topic, onOpen);
55
- const shown = assistantAvailable(assistant) && supported;
56
- // Nothing to say where the row shows nothing; `null` while its pick is on its way.
57
- useShownAskPrompts(shown ? (isLoading ? null : prompts.map(prompt => prompt.id)) : undefined);
58
- // The row's launcher opens the same chat as its questions, with the row's topic: it reads the runtime.
59
- const scoped = useMemo(() => (assistant ? { ...assistant, open } : null), [assistant, open]);
60
- if (!assistantAvailable(assistant) || !supported) return null;
80
+ const slots = count ?? (card ? assistant?.askCard?.count : undefined) ?? ASK_PROMPTS_DEFAULT_COUNT;
81
+ const ask = useAskSurface({ topic, count: slots, exclude, enabled, onOpen });
82
+ if (!assistantAvailable(assistant)) return null;
83
+
84
+ const shared = {
85
+ prompts: ask.prompts,
86
+ loading: ask.isLoading || loading,
87
+ source: assistant.source,
88
+ icon: assistant.icon,
89
+ onOpen: ask.open,
90
+ onAsk: ask.reportAsk,
91
+ };
92
+ if (card) {
93
+ const name = label ?? assistant.name;
94
+ if (!name) return null;
95
+ const words = description ?? assistant.askCard?.description ?? DEFAULT_ASK_CARD.description;
96
+ return (
97
+ <AssistantRuntimeContext.Provider value={ask.scoped}>
98
+ <AskCard
99
+ {...shared}
100
+ title={fillAssistant(title ?? assistant.askCard?.title ?? DEFAULT_ASK_CARD.title, name)}
101
+ description={words ? fillAssistant(words, name) : undefined}
102
+ // Fixed slots, one question per row: chip skeletons until the pick lands, the same box after.
103
+ count={ask.supported ? slots : 0}
104
+ label={name}
105
+ className={className}
106
+ />
107
+ </AssistantRuntimeContext.Provider>
108
+ );
109
+ }
110
+ if (!ask.supported) return null;
61
111
  return (
62
- <AssistantRuntimeContext.Provider value={scoped}>
112
+ <AssistantRuntimeContext.Provider value={ask.scoped}>
63
113
  <AskPrompts
64
- prompts={prompts}
65
- loading={isLoading || loading}
66
- count={count}
67
- source={assistant.source}
114
+ {...shared}
115
+ count={slots}
68
116
  label={label ?? assistant.name ?? undefined}
69
- icon={assistant.icon}
70
- onOpen={open}
71
- onAsk={prompt => assistant.onAsk?.({ promptId: prompt.id, topic })}
72
117
  align={align}
73
118
  fadeColor={fadeColor}
74
119
  className={className}
@@ -61,29 +61,14 @@ import { MingoIcon } from '../../icons';
61
61
  import { ArrowRightUpIcon } from '../../icons-v2-generated/arrows/arrow-right-up-icon';
62
62
  import { ClickupLogoIcon } from '../../icons-v2-generated/brand-logos/clickup-logo-icon';
63
63
  import { SlackLogoGreyIcon } from '../../icons-v2-generated/brand-logos/slack-logo-grey-icon';
64
- import { ChartBar01VerIcon } from '../../icons-v2-generated/charts/chart-bar-01-ver-icon';
65
- import { ChartPieIcon } from '../../icons-v2-generated/charts/chart-pie-icon';
66
64
  import { PresentationBarIcon } from '../../icons-v2-generated/charts/presentation-bar-icon';
67
65
  import { PresentationLineIcon } from '../../icons-v2-generated/charts/presentation-line-icon';
68
- import { BracketCurlyCheckIcon } from '../../icons-v2-generated/coding/bracket-curly-check-icon';
69
66
  import { CodeIcon } from '../../icons-v2-generated/coding/code-icon';
70
- import { CodeSquareIcon } from '../../icons-v2-generated/coding/code-square-icon';
71
- import { CodingBranchIcon } from '../../icons-v2-generated/coding/coding-branch-icon';
72
67
  import { CodingCommitIcon } from '../../icons-v2-generated/coding/coding-commit-icon';
73
- import { CodingMergeIcon } from '../../icons-v2-generated/coding/coding-merge-icon';
74
68
  import { CodingPullRequestIcon } from '../../icons-v2-generated/coding/coding-pull-request-icon';
75
- import { PackageIcon } from '../../icons-v2-generated/coding/package-icon';
76
- import { CallIcon } from '../../icons-v2-generated/communication/call-icon';
77
- import { ChatQuoteIcon } from '../../icons-v2-generated/communication/chat-quote-icon';
78
69
  import { CalendarIcon } from '../../icons-v2-generated/date-and-time/calendar-icon';
79
- import { ClipboardListIcon } from '../../icons-v2-generated/documents/clipboard-list-icon';
80
- import { Copy01Icon } from '../../icons-v2-generated/documents/copy-01-icon';
81
- import { FileCodeIcon } from '../../icons-v2-generated/documents/file-code-icon';
82
70
  import { FileContentIcon } from '../../icons-v2-generated/documents/file-content-icon';
83
71
  import { NewspaperIcon } from '../../icons-v2-generated/documents/newspaper-icon';
84
- import { BankIcon } from '../../icons-v2-generated/finance/bank-icon';
85
- import { CoinsExchangeCurrencyIcon } from '../../icons-v2-generated/finance/coins-exchange-currency-icon';
86
- import { MoneyBillDollarIcon } from '../../icons-v2-generated/finance/money-bill-dollar-icon';
87
72
  import { MicrophoneIcon } from '../../icons-v2-generated/household/microphone-icon';
88
73
  import { AlertTriangleIcon } from '../../icons-v2-generated/interface/alert-triangle-icon';
89
74
  import { EyeIcon } from '../../icons-v2-generated/interface/eye-icon';
@@ -113,10 +98,9 @@ import { resolveCardDestination } from '../utils/card-destination';
113
98
  import { resolveHrefForRuntime } from '../utils/chat-nav-resolution';
114
99
  import { executeNavigation } from '../utils/execute-navigation';
115
100
  import { clickupTaskUrl } from '../utils/external-app-urls';
116
- import { resolveIcon } from '../utils/icon-library';
117
101
  import { computeIsNewTab, buildAnchorProps } from '../utils/nav-anchor-props';
118
102
  import { readFetchedCardTitle } from '../utils/resolve-fetched-card-href';
119
- import { getSourceLabel, SOURCE_ICON_NAMES } from '../utils/source-icons';
103
+ import { defaultTableIdForDocumentType, getSourceLabel, SOURCE_ICON_NAMES } from '../utils/source-icons';
120
104
  import { resolveSourceIcon, sourceRowCtxFromRuntime } from '../utils/source-row-cta';
121
105
  import { BlockCard } from './block-card';
122
106
  import { BlogCardSkeleton } from './blog-card';
@@ -1393,18 +1377,16 @@ function refHydratedEntry(
1393
1377
  };
1394
1378
  }
1395
1379
 
1380
+ /** The leading glyph carries the type signal (the badge pill is dropped); it is the SOURCE's icon (`sourceGlyph`). */
1396
1381
  interface FinancialCardConfig {
1397
1382
  label: string;
1398
- /** Per-type leading glyph — carries the type signal now that the
1399
- * badge pill is dropped. */
1400
- icon: () => React.ReactNode;
1401
1383
  }
1402
1384
  const FINANCIAL_CARD_CONFIGS: Record<string, FinancialCardConfig> = {
1403
- financial_kpi: { label: 'Financial KPI', icon: () => <ChartBar01VerIcon size={24} /> },
1404
- cap_table: { label: 'Cap table entry', icon: () => <ChartPieIcon size={24} /> },
1405
- profit_loss: { label: 'P&L period', icon: () => <MoneyBillDollarIcon size={24} /> },
1406
- balance_sheet: { label: 'Balance sheet', icon: () => <BankIcon size={24} /> },
1407
- cash_flow: { label: 'Cash flow', icon: () => <CoinsExchangeCurrencyIcon size={24} /> },
1385
+ financial_kpi: { label: 'Financial KPI' },
1386
+ cap_table: { label: 'Cap table entry' },
1387
+ profit_loss: { label: 'P&L period' },
1388
+ balance_sheet: { label: 'Balance sheet' },
1389
+ cash_flow: { label: 'Cash flow' },
1408
1390
  };
1409
1391
  /** Expand a per-family config map into registry entries — ONE loop shell
1410
1392
  * for every card family (financial, github, program, roadmap); adding a
@@ -1423,7 +1405,7 @@ function financialRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1423
1405
  refHydratedEntry(docType, cfg.label, (displayRef, opts) => (
1424
1406
  <GenericFinancialChatCard
1425
1407
  chatRef={displayRef}
1426
- icon={cfg.icon()}
1408
+ icon={sourceGlyph(docType)}
1427
1409
  isNewTab={opts.isNewTab}
1428
1410
  discuss={opts.discuss}
1429
1411
  />
@@ -1463,11 +1445,13 @@ const GITHUB_CARD_CONFIGS: Record<string, GitHubCardConfig> = {
1463
1445
  interface GlyphCardConfig {
1464
1446
  label: string;
1465
1447
  /**
1466
- * The card's leading glyph. It is handed the hydrated ref, so a type whose
1467
- * records carry their own mark (a vendor's logo, a page's menu icon) draws
1468
- * that and keeps a generic glyph for a record without one.
1448
+ * ONLY for a type whose records carry their own mark (a vendor's logo, a
1449
+ * page's menu icon): it is handed the hydrated ref and draws that mark.
1450
+ * Every other type leaves it out and gets its SOURCE's icon
1451
+ * (`sourceGlyph`): the one `SOURCE_ICON_NAMES` entry its chip draws too, so
1452
+ * a card and its chip can never show different icons.
1469
1453
  */
1470
- icon: (ref?: ChatRef) => React.ReactNode;
1454
+ icon?: (ref?: ChatRef) => React.ReactNode;
1471
1455
  /**
1472
1456
  * Video-bearing ref types: render the media card (cover from the ref's
1473
1457
  * `metadata.videoPoster`, else the host's OG placeholder; the type label as the
@@ -1481,41 +1465,51 @@ interface GlyphCardConfig {
1481
1465
  * (`/api/design-docs`, `/api/openframe-tenants`, `/api/prospect-calls` — ChatRef-shaped items,
1482
1466
  * same preset as github / slack). Adding one = one line here + the
1483
1467
  * `list-url.ts` builder + the `source-icons.ts` label / icon / type entries. */
1484
- /** The OpenFrame logo every OpenFrame surface uses (the `openframe` icon name). */
1485
- const OpenFrameGlyph = resolveIcon('openframe');
1486
1468
  const REF_GLYPH_CARD_CONFIGS: Record<string, GlyphCardConfig> = {
1487
- openframe_tenant: { label: 'OpenFrame tenant', icon: () => <OpenFrameGlyph size={24} /> },
1488
- prospect_call: { label: 'Prospect call', icon: () => <CallIcon size={24} />, media: true },
1469
+ openframe_tenant: { label: 'OpenFrame tenant' },
1470
+ prospect_call: { label: 'Prospect call', media: true },
1489
1471
  // Code intelligence (product-hub internal): the review rules a repository is
1490
1472
  // held to, the repositories themselves, and one row per deployment
1491
1473
  // environment. Glyph cards like their siblings — none of the three has a
1492
1474
  // cover image, and a rule is read in the admin screen it links to.
1493
- code_rule: { label: 'Code rule', icon: () => <BracketCurlyCheckIcon size={24} /> },
1494
- code_repo: { label: 'Repository', icon: () => <CodingBranchIcon size={24} /> },
1495
- code_deployment: { label: 'Deployment', icon: () => <PackageIcon size={24} /> },
1475
+ code_rule: { label: 'Code rule' },
1476
+ code_repo: { label: 'Repository' },
1477
+ code_deployment: { label: 'Deployment' },
1496
1478
  // The graph's own objects: one source file's public surface, one public
1497
1479
  // symbol, a near-duplicate cluster, and a pull request's blast radius.
1498
1480
  // Glyph cards for the same reason as their three siblings — none carries a
1499
1481
  // cover image, and each links to the admin screen that renders it in full.
1500
- code_file: { label: 'Code file', icon: () => <FileCodeIcon size={24} /> },
1501
- code_symbol: { label: 'Code symbol', icon: () => <CodeSquareIcon size={24} /> },
1502
- code_duplicate: { label: 'Duplicate code', icon: () => <Copy01Icon size={24} /> },
1503
- code_impact: { label: 'Change impact', icon: () => <CodingPullRequestIcon size={24} /> },
1482
+ code_file: { label: 'Code file' },
1483
+ code_symbol: { label: 'Code symbol' },
1484
+ code_duplicate: { label: 'Duplicate code' },
1485
+ code_impact: { label: 'Change impact' },
1504
1486
  // A change set: pull requests across repositories declared as one change, with the ClickUp tasks and design
1505
1487
  // docs its pull requests are attached to. ONE card for it wherever a set is shown (chat, a design doc's page).
1506
- change_set: { label: 'Change set', icon: () => <CodingMergeIcon size={24} /> },
1488
+ change_set: { label: 'Change set' },
1507
1489
  // A page of the public website (pricing, product, legal, and every page the navigation lists), read live
1508
1490
  // from the data the page shows. A glyph card: a page has no cover of its own, and the card opens the page.
1509
1491
  site_page: { label: 'Website page', icon: ref => <SitePageGlyph iconName={refText(ref, 'icon_name')} /> },
1510
1492
  // What OpenFrame charges, read live from its billing: the plan in force, or one AI model's token exchange
1511
1493
  // rate. A glyph card; it opens the pricing page.
1512
- openframe_price: { label: 'OpenFrame price', icon: () => <OpenFrameGlyph size={24} /> },
1494
+ openframe_price: { label: 'OpenFrame price' },
1513
1495
  // A vendor (product) of the OpenMSP directory. A glyph card; it opens the vendor's page there.
1514
1496
  vendor: { label: 'Vendor', icon: ref => <VendorGlyph title={ref?.title ?? ''} logoUrl={refText(ref, 'logo_url')} /> },
1515
1497
  };
1498
+ /**
1499
+ * A card type's icon: its SOURCE's, resolved exactly as the source's chip
1500
+ * resolves it (`resolveSourceIcon` over `SOURCE_ICON_NAMES`). One name per
1501
+ * source, one resolver, so a card and its chip always match.
1502
+ */
1503
+ function sourceGlyph(docType: string): React.ReactNode {
1504
+ const { Icon } = resolveSourceIcon({ sourceRepo: defaultTableIdForDocumentType(docType), documentType: docType });
1505
+ return <Icon className="size-6" />;
1506
+ }
1507
+
1516
1508
  function refGlyphRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1517
- return registryEntries(REF_GLYPH_CARD_CONFIGS, (cfg, docType) =>
1518
- refHydratedEntry(docType, cfg.label, (displayRef, opts) =>
1509
+ return registryEntries(REF_GLYPH_CARD_CONFIGS, (config, docType) => {
1510
+ const glyph = config.icon ?? (() => sourceGlyph(docType));
1511
+ const cfg = config;
1512
+ return refHydratedEntry(docType, cfg.label, (displayRef, opts) =>
1519
1513
  cfg.media ? (
1520
1514
  <EntityMingoCard
1521
1515
  title={displayRef.title}
@@ -1524,7 +1518,7 @@ function refGlyphRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1524
1518
  displayRef.metadata?.videoPoster,
1525
1519
  opts.extras?.buildOgPlaceholderUrl?.(displayRef.title ?? '') ?? null,
1526
1520
  )}
1527
- fallbackIcon={cfg.icon(displayRef)}
1521
+ fallbackIcon={glyph(displayRef)}
1528
1522
  status={{ label: cfg.label, variant: 'grey' }}
1529
1523
  chatRef={displayRef}
1530
1524
  isNewTab={opts.isNewTab}
@@ -1532,15 +1526,10 @@ function refGlyphRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1532
1526
  menuAriaLabel={`${cfg.label} actions`}
1533
1527
  />
1534
1528
  ) : (
1535
- <GlyphChatCard
1536
- chatRef={displayRef}
1537
- icon={cfg.icon(displayRef)}
1538
- isNewTab={opts.isNewTab}
1539
- discuss={opts.discuss}
1540
- />
1529
+ <GlyphChatCard chatRef={displayRef} icon={glyph(displayRef)} isNewTab={opts.isNewTab} discuss={opts.discuss} />
1541
1530
  ),
1542
- ),
1543
- );
1531
+ );
1532
+ });
1544
1533
  }
1545
1534
 
1546
1535
  /** People-hub employee feeds hydrate from their EXISTING list APIs
@@ -1558,9 +1547,9 @@ function fetchedEmployeeEntryDisplayRef(item: unknown, chatRef: ChatRef): ChatRe
1558
1547
  return { ...chatRef, title, preview: preview.length > 0 ? preview : chatRef.preview };
1559
1548
  }
1560
1549
  const EMPLOYEE_ENTRY_CARD_CONFIGS: Record<string, GlyphCardConfig> = {
1561
- what_i_shipped: { label: 'What I Shipped', icon: () => <Rocket02Icon size={24} /> },
1562
- how_i_work: { label: 'How I Work', icon: () => <ClipboardListIcon size={24} /> },
1563
- ai_prompt: { label: 'Squawkbox', icon: () => <ChatQuoteIcon size={24} /> },
1550
+ what_i_shipped: { label: 'What I Shipped' },
1551
+ how_i_work: { label: 'How I Work' },
1552
+ ai_prompt: { label: 'Squawkbox' },
1564
1553
  };
1565
1554
  function employeeEntryRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1566
1555
  return registryEntries(EMPLOYEE_ENTRY_CARD_CONFIGS, (cfg, docType) => ({
@@ -1571,7 +1560,7 @@ function employeeEntryRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1571
1560
  render: (item, chatRef, opts) => (
1572
1561
  <GlyphChatCard
1573
1562
  chatRef={fetchedEmployeeEntryDisplayRef(item, chatRef)}
1574
- icon={cfg.icon()}
1563
+ icon={sourceGlyph(docType)}
1575
1564
  isNewTab={opts.isNewTab}
1576
1565
  discuss={opts.discuss}
1577
1566
  />
@@ -45,9 +45,10 @@ export {
45
45
  buildAskPromptsUrl,
46
46
  useAskPrompts,
47
47
  useAssistantOpen,
48
- useShownAskPromptIds,
49
- useShownAskPrompts,
48
+ useAskSurface,
50
49
  type AskPromptsResponse,
51
50
  type UseAskPromptsOptions,
52
51
  type UseAskPromptsResult,
52
+ type UseAskSurfaceOptions,
53
+ type UseAskSurfaceResult,
53
54
  } from './use-ask-prompts';
@@ -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
  }