@flamingo-stack/openframe-frontend-core 0.0.243-snapshot.20260610094541 → 0.0.243

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 (131) hide show
  1. package/dist/{chunk-DV3YZWGN.cjs → chunk-3TDLFCCS.cjs} +33 -20
  2. package/dist/chunk-3TDLFCCS.cjs.map +1 -0
  3. package/dist/{chunk-ORJREQ2W.js → chunk-4XLJWX2N.js} +4 -4
  4. package/dist/{chunk-P5C732SC.js → chunk-7ZHFCEJH.js} +6 -6
  5. package/dist/{chunk-EJ7J4QAD.cjs → chunk-BKPS6M2F.cjs} +12 -12
  6. package/dist/{chunk-EJ7J4QAD.cjs.map → chunk-BKPS6M2F.cjs.map} +1 -1
  7. package/dist/{chunk-EPXROXYC.cjs → chunk-CZITXP77.cjs} +645 -713
  8. package/dist/chunk-CZITXP77.cjs.map +1 -0
  9. package/dist/{chunk-Z6GGYZDR.cjs → chunk-DUZBTUXF.cjs} +27 -27
  10. package/dist/{chunk-Z6GGYZDR.cjs.map → chunk-DUZBTUXF.cjs.map} +1 -1
  11. package/dist/chunk-F3GMWNAT.js +285 -0
  12. package/dist/chunk-F3GMWNAT.js.map +1 -0
  13. package/dist/{chunk-M7DPUWAL.js → chunk-FRRMXLU5.js} +120 -188
  14. package/dist/chunk-FRRMXLU5.js.map +1 -0
  15. package/dist/chunk-HU252PYJ.cjs +285 -0
  16. package/dist/chunk-HU252PYJ.cjs.map +1 -0
  17. package/dist/{chunk-PTSAF7T2.js → chunk-JJV3XGCY.js} +2 -2
  18. package/dist/{chunk-APGMNHPY.js → chunk-MKVKVJQQ.js} +32 -19
  19. package/dist/chunk-MKVKVJQQ.js.map +1 -0
  20. package/dist/{chunk-STKMIMLP.cjs → chunk-MP5HOP7N.cjs} +12 -12
  21. package/dist/{chunk-STKMIMLP.cjs.map → chunk-MP5HOP7N.cjs.map} +1 -1
  22. package/dist/{chunk-NKGNRFSS.js → chunk-SEVJT2HP.js} +8 -8
  23. package/dist/{chunk-FSS2MIEA.cjs → chunk-SGJBUL2Y.cjs} +20 -3
  24. package/dist/chunk-SGJBUL2Y.cjs.map +1 -0
  25. package/dist/{chunk-WD2MZVJI.js → chunk-V35PZSUA.js} +5 -5
  26. package/dist/{chunk-LJGBFD52.js → chunk-VAUI3CND.js} +18 -1
  27. package/dist/chunk-VAUI3CND.js.map +1 -0
  28. package/dist/{chunk-OB45JHDY.cjs → chunk-VFKQMAUF.cjs} +3 -3
  29. package/dist/{chunk-OB45JHDY.cjs.map → chunk-VFKQMAUF.cjs.map} +1 -1
  30. package/dist/{chunk-RJC3L5DT.cjs → chunk-VXP553WT.cjs} +7 -7
  31. package/dist/{chunk-RJC3L5DT.cjs.map → chunk-VXP553WT.cjs.map} +1 -1
  32. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  33. package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
  34. package/dist/components/chat/index.cjs +6 -16
  35. package/dist/components/chat/index.cjs.map +1 -1
  36. package/dist/components/chat/index.js +5 -15
  37. package/dist/components/chat/types/entities/content-ref.d.ts +5 -20
  38. package/dist/components/chat/types/entities/content-ref.d.ts.map +1 -1
  39. package/dist/components/chat/utils/index.d.ts +0 -1
  40. package/dist/components/chat/utils/index.d.ts.map +1 -1
  41. package/dist/components/contact/index.cjs +7 -7
  42. package/dist/components/contact/index.js +6 -6
  43. package/dist/components/faq/faq-section.d.ts +7 -1
  44. package/dist/components/faq/faq-section.d.ts.map +1 -1
  45. package/dist/components/faq/index.cjs +3 -3
  46. package/dist/components/faq/index.js +2 -2
  47. package/dist/components/features/index.cjs +6 -6
  48. package/dist/components/features/index.js +5 -5
  49. package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
  50. package/dist/components/index.cjs +87 -93
  51. package/dist/components/index.cjs.map +1 -1
  52. package/dist/components/index.d.ts +1 -0
  53. package/dist/components/index.d.ts.map +1 -1
  54. package/dist/components/index.js +31 -37
  55. package/dist/components/index.js.map +1 -1
  56. package/dist/components/navigation/index.cjs +6 -6
  57. package/dist/components/navigation/index.js +5 -5
  58. package/dist/components/onboarding-guides/index.cjs +30 -30
  59. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  60. package/dist/components/onboarding-guides/index.js +11 -11
  61. package/dist/components/related-content/index.cjs +26 -0
  62. package/dist/components/related-content/index.cjs.map +1 -0
  63. package/dist/components/related-content/index.d.ts +2 -0
  64. package/dist/components/related-content/index.d.ts.map +1 -0
  65. package/dist/components/related-content/index.js +26 -0
  66. package/dist/components/related-content/index.js.map +1 -0
  67. package/dist/components/related-content/related-content-section.d.ts +141 -0
  68. package/dist/components/related-content/related-content-section.d.ts.map +1 -0
  69. package/dist/components/tickets/index.cjs +73 -73
  70. package/dist/components/tickets/index.cjs.map +1 -1
  71. package/dist/components/tickets/index.js +15 -15
  72. package/dist/components/ui/index.cjs +9 -19
  73. package/dist/components/ui/index.cjs.map +1 -1
  74. package/dist/components/ui/index.js +13 -23
  75. package/dist/embed-shims/index.cjs +3 -3
  76. package/dist/embed-shims/index.cjs.map +1 -1
  77. package/dist/embed-shims/index.js +5 -5
  78. package/dist/index.cjs +17 -11
  79. package/dist/index.cjs.map +1 -1
  80. package/dist/index.js +55 -49
  81. package/dist/types/content-ref.d.ts +35 -0
  82. package/dist/types/content-ref.d.ts.map +1 -0
  83. package/dist/types/index.d.ts +1 -0
  84. package/dist/types/index.d.ts.map +1 -1
  85. package/dist/utils/content-ref-groups.d.ts +58 -0
  86. package/dist/utils/content-ref-groups.d.ts.map +1 -0
  87. package/dist/utils/extract-items.d.ts +18 -0
  88. package/dist/utils/extract-items.d.ts.map +1 -0
  89. package/dist/utils/index.cjs +81 -0
  90. package/dist/utils/index.cjs.map +1 -1
  91. package/dist/utils/index.d.ts +4 -1
  92. package/dist/utils/index.d.ts.map +1 -1
  93. package/dist/utils/index.js +74 -1
  94. package/dist/utils/index.js.map +1 -1
  95. package/dist/utils/list-url.d.ts +6 -0
  96. package/dist/utils/list-url.d.ts.map +1 -1
  97. package/dist/utils/suggestion-url.d.ts +26 -0
  98. package/dist/utils/suggestion-url.d.ts.map +1 -0
  99. package/package.json +7 -1
  100. package/src/components/chat/chat-message-list.tsx +49 -74
  101. package/src/components/chat/hooks/use-chat-card-item.ts +3 -38
  102. package/src/components/chat/types/entities/content-ref.ts +5 -21
  103. package/src/components/chat/utils/index.ts +0 -12
  104. package/src/components/faq/faq-section.tsx +69 -24
  105. package/src/components/features/policy-configuration-panel.tsx +0 -1
  106. package/src/components/index.ts +1 -0
  107. package/src/components/related-content/index.ts +9 -0
  108. package/src/components/related-content/related-content-section.tsx +612 -0
  109. package/src/types/content-ref.ts +34 -0
  110. package/src/types/index.ts +1 -0
  111. package/src/utils/content-ref-groups.ts +89 -0
  112. package/src/utils/extract-items.ts +48 -0
  113. package/src/utils/index.ts +16 -1
  114. package/src/utils/list-url.ts +9 -0
  115. package/src/utils/suggestion-url.ts +39 -0
  116. package/dist/chunk-APGMNHPY.js.map +0 -1
  117. package/dist/chunk-DV3YZWGN.cjs.map +0 -1
  118. package/dist/chunk-EPXROXYC.cjs.map +0 -1
  119. package/dist/chunk-FSS2MIEA.cjs.map +0 -1
  120. package/dist/chunk-LJGBFD52.js.map +0 -1
  121. package/dist/chunk-M7DPUWAL.js.map +0 -1
  122. package/dist/components/chat/utils/history-merge.d.ts +0 -92
  123. package/dist/components/chat/utils/history-merge.d.ts.map +0 -1
  124. package/src/components/chat/__tests__/chat-message-list.test.tsx +0 -92
  125. package/src/components/chat/utils/__tests__/history-merge.test.ts +0 -339
  126. package/src/components/chat/utils/history-merge.ts +0 -255
  127. /package/dist/{chunk-ORJREQ2W.js.map → chunk-4XLJWX2N.js.map} +0 -0
  128. /package/dist/{chunk-P5C732SC.js.map → chunk-7ZHFCEJH.js.map} +0 -0
  129. /package/dist/{chunk-PTSAF7T2.js.map → chunk-JJV3XGCY.js.map} +0 -0
  130. /package/dist/{chunk-NKGNRFSS.js.map → chunk-SEVJT2HP.js.map} +0 -0
  131. /package/dist/{chunk-WD2MZVJI.js.map → chunk-V35PZSUA.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/related-content/related-content-section.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * RelatedContentSection\n *\n * Renders content references grouped by type using the canonical card\n * components. MOVED from the hub (`components/shared/related-content-card.tsx`)\n * so any consuming app can embed it; the hub keeps a thin wrapper that\n * pre-binds its host-specific injections (nav hook, URL recomposition,\n * program configs, admin campaign card).\n *\n * THREE data modes (precedence top-down):\n * 1. CONTROLLED — `contentRefs` provided (even `[]`): render exactly those\n * refs, no suggestion fetch (the original investor-update behavior).\n * 2. SUGGESTION — `entityType` + `entityId` provided: self-fetch\n * `GET {apiBaseUrl}/api/related-content?entityType&entityId[&count][&excludeTypes]`\n * (the generic 5-tier engine's second web service). `minResults` maps to\n * `count`; absent → param not sent (server default applies). Each ref\n * carries a `reason` (data-only — never rendered, matching the\n * FaqSection/FaqWithReason precedent).\n * 3. SSR-HYDRATED suggestion — also pass `initialItems` (the server page\n * called the engine directly); the first client fetch is skipped per the\n * `useSelfFetch` initialData contract.\n *\n * Group layout (list vs grid) + card size (lg vs default) come from\n * `CONTENT_REF_GROUPS` in `../../utils/content-ref-groups` — single source of\n * truth, no per-type logic in this file. Skeletons come from\n * `renderSkeletonForType` so the placeholder height matches the loaded card\n * exactly (zero layout shift on resolve).\n *\n * One API call per content type via the shared list-URL builder\n * (`buildListUrl` — injectable; defaults to the lib's byte-parity-tested\n * builder prefixed with `apiBaseUrl`). Fetching uses `useSelfFetch` (plain\n * fetch, NO react-query) so third-party embedders need no QueryClientProvider;\n * cards are imported via DEEP module paths (not the chat barrel) so this\n * chunk never reaches `@tanstack/react-query`.\n *\n * LOCKSTEP NOTE: this file's per-type card/skeleton dispatch is the SIZED\n * sibling of the chat-side `CHAT_CARD_REGISTRY` (`../chat/entity-cards/\n * dispatch.tsx`), which renders compact `size='sm'` cards wired to the chat\n * runtime. Two dispatchers by design — when registering a new fetch-mode\n * content type, add it BOTH there and here (cards + skeleton + list URL).\n */\n\nimport React, { useMemo } from 'react';\nimport {\n CONTENT_REF_GROUPS,\n getContentRefLabelOrTitleCase,\n orderContentRefTypes,\n type ContentRefGroupConfig,\n} from '../../utils/content-ref-groups';\nimport type { ContentRef, ContentRefWithReason } from '../../types/content-ref';\nimport { useSelfFetch } from '../../hooks/use-self-fetch';\nimport { extractItems } from '../../utils/extract-items';\nimport { buildListUrl as libBuildListUrl, canonicalContentRefType } from '../../utils/list-url';\nimport { buildSuggestionUrl } from '../../utils/suggestion-url';\nimport { decideNewTab } from '../chat/utils/decide-new-tab';\n// DEEP card imports — NOT the `../chat` barrel (the barrel statically reaches\n// @tanstack/react-query via embeddable-chat + its hooks). Deep paths keep this\n// component's SOURCE graph react-query-free. Note: tsup's shared-chunk\n// splitting may still colocate the cards with chat hooks in one dist chunk\n// (react-query is a required peerDep, so resolution always succeeds) — the\n// guarantee that matters here is the RUNTIME one: nothing on this path ever\n// instantiates a QueryClient, so embedders need NO QueryClientProvider.\nimport { BlogCard, BlogCardSkeleton } from '../chat/entity-cards/blog-card';\nimport { CaseStudyCard, CaseStudyCardSkeleton } from '../chat/entity-cards/case-study-card';\nimport { CustomerInterviewCard, CustomerInterviewCardSkeleton } from '../chat/entity-cards/customer-interview-card';\nimport { ProductReleaseCard, ProductReleaseCardSkeleton } from '../chat/entity-cards/product-release-card';\nimport { buildProductReleaseCardProps } from '../chat/entity-cards/product-release-card-defaults';\nimport { ProgramCard, ProgramCardSkeleton } from '../chat/entity-cards/program-card';\nimport { InvestorUpdateCard, InvestorUpdateCardSkeleton } from '../chat/entity-cards/investor-update-card';\nimport { OnboardingGuideCard, OnboardingGuideCardSkeleton } from '../chat/entity-cards/onboarding-guide-card';\nimport { RoadmapCard, RoadmapCardSkeleton } from '../chat/entity-cards/roadmap-card';\n// Type-only — erased at build, no runtime dependency on the dispatch module.\nimport type { ChatCardDispatchExtras } from '../chat/entity-cards/dispatch';\n\ntype CardSize = 'lg' | 'default' | 'sm';\n\n/** Anchor prop bundle the per-card link surface receives — same shape the\n * hub's `useNavLink` returns and the chat dispatcher's anchor builders\n * produce. `null` = non-anchor mode (no URL). */\nexport interface CardLinkAnchorProps {\n href: string;\n target?: '_blank';\n rel?: 'noopener noreferrer';\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;\n}\n\n/** Render-prop component injection for the navigation decision — keeps hook\n * calls legal (hooks live INSIDE the injected component; `CardForType`\n * itself calls zero hooks). The hub injects a `useNavLink`-backed provider;\n * the default is hook-free (pure `decideNewTab`). MUST be defined at module\n * scope by hosts — an inline arrow would remount every card each render. */\nexport interface CardLinkProviderProps {\n href: string | null;\n targetPlatform: string | null;\n children: (linkProps: CardLinkAnchorProps | null) => React.ReactElement | null;\n}\nexport type CardLinkProvider = React.ComponentType<CardLinkProviderProps>;\n\n/** Default link provider for standalone embeds: relative/-same-origin hrefs\n * stay same-tab, cross-origin pops a new tab (pure `decideNewTab` with no\n * platform context — `currentSource: ''` falls through to the origin\n * check). No router integration, no hooks. */\nfunction DefaultLinkPropsProvider({ href, targetPlatform, children }: CardLinkProviderProps): React.ReactElement | null {\n if (!href) return children(null);\n const newTab = decideNewTab({ href, targetPlatform, currentSource: '' });\n return children(\n newTab\n ? { href, target: '_blank', rel: 'noopener noreferrer' }\n : { href },\n );\n}\n\n/** Default href resolution: trust the ref's stored url/targetPlatform as the\n * API composed them. The hub overrides this with its `buildContentURL`\n * re-composition so dev gets localhost and prod gets platform domains. */\nfunction defaultResolveHref(ref: ContentRef): { href: string | null; targetPlatform: string | null } {\n return { href: ref.url || null, targetPlatform: ref.targetPlatform ?? null };\n}\n\n/** Host-injected renderer pair for the admin-only `marketing_campaign` type.\n * Absent (every non-hub embed) → the type renders nothing (its list URL\n * hits `/api/admin`, unreachable outside the hub anyway). */\nexport interface AdminCampaignCardSlot {\n Card: React.ComponentType<{ campaign: any }>;\n Skeleton: React.ComponentType<{ size?: 'default' | 'sm' }>;\n}\n\n/**\n * Per-type skeleton dispatch — returns the SAME colocated skeleton the\n * resolved card renders, sized to match (zero layout shift on resolve).\n * The chat-side `CHAT_CARD_REGISTRY` already does this via\n * `entry.skeleton()`; this surface exposes the same discipline to the\n * related-content rail.\n */\nfunction renderSkeletonForType(\n type: string,\n size: CardSize,\n adminCampaignCard?: AdminCampaignCardSlot,\n): React.ReactNode {\n // Most card skeletons accept only `{default, sm}`. `'lg'` collapses to\n // `'default'`. ProductReleaseCardSkeleton uses lg/sm pair.\n const legacySize: 'default' | 'sm' = size === 'sm' ? 'sm' : 'default';\n switch (type) {\n case 'blog_post_existing':\n return <BlogCardSkeleton size={legacySize} />;\n case 'case_study':\n return <CaseStudyCardSkeleton size={legacySize} />;\n case 'customer_interview':\n return <CustomerInterviewCardSkeleton size={legacySize} />;\n case 'product_release':\n return <ProductReleaseCardSkeleton size={size === 'sm' ? 'sm' : 'lg'} />;\n case 'podcast':\n case 'webinar':\n case 'event':\n return <ProgramCardSkeleton size={legacySize} />;\n case 'investor_update':\n return <InvestorUpdateCardSkeleton size={legacySize} />;\n case 'onboarding_guide':\n // The rich catalog variant (hero + author grid, clamped description) —\n // the step-numbered 'default' variant is for the guide detail page's\n // \"More in section\" rail, not this full-width row.\n return <OnboardingGuideCardSkeleton size={size === 'sm' ? 'sm' : 'catalog'} />;\n case 'marketing_campaign':\n return adminCampaignCard ? <adminCampaignCard.Skeleton size={legacySize} /> : null;\n case 'roadmap_item':\n case 'delivery_item':\n case 'internal_task':\n return <RoadmapCardSkeleton size={legacySize} />;\n default:\n return null;\n }\n}\n\n/**\n * Per-type card dispatch — renders the right card with the right size.\n * Sized cards (`'lg'` / `'default'`) are unique to this rail — the chat\n * dispatcher only renders `'sm'`, so we go directly through the per-type\n * cards here.\n *\n * PURE FUNCTION COMPONENT WITH ZERO HOOK CALLS: the placeholder comes from a\n * plain `extras.buildOgPlaceholderUrl` call (the chat `dispatch.tsx`\n * pattern) and the anchor-prop bundle arrives via the `LinkProvider`\n * render-prop from the parent — so per-card hook legality is owned by the\n * injected provider component, not by this switch.\n *\n * `href` comes from the host's `resolveHref(ref)` (hub: live\n * `buildContentURL` recomposition; default: the ref's stored url).\n */\nfunction CardForType({\n type,\n item,\n size,\n href,\n targetPlatform,\n linkProps,\n extras,\n adminCampaignCard,\n}: {\n type: string;\n item: any;\n contentRef: ContentRef;\n size: CardSize;\n href: string;\n targetPlatform: string | null;\n linkProps: CardLinkAnchorProps | null;\n extras?: ChatCardDispatchExtras;\n adminCampaignCard?: AdminCampaignCardSlot;\n}): React.ReactNode {\n // Most card variants accept only `{default, sm}`. `'lg'` collapses to\n // `'default'` for those. ProductReleaseCard uses its own lg/sm pair.\n const legacySize: 'default' | 'sm' = size === 'sm' ? 'sm' : 'default';\n // OG placeholder URL — injected into the pure-presentation cards so they\n // render a branded fallback when the row's featured image is null. Plain\n // function call (NOT a hook). Title is the universal field across all card\n // item shapes used here.\n const placeholderUrl =\n extras?.buildOgPlaceholderUrl?.((item?.title as string | undefined) ?? '') ?? undefined;\n\n // Top-level target/rel for cards that take them as separate props\n // (BlogCard, CaseStudyCard, …). ProductReleaseCard takes the bundle as a\n // single `anchorProps={...}` and uses `linkProps` directly. When the host\n // didn't surface a URL, `linkProps` is null and the card stays in\n // non-anchor mode.\n const anchorAttrs: Pick<CardLinkAnchorProps, 'target' | 'rel'> = linkProps\n ? { target: linkProps.target, rel: linkProps.rel }\n : {};\n\n switch (type) {\n case 'blog_post_existing':\n return <BlogCard post={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'case_study':\n return <CaseStudyCard study={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'customer_interview':\n return <CustomerInterviewCard interview={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'product_release': {\n // Anchor-prop pattern: build product-release lg-variant props from the\n // shared `buildProductReleaseCardProps` so this rail and the /releases\n // catalog page render byte-identically. The card wraps in\n // `<a {...anchorProps}>` ONLY when `anchorProps.href` is set — pass\n // `undefined` (not an empty object) when href is empty so the card\n // stays in non-anchor mode without rendering a dead <a> tag.\n const releaseSize = size === 'sm' ? 'sm' : 'lg';\n const buildReleaseProps = extras?.buildProductReleaseCardProps ?? buildProductReleaseCardProps;\n const releaseProps = buildReleaseProps(item);\n return (\n <ProductReleaseCard\n size={releaseSize}\n title={item.title}\n summary={item.summary}\n version={item.version}\n {...releaseProps}\n anchorProps={linkProps ?? undefined}\n />\n );\n }\n case 'podcast':\n return extras?.programConfigs?.podcast\n ? <ProgramCard config={extras.programConfigs.podcast} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'webinar':\n return extras?.programConfigs?.webinar\n ? <ProgramCard config={extras.programConfigs.webinar} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'event':\n return extras?.programConfigs?.event\n ? <ProgramCard config={extras.programConfigs.event} item={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />\n : null;\n case 'investor_update':\n return <InvestorUpdateCard update={item} size={legacySize} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'onboarding_guide':\n // Catalog variant (see skeleton note) — full-width rich card with a\n // line-clamped description instead of the step-numbered rail card.\n return <OnboardingGuideCard guide={item} size={size === 'sm' ? 'sm' : 'catalog'} href={href} targetPlatform={targetPlatform} placeholderUrl={placeholderUrl} {...anchorAttrs} />;\n case 'marketing_campaign':\n return adminCampaignCard ? <adminCampaignCard.Card campaign={item} /> : null;\n case 'roadmap_item':\n case 'delivery_item':\n case 'internal_task':\n return (\n <RoadmapCard\n item={item}\n href={href ?? ''}\n targetPlatform={targetPlatform}\n userVote={null}\n onVote={() => {}}\n size={legacySize}\n cardType={type as 'roadmap_item' | 'delivery_item' | 'internal_task'}\n {...anchorAttrs}\n />\n );\n default:\n return null;\n }\n}\n\n// =============================================================================\n// Fetch all items for a type in ONE server-sorted call, via the injectable\n// list-URL builder. `useSelfFetch` (URL = cache key) replaces the hub's old\n// react-query usage: `enabled` ≙ `url === null`, `!res.ok`/network error ≙\n// `error → items null → group renders nothing`. Accepted deltas vs\n// react-query: no retry/backoff, no focus refetch, no cross-mount cache.\n// =============================================================================\n\nfunction useGroupItems(\n type: string,\n refs: ContentRef[],\n buildUrl: (type: string, ids: string[]) => string | null,\n) {\n const ids = refs.map((r) => r.id);\n const url = ids.length > 0 ? buildUrl(type, ids) : null;\n const { data, isLoading } = useSelfFetch<unknown>(url);\n const items = data != null ? extractItems(data) : null;\n return { items, isLoading };\n}\n\n// =============================================================================\n// Per-group renderer — one API call, server-sorted, then render cards via the\n// dispatcher with per-type skeletons + per-type layout from CONTENT_REF_GROUPS.\n// =============================================================================\n\n/** Map columns prop → tailwind grid class. Only consulted for grid-layout\n * groups; list-layout groups stack vertically. */\nfunction gridClassFor(columns: 2 | 3): string {\n return columns === 3\n ? 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6'\n : 'grid grid-cols-1 sm:grid-cols-2 gap-6';\n}\n\n/** Resolve the group config for a type, falling back to a grid layout with\n * the default card size for unregistered types so the section still renders\n * rather than silently dropping them. The `label` field on the fallback is\n * intentionally a placeholder — the section heading goes through\n * `getContentRefLabelOrTitleCase(type)` instead so cross-surface labels\n * stay consistent between this rail and the investor-email builder. */\nfunction resolveGroupConfig(type: string): ContentRefGroupConfig {\n return CONTENT_REF_GROUPS[type] ?? {\n label: type,\n order: 999,\n layout: 'grid',\n gridSize: 'default',\n };\n}\n\nfunction ContentGroup({\n type,\n refs,\n columns,\n buildUrl,\n resolveHref,\n LinkProvider,\n extras,\n adminCampaignCard,\n}: {\n type: string;\n refs: ContentRef[];\n columns: 2 | 3;\n buildUrl: (type: string, ids: string[]) => string | null;\n resolveHref: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };\n LinkProvider: CardLinkProvider;\n extras?: ChatCardDispatchExtras;\n adminCampaignCard?: AdminCampaignCardSlot;\n}) {\n const { items, isLoading } = useGroupItems(type, refs, buildUrl);\n const config = resolveGroupConfig(type);\n const isListLayout = config.layout === 'list';\n const cardSize = config.gridSize;\n\n // Skeleton gate: `isLoading && !items` — SSR HTML and the client's first\n // paint render identical skeletons (useSelfFetch starts isLoading=true on\n // both sides), and once items exist they are never replaced by skeletons.\n if (isLoading && !items) {\n const skeletons = refs.map((r) => (\n <div key={r.id}>{renderSkeletonForType(type, cardSize, adminCampaignCard)}</div>\n ));\n return isListLayout ? (\n <div className=\"space-y-4\">{skeletons}</div>\n ) : (\n <div className={gridClassFor(columns)}>{skeletons}</div>\n );\n }\n\n if (!items || items.length === 0) return null;\n\n // Index fetched rows by id, then render in REF order — refs carry the\n // intended sequence (suggestion mode: the engine's tier order, so\n // same-platform/tag-matched items lead; controlled mode: the curated\n // display_order). The list APIs return rows date-sorted, which would\n // otherwise scramble that ordering (same-platform items sinking below\n // newer cross-platform ones).\n const itemById = new Map((items as any[]).map((it) => [String(it.id), it]));\n\n const cards = refs\n .map((contentRef) => {\n const itemId = String(contentRef.id);\n const item = itemById.get(itemId);\n if (!item) return null;\n // Re-compose the URL via the host's resolver (hub: buildContentURL so\n // dev gets localhost and prod the right platform domain; default: the\n // ref's stored url as the API composed it).\n const resolved = resolveHref(contentRef);\n const href = resolved.href ?? '';\n const targetPlatform = resolved.targetPlatform ?? contentRef.targetPlatform ?? null;\n return (\n <div key={itemId}>\n <LinkProvider href={href || null} targetPlatform={targetPlatform}>\n {(linkProps) => (\n <CardForType\n type={type}\n item={item}\n contentRef={contentRef}\n size={cardSize}\n href={href}\n targetPlatform={targetPlatform}\n linkProps={linkProps}\n extras={extras}\n adminCampaignCard={adminCampaignCard}\n />\n )}\n </LinkProvider>\n </div>\n );\n })\n .filter(Boolean);\n\n if (cards.length === 0) return null;\n\n return isListLayout ? (\n <div className=\"space-y-4\">{cards}</div>\n ) : (\n <div className={gridClassFor(columns)}>{cards}</div>\n );\n}\n\n// =============================================================================\n// Main component\n// =============================================================================\n\ninterface RelatedContentResponse {\n refs: ContentRefWithReason[];\n}\n\nexport interface RelatedContentSectionProps {\n /** CONTROLLED mode (the original behavior). When defined — even `[]` — no\n * suggestion fetch runs and exactly these refs render. */\n contentRefs?: ContentRef[];\n /** SUGGESTION mode (with `entityId`): self-fetch suggestions for this host\n * entity from `{apiBaseUrl}/api/related-content`. Ignored when\n * `contentRefs` is provided. */\n entityType?: string;\n entityId?: number | string;\n /** Maps to the suggestion API's `count` param — the PER-TYPE fill target\n * for every candidate type EXCEPT the host's own. Absent → param not sent\n * (server default applies). */\n minResults?: number;\n /** Maps to the suggestion API's `sameTypeCount` param — the budget for the\n * candidate type MATCHING the host's own `entityType` (same-type boost:\n * a blog post's rail leads with more blog posts). Absent → param not\n * sent (host's type uses the server's `count`). */\n sameTypeMinResults?: number;\n /** SSR hydrate for suggestion mode — the server page ran the engine and\n * drills the refs here; the first client fetch is skipped (useSelfFetch\n * initialData contract). */\n initialItems?: ContentRefWithReason[];\n /** Section title (default: \"Related Content\") */\n title?: string;\n /**\n * Grid columns at desktop. 2 = denser cards / wider summary (original\n * investor-update layout); 3 = more cards per row for dashboards.\n * Only consulted for grid-layout groups. Default: 2.\n */\n columns?: 2 | 3;\n /**\n * ContentRef.type values to exclude. Honored in ALL modes — controlled\n * mode post-filters (original behavior); suggestion mode ALSO forwards the\n * list verbatim as the API's `excludeTypes=` param so excluded types never\n * consume engine fill slots (`minResults` stays honored). The subtraction\n * happens SERVER-side — this component never mirrors the hub's candidate\n * list.\n */\n excludeTypes?: string[];\n /** Fetch-URL prefix for third-party embeds / reverse proxies\n * ('' = same-origin). Applied to BOTH the suggestion fetch and the\n * default per-group list fetches. */\n apiBaseUrl?: string;\n /** Host injection bundle — REUSES the chat dispatcher's\n * `ChatCardDispatchExtras` (programConfigs, buildOgPlaceholderUrl,\n * buildProductReleaseCardProps override). Program groups render nothing\n * when their config is absent. */\n extras?: ChatCardDispatchExtras;\n /** Hub injects its `buildContentURL` recomposition; default uses the\n * ref's stored `url`/`targetPlatform` as the API composed them. */\n resolveHref?: (ref: ContentRef) => { href: string | null; targetPlatform: string | null };\n /** Hub injects its registry-driven entity-list-api builder; default = the\n * lib's `buildListUrl(type, ids, apiBaseUrl)`. */\n buildListUrl?: (type: string, ids: string[]) => string | null;\n /** Hub injects a `useNavLink`-backed render-prop provider; default = pure\n * anchor via `decideNewTab`. MUST be a module-scope component. */\n LinkProvider?: CardLinkProvider;\n /** Renderer pair for the admin-only `marketing_campaign` type. Absent →\n * the type renders nothing. */\n adminCampaignCard?: AdminCampaignCardSlot;\n}\n\nexport function RelatedContentSection({\n contentRefs,\n entityType,\n entityId,\n minResults,\n sameTypeMinResults,\n initialItems,\n title = 'Related Content',\n columns = 2,\n excludeTypes,\n apiBaseUrl = '',\n extras,\n resolveHref = defaultResolveHref,\n buildListUrl,\n LinkProvider = DefaultLinkPropsProvider,\n adminCampaignCard,\n}: RelatedContentSectionProps) {\n // ── Hooks above EVERY early return (the original `if (!contentRefs.length)\n // return null` guard moved below them). ──\n\n // Suggestion-mode fetch URL — null in controlled mode (contentRefs defined,\n // even []) or when the entity scope is incomplete.\n const suggestUrl =\n contentRefs === undefined && entityType && entityId !== undefined && entityId !== null && entityId !== ''\n ? buildSuggestionUrl('/api/related-content', {\n apiBaseUrl,\n entityType,\n entityId,\n count: minResults,\n extraParams: {\n sameTypeCount: sameTypeMinResults !== undefined ? String(sameTypeMinResults) : undefined,\n excludeTypes: excludeTypes && excludeTypes.length > 0 ? excludeTypes.join(',') : undefined,\n },\n })\n : null;\n // Memoize the initialData wrapper — useSelfFetch re-syncs on [initialData],\n // and a fresh per-render object would loop setState under re-rendering\n // parents (the latent FaqSection bug, fixed there in the same change).\n const initialData = useMemo<RelatedContentResponse | undefined>(\n () => (initialItems ? { refs: initialItems } : undefined),\n [initialItems],\n );\n const { data } = useSelfFetch<RelatedContentResponse>(suggestUrl, { initialData });\n\n // Default group fetcher: the lib's byte-parity-tested builder, prefixed for\n // embeds. Memoized so group-fetch URLs stay value-stable across renders.\n const effectiveBuildListUrl = useMemo(\n () => buildListUrl ?? ((type: string, ids: string[]) => libBuildListUrl(type, ids, apiBaseUrl)),\n [buildListUrl, apiBaseUrl],\n );\n\n const refs: ContentRef[] = contentRefs ?? data?.refs ?? [];\n\n // Per-consumer type gating — drops refs whose `type` is in the exclude\n // list. In suggestion mode the server already subtracted these (the param\n // is forwarded above); the client filter stays as an idempotent guard and\n // IS the mechanism in controlled mode (original behavior).\n const exclude = new Set(excludeTypes || []);\n const visibleRefs = exclude.size > 0 ? refs.filter((r) => !exclude.has(r.type)) : refs;\n // Zero refs (still loading in suggestion mode, or genuinely empty) → no\n // empty shell.\n if (!visibleRefs.length) return null;\n\n const grouped: Record<string, ContentRef[]> = {};\n for (const ref of visibleRefs) {\n if (!grouped[ref.type]) grouped[ref.type] = [];\n grouped[ref.type].push(ref);\n }\n\n // Registered types in CONTENT_REF_GROUPS order, then any unregistered\n // types appended (same shape the investor-email builder uses — both\n // consume `orderContentRefTypes` so cross-surface ordering matches).\n // SAME-TYPE FIRST: when a host entityType is known (suggestion / SSR\n // modes), its own content-type group is hoisted to the top — a blog\n // post's rail leads with blog posts. Rail group keys are compared via\n // the shared alias canonicalizer (blog_post_existing ↔ blog_post).\n let orderedTypes = orderContentRefTypes(Object.keys(grouped));\n if (entityType) {\n const sameType = orderedTypes.filter((t) => canonicalContentRefType(t) === entityType);\n if (sameType.length > 0) {\n orderedTypes = [...sameType, ...orderedTypes.filter((t) => canonicalContentRefType(t) !== entityType)];\n }\n }\n\n return (\n <div className=\"space-y-8\">\n <h2 className=\"text-2xl font-bold text-ods-text-primary\">{title}</h2>\n {orderedTypes.map((type) => (\n <div key={type} className=\"space-y-4\">\n <h3 className=\"font-['Azeret_Mono'] text-[14px] font-semibold uppercase text-ods-text-secondary tracking-wider\">\n {getContentRefLabelOrTitleCase(type)}\n </h3>\n <ContentGroup\n type={type}\n refs={grouped[type]}\n columns={columns}\n buildUrl={effectiveBuildListUrl}\n resolveHref={resolveHref}\n LinkProvider={LinkProvider}\n extras={extras}\n adminCampaignCard={adminCampaignCard}\n />\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":["buildListUrl"]}
@@ -1,4 +1,24 @@
1
1
  "use client";
2
+ import {
3
+ ToolIcon,
4
+ dotColorByVariant,
5
+ getSmallPlatformIcon,
6
+ progressColorByVariant,
7
+ toolLabels,
8
+ useAutoLimitTags,
9
+ useCopyToClipboard,
10
+ useDebounce,
11
+ useHorizontalScrollbar,
12
+ useHumanitySignals,
13
+ useImageEdgeColor,
14
+ useLgUp,
15
+ useLocalStorage,
16
+ useMdUp,
17
+ useNearViewport,
18
+ useOgPlaceholder,
19
+ useOnboardingState,
20
+ useToast
21
+ } from "./chunk-UECRKCKH.js";
2
22
  import {
3
23
  useChatRuntime,
4
24
  useRequiredChatRuntime
@@ -9,6 +29,12 @@ import {
9
29
  import {
10
30
  dynamic
11
31
  } from "./chunk-EL5YVPD5.js";
32
+ import {
33
+ init_next_navigation,
34
+ usePathname,
35
+ useRouter,
36
+ useSearchParams
37
+ } from "./chunk-PLJLE4A4.js";
12
38
  import {
13
39
  Button,
14
40
  Checkbox,
@@ -30,32 +56,6 @@ import {
30
56
  init_next_link,
31
57
  next_link_default
32
58
  } from "./chunk-OHPI2HRK.js";
33
- import {
34
- ToolIcon,
35
- dotColorByVariant,
36
- getSmallPlatformIcon,
37
- progressColorByVariant,
38
- toolLabels,
39
- useAutoLimitTags,
40
- useCopyToClipboard,
41
- useDebounce,
42
- useHorizontalScrollbar,
43
- useHumanitySignals,
44
- useImageEdgeColor,
45
- useLgUp,
46
- useLocalStorage,
47
- useMdUp,
48
- useNearViewport,
49
- useOgPlaceholder,
50
- useOnboardingState,
51
- useToast
52
- } from "./chunk-UECRKCKH.js";
53
- import {
54
- init_next_navigation,
55
- usePathname,
56
- useRouter,
57
- useSearchParams
58
- } from "./chunk-PLJLE4A4.js";
59
59
  import {
60
60
  BashIcon,
61
61
  ClickUpIcon,
@@ -4397,15 +4397,12 @@ var ChatMessageList = forwardRef13(
4397
4397
  onLoadMoreRef.current = onLoadMore;
4398
4398
  const isFetchingRef = useRef6(isFetchingNextPage);
4399
4399
  isFetchingRef.current = isFetchingNextPage;
4400
- const dialogIdRef = useRef6(dialogId);
4401
- dialogIdRef.current = dialogId;
4402
4400
  const prevRenderRef = useRef6({ dialogId: void 0, messageCount: 0, lastMessageId: void 0 });
4403
- const loadOlderAnchorRef = useRef6(null);
4401
+ const prependRef = useRef6({ firstMessageId: void 0, firstMessageContent: void 0, scrollHeight: 0 });
4404
4402
  useEffect6(() => {
4405
4403
  if (!autoScroll) return;
4406
4404
  const dialogChanged = dialogId !== prevRenderRef.current.dialogId;
4407
4405
  const prevCount = prevRenderRef.current.messageCount;
4408
- const prevLastId = prevRenderRef.current.lastMessageId;
4409
4406
  const newCount = messages.length;
4410
4407
  const currentLastId = messages[messages.length - 1]?.id;
4411
4408
  prevRenderRef.current = { dialogId, messageCount: newCount, lastMessageId: currentLastId };
@@ -4418,8 +4415,7 @@ var ChatMessageList = forwardRef13(
4418
4415
  return;
4419
4416
  }
4420
4417
  if (newCount > prevCount) {
4421
- const prevLastIdx = prevLastId ? messages.findIndex((m) => m.id === prevLastId) : -1;
4422
- const newSlice = prevLastIdx >= 0 ? messages.slice(prevLastIdx + 1) : messages.slice(prevCount);
4418
+ const newSlice = messages.slice(prevCount);
4423
4419
  const hasNewUser = newSlice.some((m) => m.role === "user");
4424
4420
  if (hasNewUser) {
4425
4421
  void scrollToBottom({ animation: "instant", ignoreEscapes: true });
@@ -4443,17 +4439,34 @@ var ChatMessageList = forwardRef13(
4443
4439
  }, [autoScroll, messages, dialogId, scrollToBottom, contentRef]);
4444
4440
  useLayoutEffect3(() => {
4445
4441
  const el = scrollRef.current;
4446
- const anchor = loadOlderAnchorRef.current;
4447
- if (!el || !anchor) return;
4448
- if (anchor.dialogId !== dialogId) {
4449
- loadOlderAnchorRef.current = null;
4442
+ if (!el) {
4443
+ prependRef.current = { firstMessageId: void 0, firstMessageContent: void 0, scrollHeight: 0 };
4444
+ return;
4445
+ }
4446
+ const currentFirstId = messages[0]?.id;
4447
+ const currentFirstContent = messages[0]?.content;
4448
+ const prevFirstId = prependRef.current.firstMessageId;
4449
+ const prevHeight = prependRef.current.scrollHeight;
4450
+ if (currentFirstId !== prevFirstId) {
4451
+ if (prevFirstId && prevHeight > 0) {
4452
+ const prevIdx = messages.findIndex((m) => m.id === prevFirstId);
4453
+ if (prevIdx > 0) {
4454
+ const addedHeight = el.scrollHeight - prevHeight;
4455
+ if (addedHeight > 0) el.scrollTop += addedHeight;
4456
+ }
4457
+ }
4458
+ prependRef.current = {
4459
+ firstMessageId: currentFirstId,
4460
+ firstMessageContent: currentFirstContent,
4461
+ scrollHeight: el.scrollHeight
4462
+ };
4450
4463
  return;
4451
4464
  }
4452
- const addedHeight = el.scrollHeight - anchor.scrollHeight;
4453
- if (addedHeight <= 0) return;
4454
- loadOlderAnchorRef.current = null;
4455
- el.scrollTop = anchor.scrollTop + addedHeight;
4456
- }, [messages, dialogId, scrollRef]);
4465
+ prependRef.current.scrollHeight = el.scrollHeight;
4466
+ if (currentFirstContent !== prependRef.current.firstMessageContent) {
4467
+ prependRef.current.firstMessageContent = currentFirstContent;
4468
+ }
4469
+ }, [messages, scrollRef]);
4457
4470
  const messageElsRef = useRef6(/* @__PURE__ */ new Map());
4458
4471
  const refCallbacksRef = useRef6(/* @__PURE__ */ new Map());
4459
4472
  const getRegisterMessageEl = (id) => {
@@ -4525,14 +4538,6 @@ var ChatMessageList = forwardRef13(
4525
4538
  const entry = entries[0];
4526
4539
  if (!entry) return;
4527
4540
  if (entry.isIntersecting && !isFetchingRef.current) {
4528
- const el = scrollRef.current;
4529
- if (el) {
4530
- loadOlderAnchorRef.current = {
4531
- dialogId: dialogIdRef.current,
4532
- scrollHeight: el.scrollHeight,
4533
- scrollTop: el.scrollTop
4534
- };
4535
- }
4536
4541
  onLoadMoreRef.current?.();
4537
4542
  }
4538
4543
  },
@@ -4540,7 +4545,7 @@ var ChatMessageList = forwardRef13(
4540
4545
  );
4541
4546
  observer.observe(sentinelElement);
4542
4547
  return () => observer.disconnect();
4543
- }, [hasNextPage, isFetchingNextPage, scrollRef]);
4548
+ }, [hasNextPage, scrollRef]);
4544
4549
  useImperativeHandle2(ref, () => scrollRef.current, [scrollRef]);
4545
4550
  if (isLoading) {
4546
4551
  return /* @__PURE__ */ jsx26(
@@ -17127,6 +17132,9 @@ function scrollElementIntoView(target, options = {}) {
17127
17132
 
17128
17133
  // src/utils/list-url.ts
17129
17134
  var ALIASES = { blog_post_existing: "blog_post" };
17135
+ function canonicalContentRefType(contentRefType) {
17136
+ return ALIASES[contentRefType] ?? contentRefType;
17137
+ }
17130
17138
  var BUILDERS = {
17131
17139
  roadmap_item: (ids, b) => `${b}/api/roadmap?task_ids=${ids.join(",")}`,
17132
17140
  delivery_item: (ids, b) => `${b}/api/delivery?task_ids=${ids.join(",")}`,
@@ -17151,6 +17159,60 @@ function buildListUrl(contentRefType, ids, base = "") {
17151
17159
  return fn ? fn(ids, base) : null;
17152
17160
  }
17153
17161
 
17162
+ // src/utils/content-ref-groups.ts
17163
+ var CONTENT_REF_GROUPS = {
17164
+ investor_update: { label: "Investor Updates", order: 1, layout: "grid", gridSize: "default" },
17165
+ product_release: { label: "Product Releases", order: 2, layout: "list", gridSize: "lg" },
17166
+ podcast: { label: "Podcasts", order: 3, layout: "list", gridSize: "default" },
17167
+ webinar: { label: "Webinars", order: 4, layout: "list", gridSize: "default" },
17168
+ case_study: { label: "Case Studies", order: 5, layout: "grid", gridSize: "default" },
17169
+ event: { label: "Events", order: 6, layout: "list", gridSize: "default" },
17170
+ blog_post_existing: { label: "Blog Posts", order: 7, layout: "grid", gridSize: "default" },
17171
+ customer_interview: { label: "Customer Interviews", order: 8, layout: "grid", gridSize: "default" },
17172
+ onboarding_guide: { label: "Onboarding Guides", order: 9, layout: "list", gridSize: "default" }
17173
+ };
17174
+ function getContentRefLabel(type) {
17175
+ return CONTENT_REF_GROUPS[type]?.label ?? null;
17176
+ }
17177
+ function getContentRefLabelOrTitleCase(type) {
17178
+ return CONTENT_REF_GROUPS[type]?.label ?? type.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase());
17179
+ }
17180
+ function orderContentRefTypes(present) {
17181
+ const presentSet = new Set(present);
17182
+ const registeredInOrder = Object.entries(CONTENT_REF_GROUPS).sort(([, a], [, b]) => a.order - b.order).map(([type]) => type).filter((type) => presentSet.has(type));
17183
+ const unregistered = [...presentSet].filter((type) => !CONTENT_REF_GROUPS[type]);
17184
+ return [...registeredInOrder, ...unregistered];
17185
+ }
17186
+
17187
+ // src/utils/extract-items.ts
17188
+ function extractItems(data) {
17189
+ if (!data || typeof data !== "object") return [];
17190
+ const obj = data;
17191
+ if (Array.isArray(obj.items)) return obj.items;
17192
+ if (Array.isArray(obj.posts)) return obj.posts;
17193
+ if (Array.isArray(obj.campaigns)) return obj.campaigns;
17194
+ if (Array.isArray(obj.completed) || Array.isArray(obj.inProgress)) {
17195
+ const completed = Array.isArray(obj.completed) ? obj.completed : [];
17196
+ const inProgress = Array.isArray(obj.inProgress) ? obj.inProgress : [];
17197
+ return [...completed, ...inProgress];
17198
+ }
17199
+ if (Array.isArray(obj.data)) return obj.data;
17200
+ if (Array.isArray(data)) return data;
17201
+ return [];
17202
+ }
17203
+ function extractItemId(type, item) {
17204
+ if (!item || typeof item !== "object") return null;
17205
+ const obj = item;
17206
+ if (type === "roadmap_item" || type === "delivery_item" || type === "internal_task") {
17207
+ const ext = obj.external_id;
17208
+ if (typeof ext === "string") return ext;
17209
+ }
17210
+ const id = obj.id;
17211
+ if (typeof id === "string") return id;
17212
+ if (typeof id === "number") return String(id);
17213
+ return null;
17214
+ }
17215
+
17154
17216
  // src/utils/content-href.ts
17155
17217
  var DEFAULT_CONTENT_SUFFIXES = {
17156
17218
  onboarding_guide: "onboarding-guides",
@@ -23837,7 +23899,6 @@ var CategorySection = ({
23837
23899
  /* @__PURE__ */ jsx195(
23838
23900
  Button,
23839
23901
  {
23840
- type: "button",
23841
23902
  variant: "transparent",
23842
23903
  onClick: (e) => {
23843
23904
  e.stopPropagation();
@@ -36485,33 +36546,6 @@ init_next_navigation();
36485
36546
 
36486
36547
  // src/components/chat/hooks/use-chat-card-item.ts
36487
36548
  import { useQuery } from "@tanstack/react-query";
36488
- function extractItems(data) {
36489
- if (!data || typeof data !== "object") return [];
36490
- const obj = data;
36491
- if (Array.isArray(obj.items)) return obj.items;
36492
- if (Array.isArray(obj.posts)) return obj.posts;
36493
- if (Array.isArray(obj.campaigns)) return obj.campaigns;
36494
- if (Array.isArray(obj.completed) || Array.isArray(obj.inProgress)) {
36495
- const completed = Array.isArray(obj.completed) ? obj.completed : [];
36496
- const inProgress = Array.isArray(obj.inProgress) ? obj.inProgress : [];
36497
- return [...completed, ...inProgress];
36498
- }
36499
- if (Array.isArray(obj.data)) return obj.data;
36500
- if (Array.isArray(data)) return data;
36501
- return [];
36502
- }
36503
- function extractItemId(type, item) {
36504
- if (!item || typeof item !== "object") return null;
36505
- const obj = item;
36506
- if (type === "roadmap_item" || type === "delivery_item" || type === "internal_task") {
36507
- const ext = obj.external_id;
36508
- if (typeof ext === "string") return ext;
36509
- }
36510
- const id = obj.id;
36511
- if (typeof id === "string") return id;
36512
- if (typeof id === "number") return String(id);
36513
- return null;
36514
- }
36515
36549
  function useChatCardItem(type, id) {
36516
36550
  const runtime = useRequiredChatRuntime();
36517
36551
  const url = runtime.endpoints.buildListUrl(type, [id]);
@@ -42061,110 +42095,6 @@ function extractIncompleteMessageState(lastMessage) {
42061
42095
  };
42062
42096
  }
42063
42097
 
42064
- // src/components/chat/utils/history-merge.ts
42065
- var SYNTHETIC_REALTIME_ID_PREFIXES = ["assistant-", "user-", "error-"];
42066
- function isSyntheticRealtimeId(id) {
42067
- return SYNTHETIC_REALTIME_ID_PREFIXES.some((prefix) => id.startsWith(prefix));
42068
- }
42069
- function flattenMessagePagesChronological(pages) {
42070
- if (!pages) return [];
42071
- return [...pages].reverse().flatMap((page) => [...page.messages].reverse());
42072
- }
42073
- function maxPersistedStreamSeq(pages) {
42074
- let max = 0;
42075
- if (!pages) return max;
42076
- for (const page of pages) {
42077
- for (const msg of page.messages) {
42078
- const seq = msg.lastChunkStreamSeq;
42079
- if (typeof seq === "number" && seq > max) max = seq;
42080
- }
42081
- }
42082
- return max;
42083
- }
42084
- function mergeHistoryWithRealtime(input) {
42085
- const {
42086
- processedHistory,
42087
- rawHistoryIds,
42088
- existingMessages,
42089
- streamingMessageId,
42090
- historyFetchedAt,
42091
- historyMaxStreamSeq = 0,
42092
- realtimeSeenStreamSeq = 0
42093
- } = input;
42094
- if (processedHistory.length === 0) return existingMessages;
42095
- const processedIds = new Set(processedHistory.map((m) => m.id));
42096
- const seqCoverageKnown = realtimeSeenStreamSeq > 0 && historyMaxStreamSeq > 0;
42097
- const historyCoversRealtime = seqCoverageKnown ? historyMaxStreamSeq >= realtimeSeenStreamSeq : null;
42098
- const trailing = processedHistory[processedHistory.length - 1];
42099
- const historyTrailingAssistant = trailing?.role === "assistant" ? trailing : null;
42100
- const historyBatchId = historyTrailingAssistant && Array.isArray(historyTrailingAssistant.content) ? historyTrailingAssistant.content.find((s) => s.type === "approval_batch")?.data?.approvalRequestId ?? null : null;
42101
- let processedToUse = processedHistory;
42102
- const pinnedSyntheticIds = /* @__PURE__ */ new Set();
42103
- const droppedSyntheticIds = /* @__PURE__ */ new Set();
42104
- if (historyBatchId) {
42105
- const batchTwins = existingMessages.filter(
42106
- (m) => m.role === "assistant" && Array.isArray(m.content) && m.content.some(
42107
- (s) => s.type === "approval_batch" && s.data?.approvalRequestId === historyBatchId
42108
- )
42109
- );
42110
- const existingWithSameBatch = batchTwins.find((m) => m.id === streamingMessageId) ?? batchTwins[batchTwins.length - 1];
42111
- if (existingWithSameBatch && Array.isArray(existingWithSameBatch.content)) {
42112
- const histSize = Array.isArray(historyTrailingAssistant?.content) ? historyTrailingAssistant.content.length : 0;
42113
- const realtimeSize = existingWithSameBatch.content.length;
42114
- if (existingWithSameBatch.id === streamingMessageId || realtimeSize > histSize) {
42115
- processedToUse = processedHistory.slice(0, -1);
42116
- pinnedSyntheticIds.add(existingWithSameBatch.id);
42117
- } else {
42118
- droppedSyntheticIds.add(existingWithSameBatch.id);
42119
- }
42120
- }
42121
- }
42122
- const realtimeMessages = existingMessages.filter((m) => {
42123
- if (pinnedSyntheticIds.has(m.id)) return true;
42124
- if (droppedSyntheticIds.has(m.id)) return false;
42125
- if (processedIds.has(m.id)) return false;
42126
- if (rawHistoryIds?.has(m.id)) return false;
42127
- if (m.role === "user" && m.id.startsWith("optimistic-") && typeof m.content === "string") {
42128
- const canBeInSnapshot = (m.timestamp?.getTime() ?? 0) <= historyFetchedAt;
42129
- return !(canBeInSnapshot && processedToUse.some((pm) => pm.role === "user" && pm.content === m.content));
42130
- }
42131
- if (isSyntheticRealtimeId(m.id) && m.id !== streamingMessageId) {
42132
- const covered = historyCoversRealtime !== null ? historyCoversRealtime : (m.timestamp?.getTime() ?? 0) <= historyFetchedAt;
42133
- if (covered) return false;
42134
- }
42135
- return true;
42136
- });
42137
- return [...processedToUse, ...realtimeMessages];
42138
- }
42139
- function computeHistoryPrepend(processedHistory, existingMessages) {
42140
- const existingIds = new Set(existingMessages.map((m) => m.id));
42141
- const newMessages = [];
42142
- let boundaryMessageIndex = -1;
42143
- for (let i = 0; i < processedHistory.length; i++) {
42144
- if (existingIds.has(processedHistory[i].id)) {
42145
- boundaryMessageIndex = i;
42146
- break;
42147
- }
42148
- newMessages.push(processedHistory[i]);
42149
- }
42150
- let boundaryMessageId;
42151
- let boundaryUpdates;
42152
- if (boundaryMessageIndex >= 0) {
42153
- const boundaryMessage = processedHistory[boundaryMessageIndex];
42154
- const existingBoundary = existingMessages.find((m) => m.id === boundaryMessage.id);
42155
- if (existingBoundary) {
42156
- const existingContent = JSON.stringify(existingBoundary.content);
42157
- const newContent = JSON.stringify(boundaryMessage.content);
42158
- if (existingContent !== newContent) {
42159
- boundaryMessageId = boundaryMessage.id;
42160
- boundaryUpdates = { content: boundaryMessage.content };
42161
- }
42162
- }
42163
- }
42164
- if (newMessages.length === 0 && !boundaryUpdates) return null;
42165
- return { newMessages, boundaryMessageId, boundaryUpdates };
42166
- }
42167
-
42168
42098
  // src/components/chat/utils/chip-action-class.ts
42169
42099
  var CHIP_ACTION_BUTTON_CLASS = "text-[11px] font-medium text-ods-text-secondary hover:text-ods-text-primary bg-transparent hover:bg-ods-card-hover border border-ods-border rounded-md px-2 py-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus";
42170
42100
 
@@ -43675,6 +43605,8 @@ export {
43675
43605
  applyProxyAuth,
43676
43606
  setEmbedAuthAdapter,
43677
43607
  embedAuthedFetch,
43608
+ extractItems,
43609
+ extractItemId,
43678
43610
  useChatCardItem,
43679
43611
  SOURCE_ICON_NAMES,
43680
43612
  getSourceIconName,
@@ -43743,11 +43675,6 @@ export {
43743
43675
  useNatsChatAdapter,
43744
43676
  useUnifiedChat,
43745
43677
  extractIncompleteMessageState,
43746
- SYNTHETIC_REALTIME_ID_PREFIXES,
43747
- flattenMessagePagesChronological,
43748
- maxPersistedStreamSeq,
43749
- mergeHistoryWithRealtime,
43750
- computeHistoryPrepend,
43751
43678
  CHIP_ACTION_BUTTON_CLASS,
43752
43679
  chatChipClass,
43753
43680
  clickupTaskUrl,
@@ -43853,7 +43780,12 @@ export {
43853
43780
  TICKET_STATUS_OPTIONS,
43854
43781
  OPENFRAME_DEV_SECTIONS,
43855
43782
  scrollElementIntoView,
43783
+ canonicalContentRefType,
43856
43784
  buildListUrl,
43785
+ CONTENT_REF_GROUPS,
43786
+ getContentRefLabel,
43787
+ getContentRefLabelOrTitleCase,
43788
+ orderContentRefTypes,
43857
43789
  DEFAULT_CONTENT_SUFFIXES,
43858
43790
  makeComposeContentUrl,
43859
43791
  buildDefaultHref,
@@ -44096,4 +44028,4 @@ export {
44096
44028
  LogsList,
44097
44029
  assets
44098
44030
  };
44099
- //# sourceMappingURL=chunk-M7DPUWAL.js.map
44031
+ //# sourceMappingURL=chunk-FRRMXLU5.js.map