@flamingo-stack/openframe-frontend-core 0.0.215 → 0.0.216

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 (228) hide show
  1. package/dist/chunk-2V4SACHE.js +302 -0
  2. package/dist/chunk-2V4SACHE.js.map +1 -0
  3. package/dist/chunk-572WQWIX.cjs +348 -0
  4. package/dist/chunk-572WQWIX.cjs.map +1 -0
  5. package/dist/{chunk-WT5JV2GS.cjs → chunk-5V6MSE3B.cjs} +39 -39
  6. package/dist/chunk-5V6MSE3B.cjs.map +1 -0
  7. package/dist/{chunk-WQZP3JIZ.js → chunk-CDLYRFDE.js} +1894 -1472
  8. package/dist/chunk-CDLYRFDE.js.map +1 -0
  9. package/dist/chunk-GVNQAGXB.js +232 -0
  10. package/dist/chunk-GVNQAGXB.js.map +1 -0
  11. package/dist/{chunk-P5EE2VJX.cjs → chunk-HOHDXYPR.cjs} +1 -1
  12. package/dist/chunk-HOHDXYPR.cjs.map +1 -0
  13. package/dist/chunk-IH76P5R6.cjs +232 -0
  14. package/dist/chunk-IH76P5R6.cjs.map +1 -0
  15. package/dist/{chunk-24KCAECR.cjs → chunk-JJR27M56.cjs} +3 -3
  16. package/dist/{chunk-24KCAECR.cjs.map → chunk-JJR27M56.cjs.map} +1 -1
  17. package/dist/chunk-K4DFAVSO.cjs +302 -0
  18. package/dist/chunk-K4DFAVSO.cjs.map +1 -0
  19. package/dist/{chunk-HICZPTRR.js → chunk-LCLTCCXS.js} +14 -14
  20. package/dist/chunk-LCLTCCXS.js.map +1 -0
  21. package/dist/{chunk-VFKQMAUF.cjs → chunk-OB45JHDY.cjs} +3 -3
  22. package/dist/{chunk-VFKQMAUF.cjs.map → chunk-OB45JHDY.cjs.map} +1 -1
  23. package/dist/{chunk-4XLJWX2N.js → chunk-ORJREQ2W.js} +4 -4
  24. package/dist/{chunk-7PCP7YQR.js → chunk-QTKU6ULP.js} +6 -6
  25. package/dist/{chunk-CIPO6DXK.js → chunk-QY75VKAS.js} +5 -5
  26. package/dist/{chunk-ZG2YY5E7.js → chunk-RFONYT63.js} +1 -1
  27. package/dist/chunk-RFONYT63.js.map +1 -0
  28. package/dist/{chunk-NGFP4RVL.cjs → chunk-SMCG2CCC.cjs} +30 -30
  29. package/dist/{chunk-NGFP4RVL.cjs.map → chunk-SMCG2CCC.cjs.map} +1 -1
  30. package/dist/{chunk-MX5MIFWA.js → chunk-UEBM4PC4.js} +5 -5
  31. package/dist/chunk-VC3ND5RB.js +348 -0
  32. package/dist/chunk-VC3ND5RB.js.map +1 -0
  33. package/dist/{chunk-UXZ3ZJ3M.cjs → chunk-XDPSSE4O.cjs} +4 -4
  34. package/dist/{chunk-UXZ3ZJ3M.cjs.map → chunk-XDPSSE4O.cjs.map} +1 -1
  35. package/dist/{chunk-D4MNFY67.cjs → chunk-ZGTDUPTW.cjs} +1316 -894
  36. package/dist/chunk-ZGTDUPTW.cjs.map +1 -0
  37. package/dist/components/chat/entity-cards/blog-card.d.ts +1 -1
  38. package/dist/components/chat/entity-cards/blog-card.d.ts.map +1 -1
  39. package/dist/components/chat/entity-cards/case-study-card.d.ts +1 -1
  40. package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
  41. package/dist/components/chat/entity-cards/customer-interview-card.d.ts +1 -1
  42. package/dist/components/chat/entity-cards/customer-interview-card.d.ts.map +1 -1
  43. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  44. package/dist/components/chat/entity-cards/investor-update-card.d.ts +1 -1
  45. package/dist/components/chat/entity-cards/investor-update-card.d.ts.map +1 -1
  46. package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts +1 -1
  47. package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts.map +1 -1
  48. package/dist/components/chat/entity-cards/program-card.d.ts +1 -1
  49. package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
  50. package/dist/components/chat/entity-cards/use-entity-card-link.d.ts +14 -0
  51. package/dist/components/chat/entity-cards/use-entity-card-link.d.ts.map +1 -0
  52. package/dist/components/chat/entity-cards/use-entity-card-placeholder.d.ts +13 -0
  53. package/dist/components/chat/entity-cards/use-entity-card-placeholder.d.ts.map +1 -0
  54. package/dist/components/chat/index.cjs +11 -11
  55. package/dist/components/chat/index.js +10 -10
  56. package/dist/components/contact/index.cjs +12 -12
  57. package/dist/components/contact/index.js +11 -11
  58. package/dist/components/features/captions-url.d.ts +18 -0
  59. package/dist/components/features/captions-url.d.ts.map +1 -0
  60. package/dist/components/features/index.cjs +23 -11
  61. package/dist/components/features/index.cjs.map +1 -1
  62. package/dist/components/features/index.d.ts +2 -0
  63. package/dist/components/features/index.d.ts.map +1 -1
  64. package/dist/components/features/index.js +24 -12
  65. package/dist/components/features/mux-origins.cjs +10 -0
  66. package/dist/components/features/mux-origins.cjs.map +1 -0
  67. package/dist/components/features/mux-origins.d.ts +26 -0
  68. package/dist/components/features/mux-origins.d.ts.map +1 -0
  69. package/dist/components/features/mux-origins.js +7 -0
  70. package/dist/components/features/mux-origins.js.map +1 -0
  71. package/dist/components/features/notifications/index.d.ts +2 -0
  72. package/dist/components/features/notifications/index.d.ts.map +1 -1
  73. package/dist/components/features/notifications/notification-drawer.d.ts +2 -1
  74. package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
  75. package/dist/components/features/notifications/notification-popups.d.ts +10 -0
  76. package/dist/components/features/notifications/notification-popups.d.ts.map +1 -0
  77. package/dist/components/features/notifications/notifications-context.d.ts +8 -1
  78. package/dist/components/features/notifications/notifications-context.d.ts.map +1 -1
  79. package/dist/components/features/notifications/types.d.ts +1 -0
  80. package/dist/components/features/notifications/types.d.ts.map +1 -1
  81. package/dist/components/features/use-video-warmup.d.ts +53 -0
  82. package/dist/components/features/use-video-warmup.d.ts.map +1 -0
  83. package/dist/components/icons/index.cjs +3 -3
  84. package/dist/components/icons/index.js +2 -2
  85. package/dist/components/icons-v2-generated/index.cjs +2 -2
  86. package/dist/components/icons-v2-generated/index.cjs.map +1 -1
  87. package/dist/components/icons-v2-generated/index.js +4 -4
  88. package/dist/components/index.cjs +132 -102
  89. package/dist/components/index.cjs.map +1 -1
  90. package/dist/components/index.d.ts +1 -0
  91. package/dist/components/index.d.ts.map +1 -1
  92. package/dist/components/index.js +94 -64
  93. package/dist/components/index.js.map +1 -1
  94. package/dist/components/navigation/index.cjs +11 -11
  95. package/dist/components/navigation/index.js +10 -10
  96. package/dist/components/onboarding-guides/build-default-href.d.ts +15 -0
  97. package/dist/components/onboarding-guides/build-default-href.d.ts.map +1 -0
  98. package/dist/components/onboarding-guides/hooks/use-onboarding-guides.d.ts +28 -0
  99. package/dist/components/onboarding-guides/hooks/use-onboarding-guides.d.ts.map +1 -0
  100. package/dist/components/onboarding-guides/index.cjs +373 -0
  101. package/dist/components/onboarding-guides/index.cjs.map +1 -0
  102. package/dist/components/onboarding-guides/index.d.ts +25 -0
  103. package/dist/components/onboarding-guides/index.d.ts.map +1 -0
  104. package/dist/components/onboarding-guides/index.js +373 -0
  105. package/dist/components/onboarding-guides/index.js.map +1 -0
  106. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +52 -0
  107. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -0
  108. package/dist/components/onboarding-guides/onboarding-guides-catalog-skeleton.d.ts +17 -0
  109. package/dist/components/onboarding-guides/onboarding-guides-catalog-skeleton.d.ts.map +1 -0
  110. package/dist/components/onboarding-guides/onboarding-guides-catalog-view.d.ts +43 -0
  111. package/dist/components/onboarding-guides/onboarding-guides-catalog-view.d.ts.map +1 -0
  112. package/dist/components/shared/doc-search/doc-search-bar.d.ts +59 -0
  113. package/dist/components/shared/doc-search/doc-search-bar.d.ts.map +1 -0
  114. package/dist/components/shared/doc-search/doc-search-result-row.d.ts +18 -0
  115. package/dist/components/shared/doc-search/doc-search-result-row.d.ts.map +1 -0
  116. package/dist/components/shared/doc-search/format-relative-path.d.ts +10 -0
  117. package/dist/components/shared/doc-search/format-relative-path.d.ts.map +1 -0
  118. package/dist/components/shared/doc-search/index.d.ts +8 -0
  119. package/dist/components/shared/doc-search/index.d.ts.map +1 -0
  120. package/dist/components/shared/doc-search/map-doc-search-results.d.ts +15 -0
  121. package/dist/components/shared/doc-search/map-doc-search-results.d.ts.map +1 -0
  122. package/dist/components/shared/doc-search/resolve-search-result-action.d.ts +37 -0
  123. package/dist/components/shared/doc-search/resolve-search-result-action.d.ts.map +1 -0
  124. package/dist/components/shared/doc-search/types.d.ts +29 -0
  125. package/dist/components/shared/doc-search/types.d.ts.map +1 -0
  126. package/dist/components/shared/doc-search/use-doc-search.d.ts +46 -0
  127. package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -0
  128. package/dist/components/tickets/help-center-card.d.ts +5 -1
  129. package/dist/components/tickets/help-center-card.d.ts.map +1 -1
  130. package/dist/components/tickets/hooks/use-ticket-actions.d.ts +8 -0
  131. package/dist/components/tickets/hooks/use-ticket-actions.d.ts.map +1 -1
  132. package/dist/components/tickets/index.cjs +316 -145
  133. package/dist/components/tickets/index.cjs.map +1 -1
  134. package/dist/components/tickets/index.js +237 -66
  135. package/dist/components/tickets/index.js.map +1 -1
  136. package/dist/components/tickets/ticket-detail-drawer.d.ts +11 -2
  137. package/dist/components/tickets/ticket-detail-drawer.d.ts.map +1 -1
  138. package/dist/components/tickets/types.d.ts +50 -1
  139. package/dist/components/tickets/types.d.ts.map +1 -1
  140. package/dist/components/ui/file-manager/index.cjs +51 -51
  141. package/dist/components/ui/file-manager/index.cjs.map +1 -1
  142. package/dist/components/ui/file-manager/index.js +2 -2
  143. package/dist/components/ui/filter-pill-row.d.ts +20 -0
  144. package/dist/components/ui/filter-pill-row.d.ts.map +1 -0
  145. package/dist/components/ui/index.cjs +16 -14
  146. package/dist/components/ui/index.cjs.map +1 -1
  147. package/dist/components/ui/index.d.ts +1 -0
  148. package/dist/components/ui/index.d.ts.map +1 -1
  149. package/dist/components/ui/index.js +21 -19
  150. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  151. package/dist/contexts/chat-runtime-context.d.ts +42 -0
  152. package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
  153. package/dist/contexts/index.cjs +2 -2
  154. package/dist/contexts/index.js +1 -1
  155. package/dist/embed-shims/index.cjs +3 -3
  156. package/dist/embed-shims/index.cjs.map +1 -1
  157. package/dist/embed-shims/index.js +5 -5
  158. package/dist/hooks/index.cjs +6 -6
  159. package/dist/hooks/index.js +5 -5
  160. package/dist/index.cjs +28 -14
  161. package/dist/index.cjs.map +1 -1
  162. package/dist/index.js +59 -45
  163. package/dist/utils/dev-sections/openframe-dev-sections.d.ts +2 -2
  164. package/dist/utils/dev-sections/openframe-dev-sections.d.ts.map +1 -1
  165. package/dist/utils/index.cjs +11 -5
  166. package/dist/utils/index.cjs.map +1 -1
  167. package/dist/utils/index.js +11 -5
  168. package/dist/utils/index.js.map +1 -1
  169. package/package.json +13 -1
  170. package/src/components/chat/entity-cards/blog-card.tsx +17 -5
  171. package/src/components/chat/entity-cards/case-study-card.tsx +23 -1
  172. package/src/components/chat/entity-cards/customer-interview-card.tsx +23 -1
  173. package/src/components/chat/entity-cards/dispatch.tsx +21 -0
  174. package/src/components/chat/entity-cards/investor-update-card.tsx +23 -1
  175. package/src/components/chat/entity-cards/onboarding-guide-card.tsx +30 -4
  176. package/src/components/chat/entity-cards/program-card.tsx +17 -3
  177. package/src/components/chat/entity-cards/use-entity-card-link.ts +66 -0
  178. package/src/components/chat/entity-cards/use-entity-card-placeholder.ts +50 -0
  179. package/src/components/features/captions-url.ts +25 -0
  180. package/src/components/features/index.ts +2 -0
  181. package/src/components/features/mux-origins.ts +27 -0
  182. package/src/components/features/notifications/index.ts +2 -0
  183. package/src/components/features/notifications/notification-drawer.tsx +100 -16
  184. package/src/components/features/notifications/notification-popups.tsx +105 -0
  185. package/src/components/features/notifications/notifications-context.tsx +16 -0
  186. package/src/components/features/notifications/types.ts +1 -0
  187. package/src/components/features/use-video-warmup.ts +176 -0
  188. package/src/components/index.ts +5 -0
  189. package/src/components/onboarding-guides/build-default-href.ts +16 -0
  190. package/src/components/onboarding-guides/hooks/use-onboarding-guides.ts +90 -0
  191. package/src/components/onboarding-guides/index.ts +39 -0
  192. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +215 -0
  193. package/src/components/onboarding-guides/onboarding-guides-catalog-skeleton.tsx +62 -0
  194. package/src/components/onboarding-guides/onboarding-guides-catalog-view.tsx +230 -0
  195. package/src/components/shared/doc-search/doc-search-bar.tsx +100 -0
  196. package/src/components/shared/doc-search/doc-search-result-row.tsx +73 -0
  197. package/src/components/shared/doc-search/format-relative-path.ts +17 -0
  198. package/src/components/shared/doc-search/index.ts +24 -0
  199. package/src/components/shared/doc-search/map-doc-search-results.ts +113 -0
  200. package/src/components/shared/doc-search/resolve-search-result-action.ts +68 -0
  201. package/src/components/shared/doc-search/types.ts +28 -0
  202. package/src/components/shared/doc-search/use-doc-search.ts +263 -0
  203. package/src/components/tickets/help-center-card.tsx +8 -0
  204. package/src/components/tickets/help-center-list.tsx +17 -3
  205. package/src/components/tickets/hooks/use-ticket-actions.ts +210 -14
  206. package/src/components/tickets/ticket-detail-drawer.tsx +145 -5
  207. package/src/components/tickets/types.ts +55 -0
  208. package/src/components/ui/filter-pill-row.tsx +72 -0
  209. package/src/components/ui/index.ts +1 -0
  210. package/src/components/ui/simple-markdown-renderer.tsx +24 -1
  211. package/src/components/ui/toaster.tsx +3 -3
  212. package/src/contexts/chat-runtime-context.tsx +41 -0
  213. package/src/stories/NotificationDrawer.stories.tsx +18 -2
  214. package/src/utils/dev-sections/openframe-dev-sections.ts +12 -5
  215. package/dist/chunk-2G3NXF6J.cjs +0 -521
  216. package/dist/chunk-2G3NXF6J.cjs.map +0 -1
  217. package/dist/chunk-D4MNFY67.cjs.map +0 -1
  218. package/dist/chunk-HICZPTRR.js.map +0 -1
  219. package/dist/chunk-P5EE2VJX.cjs.map +0 -1
  220. package/dist/chunk-R6MLPU4A.js +0 -521
  221. package/dist/chunk-R6MLPU4A.js.map +0 -1
  222. package/dist/chunk-WQZP3JIZ.js.map +0 -1
  223. package/dist/chunk-WT5JV2GS.cjs.map +0 -1
  224. package/dist/chunk-ZG2YY5E7.js.map +0 -1
  225. /package/dist/{chunk-4XLJWX2N.js.map → chunk-ORJREQ2W.js.map} +0 -0
  226. /package/dist/{chunk-7PCP7YQR.js.map → chunk-QTKU6ULP.js.map} +0 -0
  227. /package/dist/{chunk-CIPO6DXK.js.map → chunk-QY75VKAS.js.map} +0 -0
  228. /package/dist/{chunk-MX5MIFWA.js.map → chunk-UEBM4PC4.js.map} +0 -0
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Format a full document path as a breadcrumb trail.
3
+ * Shows parent folders only (excludes the last segment / filename).
4
+ *
5
+ * @example
6
+ * formatRelativePath('openframe-oss-tenant/architecture/api-controllers.md')
7
+ * // → 'Openframe oss tenant / Architecture'
8
+ */
9
+ export function formatRelativePath(fullPath: string): string {
10
+ if (!fullPath) return ''
11
+ const segments = fullPath.replace(/\.md$/, '').split('/')
12
+ // Show only parent path (exclude the filename itself since the title already shows it)
13
+ const parentSegments = segments.length > 1 ? segments.slice(0, -1) : segments
14
+ return parentSegments
15
+ .map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1).replace(/-/g, ' '))
16
+ .join(' / ')
17
+ }
@@ -0,0 +1,24 @@
1
+ export {
2
+ DocSearchBar,
3
+ type DocSearchBarProps,
4
+ } from './doc-search-bar'
5
+ export {
6
+ DocSearchResultRow,
7
+ type DocSearchResultRowProps,
8
+ type DocSearchResultRowEntry,
9
+ } from './doc-search-result-row'
10
+ export { formatRelativePath } from './format-relative-path'
11
+
12
+ // Hook + supporting helpers — moved from hub `hooks/use-docs.ts` so
13
+ // embedders can mount the search bar directly without re-implementing
14
+ // the debounced fetch + result navigation logic.
15
+ export {
16
+ useDocSearch,
17
+ type UseDocSearchConfig,
18
+ } from './use-doc-search'
19
+ export {
20
+ resolveSearchResultAction,
21
+ type SearchResultAction,
22
+ } from './resolve-search-result-action'
23
+ export { mapDocSearchResults } from './map-doc-search-results'
24
+ export type { DocSearchResult } from './types'
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Map RAG `/api/docs/search` wire results into the `<DocSearchBar>`
3
+ * dropdown's row shape, collapsing entity-table rows into grouped
4
+ * results so the dropdown lists ONE "Cap Table (12 records)" row
5
+ * instead of 12 individual rows.
6
+ *
7
+ * Pure transform — no telemetry, no navigation, no React deps. Lifted
8
+ * from the hub's `hooks/use-docs.ts:mapDocSearchResults` (the hub's
9
+ * `traceCompose` call was hub-only telemetry and is intentionally
10
+ * dropped — callers that want logging can wrap this helper).
11
+ */
12
+
13
+ import type { SearchResult } from '../../ui/search-input'
14
+ import type { DocSearchResult } from './types'
15
+
16
+ /** Source repos that should be collapsed into grouped results in the search bar.
17
+ * Only financial tables (all rows link to the same admin page).
18
+ * Content tables (blog, webinar, podcast, etc.) stay individual since each has a unique URL. */
19
+ const SEARCH_GROUP_REPOS = new Set([
20
+ 'financial-cap-table',
21
+ 'financial-kpis',
22
+ 'financial-pnl',
23
+ 'financial-balance-sheet',
24
+ 'financial-cash-flow',
25
+ ])
26
+
27
+ const ENTITY_LABELS: Record<string, string> = {
28
+ 'financial-cap-table': 'Cap Table',
29
+ 'financial-kpis': 'Financial KPIs',
30
+ 'financial-pnl': 'Profit & Loss',
31
+ 'financial-balance-sheet': 'Balance Sheets',
32
+ 'financial-cash-flow': 'Cash Flow',
33
+ 'blog-posts': 'Blog Posts',
34
+ 'product-releases': 'Product Releases',
35
+ 'case-studies': 'Case Studies',
36
+ webinars: 'Webinars',
37
+ events: 'Events',
38
+ podcasts: 'Podcasts',
39
+ }
40
+
41
+ export function mapDocSearchResults(docs: DocSearchResult[]): SearchResult[] {
42
+ const entityGroups = new Map<string, DocSearchResult[]>()
43
+ // Track insertion order — groups appear where the FIRST row of that
44
+ // repo appeared in the response.
45
+ const order: Array<
46
+ { type: 'entity'; repo: string } | { type: 'doc'; doc: DocSearchResult }
47
+ > = []
48
+ const seenRepos = new Set<string>()
49
+
50
+ for (const doc of docs) {
51
+ if (doc.sourceRepo && SEARCH_GROUP_REPOS.has(doc.sourceRepo)) {
52
+ const group = entityGroups.get(doc.sourceRepo) || []
53
+ group.push(doc)
54
+ entityGroups.set(doc.sourceRepo, group)
55
+ if (!seenRepos.has(doc.sourceRepo)) {
56
+ seenRepos.add(doc.sourceRepo)
57
+ order.push({ type: 'entity', repo: doc.sourceRepo })
58
+ }
59
+ } else {
60
+ order.push({ type: 'doc', doc })
61
+ }
62
+ }
63
+
64
+ const results: SearchResult[] = []
65
+ for (const entry of order) {
66
+ if (entry.type === 'entity') {
67
+ const rows = entityGroups.get(entry.repo)!
68
+ const label = ENTITY_LABELS[entry.repo] || entry.repo
69
+ results.push({
70
+ id: `group-${entry.repo}`,
71
+ title: `${label} (${rows.length} ${rows.length === 1 ? 'record' : 'records'})`,
72
+ path: rows[0].path,
73
+ type: 'file',
74
+ metadata: {
75
+ documentType: rows[0].documentType,
76
+ externalUrl: rows[0].externalUrl,
77
+ sourceRepo: entry.repo,
78
+ id: rows[0].entityId,
79
+ isGroup: true,
80
+ items: rows.map((r) => ({
81
+ name: r.name,
82
+ externalUrl: r.externalUrl,
83
+ id: r.entityId,
84
+ sourceRepo: r.sourceRepo,
85
+ documentType: r.documentType,
86
+ })),
87
+ },
88
+ })
89
+ } else {
90
+ const doc = entry.doc
91
+ const isNonMarkdown = doc.documentType && doc.documentType !== 'markdown'
92
+ results.push({
93
+ id: doc.path,
94
+ title: doc.name,
95
+ description: isNonMarkdown ? doc.name : doc.snippet,
96
+ path: doc.path,
97
+ type: doc.type,
98
+ metadata: {
99
+ matchType: doc.matchType,
100
+ ...(doc.documentType ? { documentType: doc.documentType } : {}),
101
+ ...(doc.externalUrl ? { externalUrl: doc.externalUrl } : {}),
102
+ ...(doc.targetPlatform != null
103
+ ? { targetPlatform: doc.targetPlatform }
104
+ : {}),
105
+ ...(doc.sourceRepo ? { sourceRepo: doc.sourceRepo } : {}),
106
+ ...(doc.entityId ? { id: doc.entityId } : {}),
107
+ },
108
+ })
109
+ }
110
+ }
111
+
112
+ return results
113
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Resolve what should happen when the user picks a search result.
3
+ * Returns one of five typed actions so the caller is a single switch.
4
+ *
5
+ * Resolution order:
6
+ * 1. `externalUrl` present → use `decideNewTab` to choose same-tab vs
7
+ * new-tab against the row's `targetPlatform`.
8
+ * 2. Row has `id` + `sourceRepo` + `documentType` → synth an Ask-AI
9
+ * action (entity drill-in via primary key, no URL).
10
+ * 3. Row has only `path` → legacy navigation fallback.
11
+ * 4. Nothing actionable → noop.
12
+ *
13
+ * Lifted from the hub's `hooks/use-docs.ts:resolveSearchResultAction`.
14
+ * Pure — no React, no telemetry.
15
+ */
16
+
17
+ import type { SearchResult } from '../../ui/search-input'
18
+ import type { ChatRef } from '../../chat/chat-ref.types'
19
+ import { decideNewTab } from '../../chat/utils/decide-new-tab'
20
+
21
+ export type SearchResultAction =
22
+ | { kind: 'navigate-same-tab'; href: string }
23
+ | { kind: 'navigate-new-tab'; href: string }
24
+ | { kind: 'ask-ai'; detail: { source: string; ref: ChatRef } }
25
+ | { kind: 'route'; path: string }
26
+ | { kind: 'noop' }
27
+
28
+ export function resolveSearchResultAction(
29
+ result: SearchResult,
30
+ source: string,
31
+ runtimeMode?: 'host' | 'embed',
32
+ ): SearchResultAction {
33
+ const meta = result.metadata ?? {}
34
+ const externalUrl = meta.externalUrl as string | undefined
35
+ if (externalUrl) {
36
+ // Same pure helper `useNavLink` and `useUnifiedNav` call — single
37
+ // decision rule across cards, chips, and autocomplete rows. Thread
38
+ // the caller's `source` as `currentSource` so the platform-vs-
39
+ // platform comparison matches the hub's pre-migration behavior.
40
+ const targetPlatform = meta.targetPlatform as string | null | undefined
41
+ const isNewTab = decideNewTab({
42
+ href: externalUrl,
43
+ targetPlatform,
44
+ surface: 'useUnifiedNav',
45
+ runtimeMode,
46
+ currentSource: source,
47
+ })
48
+ return isNewTab
49
+ ? { kind: 'navigate-new-tab', href: externalUrl }
50
+ : { kind: 'navigate-same-tab', href: externalUrl }
51
+ }
52
+ const rowId = meta.id as string | undefined
53
+ const sourceRepo = meta.sourceRepo as string | undefined
54
+ const documentType = meta.documentType as string | undefined
55
+ if (rowId && sourceRepo && documentType) {
56
+ return {
57
+ kind: 'ask-ai',
58
+ detail: {
59
+ source,
60
+ ref: { type: documentType, id: rowId, title: result.title, url: null },
61
+ },
62
+ }
63
+ }
64
+ if (result.path) {
65
+ return { kind: 'route', path: result.path }
66
+ }
67
+ return { kind: 'noop' }
68
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Wire shape returned by `/api/docs/search` (one row per result).
3
+ * The hub's server-side RAG layer composes this; the lib hook
4
+ * consumes it. Keep field-for-field identical with the hub's
5
+ * `lib/data/doc-search-utils.ts:DocSearchResult` interface.
6
+ */
7
+ export interface DocSearchResult {
8
+ path: string
9
+ name: string
10
+ type: 'file' | 'folder'
11
+ snippet: string
12
+ matchType: 'title' | 'content'
13
+ documentType?: string
14
+ externalUrl?: string
15
+ /** Platform that owns `externalUrl`. Threaded from the rag-config's
16
+ * `resolveUrl` so the search-bar click handler can hand it straight
17
+ * to the lib's `decideNewTab` for the same-tab-vs-new-tab decision.
18
+ * `null` when external/unknown. */
19
+ targetPlatform?: string | null
20
+ sourceRepo?: string
21
+ /** Row's `RagTableConfig.primaryKey` value when known. Surfaced so the
22
+ * search bar consumer can synthesize a `ChatRef` for entity rows
23
+ * that have no public viewer (cap_table, financial-kpis, etc.) — the
24
+ * click handler opens the chat panel pre-filled with a row drill-in
25
+ * via `entityIdFilter` instead of synthesizing a 404 URL from
26
+ * `result.path`. */
27
+ entityId?: string
28
+ }
@@ -0,0 +1,263 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * `useDocSearch` — debounced RAG-search hook against `/api/docs/search`.
5
+ *
6
+ * Pure fetch + navigation glue. Embedders can mount this directly
7
+ * (any host with a reverse-proxy that exposes `/api/docs/search` will
8
+ * work). Hub callers wire it into the lib `<DocSearchBar>` for the
9
+ * canonical typeahead dropdown.
10
+ *
11
+ * ## What moved from hub to lib
12
+ *
13
+ * Lifted from `multi-platform-hub/hooks/use-docs.ts:useDocSearch`. Two
14
+ * hub-only concerns are now optional injection points instead of
15
+ * direct imports:
16
+ *
17
+ * - `useDocNavigation()` (hub's in-page doc-tree swap) → optional
18
+ * `onInPageSwap?: (path: string) => boolean` config callback. When
19
+ * present and returns true, the hook treats a same-origin result
20
+ * click as "handled in-page"; when absent or returns false, the
21
+ * hook falls back to `onNavigate(path)` (`router.push` on hub,
22
+ * `window.location.assign` on bare embedders).
23
+ * - `traceCompose` (hub-only telemetry) → dropped. The lib has no
24
+ * equivalent runtime-context yet; bring it back when there is one.
25
+ *
26
+ * Everything else (debounce, `useChatRuntime` for embed-mode short-
27
+ * circuit, embed-shim router, the action-resolver + result-mapper) is
28
+ * now lib-resident.
29
+ */
30
+
31
+ import { useState, useEffect, useCallback } from 'react'
32
+ import { useRouter } from '../../../embed-shims'
33
+ import { useDebounce } from '../../../hooks/ui/use-debounce'
34
+ import { useChatRuntime } from '../../../contexts/chat-runtime-context'
35
+ import type { SearchResult } from '../../ui/search-input'
36
+ import {
37
+ resolveExternalNavigation,
38
+ stripSameOriginToPath,
39
+ NEW_TAB_FEATURES,
40
+ } from '../../chat/utils/chat-nav-resolution'
41
+ import type { DocSearchResult } from './types'
42
+ import { mapDocSearchResults } from './map-doc-search-results'
43
+ import { resolveSearchResultAction } from './resolve-search-result-action'
44
+
45
+ export interface UseDocSearchConfig {
46
+ /** Discriminator passed to `/api/docs/search?source=` (e.g.
47
+ * `'openframe'`). Embedders set it to whatever discriminator their
48
+ * reverse-proxy expects. */
49
+ source: string
50
+ /** Base route prefix this search lives under (e.g. `'/onboarding-guides'`).
51
+ * When a result's href starts with `${baseRoute}/`, the hook
52
+ * attempts the optional in-page swap path before falling through
53
+ * to a full nav. */
54
+ baseRoute: string
55
+ /** Imperative navigation fallback. Called when no override
56
+ * (in-page swap, new-tab) applies. Hub callers pass
57
+ * `(path) => router.push(path)`; embedders pass an equivalent. */
58
+ onNavigate: (path: string) => void
59
+ /** Optional `RagTableConfig.id` list to narrow the search to specific
60
+ * tables (e.g. `['onboarding-guides']`). Forwarded to
61
+ * `/api/docs/search?tableIds=…` which intersects with the source's
62
+ * standing set. */
63
+ tableIds?: string[]
64
+ /** Optional in-page swap callback. When the result's href is under
65
+ * `baseRoute` AND this callback returns true, the hook treats the
66
+ * click as handled in-page (no router push). Hub's
67
+ * `<DocumentationSection>` wires this to
68
+ * `useDocNavigation().navigate(path)`. */
69
+ onInPageSwap?: (path: string) => boolean
70
+ /** Optional endpoint override. Defaults to `'/api/docs/search'`
71
+ * (the hub's reverse-proxy route). Embedders with a different
72
+ * path can override. */
73
+ searchEndpoint?: string
74
+ }
75
+
76
+ export function useDocSearch(config: UseDocSearchConfig) {
77
+ const {
78
+ source,
79
+ baseRoute,
80
+ onNavigate,
81
+ tableIds,
82
+ onInPageSwap,
83
+ searchEndpoint = '/api/docs/search',
84
+ } = config
85
+ const tableIdsKey = tableIds && tableIds.length > 0 ? tableIds.join(',') : ''
86
+
87
+ const router = useRouter()
88
+ // Optional chat-runtime read — when present and mode='embed' the
89
+ // search-result row click short-circuits to a new-tab open against
90
+ // the absolutized URL. Null/host preserves today's behavior.
91
+ const runtime = useChatRuntime()
92
+
93
+ const [query, setQuery] = useState('')
94
+ const [results, setResults] = useState<SearchResult[]>([])
95
+ const [isFetching, setIsFetching] = useState(false)
96
+ const debouncedQuery = useDebounce(query, 300)
97
+
98
+ useEffect(() => {
99
+ if (!debouncedQuery || debouncedQuery.trim().length < 2) {
100
+ setResults([])
101
+ setIsFetching(false)
102
+ return
103
+ }
104
+
105
+ let cancelled = false
106
+
107
+ async function fetchResults() {
108
+ setIsFetching(true)
109
+ try {
110
+ const params = new URLSearchParams({
111
+ q: debouncedQuery,
112
+ source,
113
+ limit: '10',
114
+ })
115
+ if (tableIdsKey) params.set('tableIds', tableIdsKey)
116
+
117
+ const response = await fetch(`${searchEndpoint}?${params.toString()}`)
118
+ if (!response.ok) {
119
+ throw new Error(`Search request failed: ${response.status}`)
120
+ }
121
+
122
+ const json = await response.json()
123
+
124
+ if (!cancelled && json.success && Array.isArray(json.data)) {
125
+ const mapped = mapDocSearchResults(json.data as DocSearchResult[])
126
+ setResults(mapped)
127
+ }
128
+ } catch (error) {
129
+ console.error('Doc search error:', error)
130
+ if (!cancelled) {
131
+ setResults([])
132
+ }
133
+ } finally {
134
+ if (!cancelled) {
135
+ setIsFetching(false)
136
+ }
137
+ }
138
+ }
139
+
140
+ fetchResults()
141
+
142
+ return () => {
143
+ cancelled = true
144
+ }
145
+ }, [debouncedQuery, source, tableIdsKey, searchEndpoint])
146
+
147
+ // Derived loading state — single source of truth for "should the
148
+ // dropdown show 'Loading...' instead of 'No results found'":
149
+ const isLoading =
150
+ query.trim().length >= 2 && (query !== debouncedQuery || isFetching)
151
+
152
+ // Track whether dropdown should stay open (external link opened in new tab).
153
+ const [keepOpen, setKeepOpen] = useState(false)
154
+
155
+ const handleResultSelect = useCallback(
156
+ (
157
+ result: SearchResult,
158
+ modifiers?: {
159
+ metaKey?: boolean
160
+ ctrlKey?: boolean
161
+ shiftKey?: boolean
162
+ altKey?: boolean
163
+ button?: number
164
+ },
165
+ ) => {
166
+ const action = resolveSearchResultAction(
167
+ result,
168
+ source,
169
+ runtime?.navigation.mode,
170
+ )
171
+ // Modifier / non-primary mouse click → force new tab regardless of
172
+ // same-tab/new-tab decision. The dropdown row is a `<div>`, not an
173
+ // `<a target="_blank">`, so the browser doesn't background-tab
174
+ // natively on cmd-click. Honor it explicitly here for parity with
175
+ // the anchor-based surfaces (cards, chips, related-content). Plain
176
+ // Enter from the keyboard passes `modifiers === undefined`.
177
+ const wantsNewTab =
178
+ modifiers &&
179
+ (modifiers.metaKey ||
180
+ modifiers.ctrlKey ||
181
+ modifiers.shiftKey ||
182
+ modifiers.altKey ||
183
+ (typeof modifiers.button === 'number' && modifiers.button !== 0))
184
+ switch (action.kind) {
185
+ case 'navigate-same-tab': {
186
+ // Embed-mode short-circuit — autocomplete row clicked while
187
+ // the chat panel is hosted inside an embedding app.
188
+ if (runtime?.navigation.mode === 'embed') {
189
+ setKeepOpen(true)
190
+ const targetPlatform =
191
+ (result.metadata?.targetPlatform as string | null | undefined) ?? null
192
+ resolveExternalNavigation({
193
+ href: action.href,
194
+ targetPlatform,
195
+ runtime,
196
+ }).open()
197
+ return
198
+ }
199
+ if (wantsNewTab) {
200
+ setKeepOpen(true)
201
+ window.open(action.href, '_blank', NEW_TAB_FEATURES)
202
+ return
203
+ }
204
+ // Same-origin click:
205
+ // 1. If the href is under the current doc-tree's baseRoute AND
206
+ // an `onInPageSwap` callback is wired AND returns true →
207
+ // consider in-page swap handled.
208
+ // 2. Otherwise → embed-shim `router.push()` (soft RSC nav on
209
+ // Next.js hosts, window.location.assign on bare hosts).
210
+ setKeepOpen(false)
211
+ const path =
212
+ baseRoute && action.href.startsWith(`${baseRoute}/`)
213
+ ? action.href.slice(baseRoute.length + 1)
214
+ : null
215
+ if (path && onInPageSwap?.(path)) return
216
+ router.push(stripSameOriginToPath(action.href))
217
+ return
218
+ }
219
+ case 'navigate-new-tab':
220
+ // Cross-origin (e.g. clicking a flamingo.run release from
221
+ // product-hub) — open in a new tab. Keep dropdown open so the
222
+ // user can pick another result without re-searching.
223
+ setKeepOpen(true)
224
+ window.open(action.href, '_blank', NEW_TAB_FEATURES)
225
+ return
226
+ case 'ask-ai':
227
+ // Row is searchable-but-not-openable (cap_table positions,
228
+ // financial-kpi snapshots, anything backed by
229
+ // `resolveUrl: () => null`). Dispatch a CustomEvent that
230
+ // GlobalAskAI listens for — opens chat + drills via
231
+ // `entityIdFilter` (primary-key only, same as inline-card Ask).
232
+ setKeepOpen(false)
233
+ window.dispatchEvent(
234
+ new CustomEvent('ask-ai:open-with-ref', { detail: action.detail }),
235
+ )
236
+ return
237
+ case 'route':
238
+ // Final fallback: legacy navigation by path. Hits when a row
239
+ // has neither URL nor pk metadata — a mapper/API regression.
240
+ setKeepOpen(false)
241
+ onNavigate(action.path)
242
+ return
243
+ case 'noop':
244
+ return
245
+ }
246
+ },
247
+ [onNavigate, source, baseRoute, router, onInPageSwap, runtime],
248
+ )
249
+
250
+ // Reset keepOpen when query changes.
251
+ useEffect(() => {
252
+ setKeepOpen(false)
253
+ }, [query])
254
+
255
+ return {
256
+ query,
257
+ setQuery,
258
+ results,
259
+ isLoading,
260
+ handleResultSelect,
261
+ keepDropdownOpen: keepOpen,
262
+ }
263
+ }
@@ -55,6 +55,10 @@ export interface HelpCenterCardProps {
55
55
  onClose: TicketDetailDrawerProps['onClose']
56
56
  onReopen: TicketDetailDrawerProps['onReopen']
57
57
  onActionCollapsed: () => void
58
+ /** Persisted reply-failure banner — forwarded to the drawer. Parent
59
+ * (`HelpCenterList`) reads via `actions.replyErrorFor(external_id)`. */
60
+ replyError?: TicketDetailDrawerProps['replyError']
61
+ onClearReplyError?: TicketDetailDrawerProps['onClearReplyError']
58
62
  }
59
63
 
60
64
  export function HelpCenterCard({
@@ -67,6 +71,8 @@ export function HelpCenterCard({
67
71
  onClose,
68
72
  onReopen,
69
73
  onActionCollapsed,
74
+ replyError,
75
+ onClearReplyError,
70
76
  }: HelpCenterCardProps) {
71
77
  const optimistic = isOptimistic(ticket)
72
78
  const rawStatus = (ticket.status ?? 'OPEN').toUpperCase()
@@ -177,6 +183,8 @@ export function HelpCenterCard({
177
183
  onClose={onClose}
178
184
  onReopen={onReopen}
179
185
  onActionCollapsed={onActionCollapsed}
186
+ replyError={replyError}
187
+ onClearReplyError={onClearReplyError}
180
188
  />
181
189
  </div>
182
190
  )}
@@ -40,7 +40,7 @@ import { useTicketsList } from './hooks/use-tickets-list'
40
40
  import { useTicketActions } from './hooks/use-ticket-actions'
41
41
  import { HelpCenterCard } from './help-center-card'
42
42
  import { HelpCenterCreateForm, HelpCenterCreateFormSkeleton } from './help-center-create-form'
43
- import type { AnyTicket, OptimisticTicket, TicketData } from './types'
43
+ import type { AnyTicket, OptimisticTicket, TicketsCacheSlot } from './types'
44
44
  import { isOptimistic } from './types'
45
45
 
46
46
  export interface HelpCenterListProps {
@@ -175,9 +175,21 @@ function HelpCenterListAuthed({
175
175
  // Every cache slot under the ['tickets'] prefix — the queryKey
176
176
  // includes search + status + page + pageSize segments so a bare
177
177
  // write would miss most slots.
178
- queryClient.setQueriesData<TicketData[] | undefined>(
178
+ //
179
+ // Cache slot is `TicketsCacheSlot` (`{ tickets, count, … }`), NOT
180
+ // a bare `TicketData[]`. The previous version called `.filter()`
181
+ // directly on the object — silently crashing only on the rare
182
+ // TICKET_NOT_FOUND path; the prod regression that landed
183
+ // 2026-05-29 surfaced the same shape mismatch in the
184
+ // close/reopen optimistic-update path. Project, filter, reassemble.
185
+ queryClient.setQueriesData<TicketsCacheSlot | undefined>(
179
186
  { queryKey: ['tickets'] },
180
- (prev) => (prev ?? []).filter((t) => t.id !== ticketId),
187
+ (prev) => {
188
+ if (!prev || !Array.isArray(prev.tickets)) return prev
189
+ const nextTickets = prev.tickets.filter((t) => t.id !== ticketId)
190
+ if (nextTickets.length === prev.tickets.length) return prev
191
+ return { ...prev, tickets: nextTickets }
192
+ },
181
193
  )
182
194
  setExpandedTicketId((prev) => (prev === ticketId ? null : prev))
183
195
  },
@@ -285,6 +297,8 @@ function HelpCenterListAuthed({
285
297
  onClose={actions.closeTicket}
286
298
  onReopen={actions.reopenTicket}
287
299
  onActionCollapsed={() => setExpandedTicketId(null)}
300
+ replyError={actions.replyErrorFor(ticket.external_id)}
301
+ onClearReplyError={() => actions.clearReplyError(ticket.external_id)}
288
302
  />
289
303
  ))}
290
304
  </div>