@flamingo-stack/openframe-frontend-core 0.0.508 → 0.0.509

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 (306) hide show
  1. package/dist/chat-protocol/decode.d.ts +58 -0
  2. package/dist/chat-protocol/decode.d.ts.map +1 -0
  3. package/dist/chat-protocol/encode.d.ts +44 -0
  4. package/dist/chat-protocol/encode.d.ts.map +1 -0
  5. package/dist/chat-protocol/env-flag.d.ts +35 -0
  6. package/dist/chat-protocol/env-flag.d.ts.map +1 -0
  7. package/dist/chat-protocol/events.d.ts +216 -0
  8. package/dist/chat-protocol/events.d.ts.map +1 -0
  9. package/dist/chat-protocol/frames.d.ts +213 -0
  10. package/dist/chat-protocol/frames.d.ts.map +1 -0
  11. package/dist/chat-protocol/index.cjs +506 -0
  12. package/dist/chat-protocol/index.cjs.map +1 -0
  13. package/dist/chat-protocol/index.d.ts +18 -0
  14. package/dist/chat-protocol/index.d.ts.map +1 -0
  15. package/dist/chat-protocol/index.js +490 -0
  16. package/dist/chat-protocol/index.js.map +1 -0
  17. package/dist/chat-protocol/ip-normalize.d.ts +44 -0
  18. package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
  19. package/dist/chat-protocol/nats-decoder.d.ts +26 -0
  20. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
  21. package/dist/{chunk-3SQ5KXHQ.cjs → chunk-2T4GTV25.cjs} +9 -9
  22. package/dist/{chunk-3SQ5KXHQ.cjs.map → chunk-2T4GTV25.cjs.map} +1 -1
  23. package/dist/{chunk-CWAOV2FK.cjs → chunk-3CGZPAGR.cjs} +3 -3
  24. package/dist/{chunk-CWAOV2FK.cjs.map → chunk-3CGZPAGR.cjs.map} +1 -1
  25. package/dist/{chunk-TLMJHMXJ.js → chunk-3QSR22IC.js} +12 -12
  26. package/dist/chunk-3QSR22IC.js.map +1 -0
  27. package/dist/{chunk-TWYNR4TZ.cjs → chunk-4G5TLHJD.cjs} +7141 -4799
  28. package/dist/chunk-4G5TLHJD.cjs.map +1 -0
  29. package/dist/{chunk-ADPMHWOE.js → chunk-72XAID7Y.js} +4 -4
  30. package/dist/{chunk-PAGKRNWK.js → chunk-7WZHBQ4J.js} +2 -2
  31. package/dist/{chunk-OCEKO5CW.js → chunk-AEELJC4N.js} +12106 -9764
  32. package/dist/chunk-AEELJC4N.js.map +1 -0
  33. package/dist/{chunk-Z42BGM6Q.cjs → chunk-BT2WWDFP.cjs} +5 -5
  34. package/dist/{chunk-Z42BGM6Q.cjs.map → chunk-BT2WWDFP.cjs.map} +1 -1
  35. package/dist/{chunk-7LEL3RIX.cjs → chunk-C7OY3PQP.cjs} +11 -11
  36. package/dist/{chunk-7LEL3RIX.cjs.map → chunk-C7OY3PQP.cjs.map} +1 -1
  37. package/dist/{chunk-RHGGEKPQ.cjs → chunk-CIOLOQKQ.cjs} +4 -4
  38. package/dist/{chunk-RHGGEKPQ.cjs.map → chunk-CIOLOQKQ.cjs.map} +1 -1
  39. package/dist/{chunk-TSZHM74B.js → chunk-DC2TKS7C.js} +2 -2
  40. package/dist/{chunk-M2GOR3XQ.cjs → chunk-DYPYR7FR.cjs} +87 -87
  41. package/dist/{chunk-M2GOR3XQ.cjs.map → chunk-DYPYR7FR.cjs.map} +1 -1
  42. package/dist/{chunk-I64ABCDX.js → chunk-I2B6X77L.js} +2 -2
  43. package/dist/{chunk-NIZDKTGL.cjs → chunk-K5MSCMDK.cjs} +37 -37
  44. package/dist/{chunk-NIZDKTGL.cjs.map → chunk-K5MSCMDK.cjs.map} +1 -1
  45. package/dist/{chunk-DTYRYB2N.js → chunk-K6QTZ7CZ.js} +2 -2
  46. package/dist/{chunk-4LTMXDUS.js → chunk-KBN7PMFT.js} +2 -2
  47. package/dist/{chunk-TBMV7I5N.js → chunk-KXZO2WZX.js} +2 -2
  48. package/dist/{chunk-QTYZMP6D.cjs → chunk-LBAKLT6T.cjs} +62 -62
  49. package/dist/chunk-LBAKLT6T.cjs.map +1 -0
  50. package/dist/{chunk-KEBYLU3U.js → chunk-LFUPI7VX.js} +2 -2
  51. package/dist/{chunk-LDTR4IVZ.cjs → chunk-M37PBG4N.cjs} +31 -31
  52. package/dist/{chunk-LDTR4IVZ.cjs.map → chunk-M37PBG4N.cjs.map} +1 -1
  53. package/dist/{chunk-WA7RR64F.js → chunk-PO5O654H.js} +6 -5
  54. package/dist/chunk-PO5O654H.js.map +1 -0
  55. package/dist/{chunk-WK4N5VBX.cjs → chunk-QJJISLPA.cjs} +28 -27
  56. package/dist/chunk-QJJISLPA.cjs.map +1 -0
  57. package/dist/{chunk-2F44PLTV.cjs → chunk-T7MCSA55.cjs} +26 -26
  58. package/dist/{chunk-2F44PLTV.cjs.map → chunk-T7MCSA55.cjs.map} +1 -1
  59. package/dist/{chunk-WJQHJD7J.js → chunk-TWSKTJHW.js} +2 -2
  60. package/dist/{chunk-ZSQHZYCO.cjs → chunk-UQDMJ6DH.cjs} +13 -13
  61. package/dist/{chunk-ZSQHZYCO.cjs.map → chunk-UQDMJ6DH.cjs.map} +1 -1
  62. package/dist/{chunk-TZRUCD56.js → chunk-V3ZAAFE3.js} +5 -5
  63. package/dist/{chunk-LRGHJPET.js → chunk-XGIFOHKE.js} +2 -2
  64. package/dist/{chunk-POOMO3PA.cjs → chunk-YQEPRYT5.cjs} +7 -7
  65. package/dist/{chunk-POOMO3PA.cjs.map → chunk-YQEPRYT5.cjs.map} +1 -1
  66. package/dist/components/case-studies/index.cjs +8 -8
  67. package/dist/components/case-studies/index.js +2 -2
  68. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  69. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  70. package/dist/components/chat/hooks/index.d.ts +0 -1
  71. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  72. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  73. package/dist/components/chat/hooks/use-chunk-catchup.d.ts.map +1 -1
  74. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
  75. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  76. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -57
  77. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  78. package/dist/components/chat/index.cjs +10 -2
  79. package/dist/components/chat/index.cjs.map +1 -1
  80. package/dist/components/chat/index.d.ts +1 -0
  81. package/dist/components/chat/index.d.ts.map +1 -1
  82. package/dist/components/chat/index.js +21 -13
  83. package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
  84. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
  85. package/dist/components/chat/stream/chat-stream-reducer.d.ts +380 -0
  86. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
  87. package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
  88. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
  89. package/dist/components/chat/stream/index.d.ts +17 -0
  90. package/dist/components/chat/stream/index.d.ts.map +1 -0
  91. package/dist/components/chat/stream/message-mutations.d.ts +77 -0
  92. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
  93. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
  94. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
  95. package/dist/components/chat/types/api.types.d.ts +1 -81
  96. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  97. package/dist/components/chat/types/processing.types.d.ts +2 -93
  98. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  99. package/dist/components/chat/types/unified-chat-state.types.d.ts +15 -0
  100. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  101. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
  102. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  103. package/dist/components/chat/utils/history-merge.d.ts.map +1 -1
  104. package/dist/components/chat/utils/index.d.ts +2 -3
  105. package/dist/components/chat/utils/index.d.ts.map +1 -1
  106. package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
  107. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  108. package/dist/components/contact/index.cjs +3 -3
  109. package/dist/components/contact/index.js +2 -2
  110. package/dist/components/docs/index.cjs +5 -5
  111. package/dist/components/docs/index.js +4 -4
  112. package/dist/components/embeds/index.cjs +3 -3
  113. package/dist/components/embeds/index.js +2 -2
  114. package/dist/components/faq/index.cjs +3 -3
  115. package/dist/components/faq/index.js +2 -2
  116. package/dist/components/features/index.cjs +2 -2
  117. package/dist/components/features/index.js +1 -1
  118. package/dist/components/help-center-pages/index.cjs +22 -22
  119. package/dist/components/help-center-pages/index.js +13 -13
  120. package/dist/components/index.cjs +174 -132
  121. package/dist/components/index.cjs.map +1 -1
  122. package/dist/components/index.js +65 -23
  123. package/dist/components/index.js.map +1 -1
  124. package/dist/components/meeting-scheduler/index.cjs +34 -34
  125. package/dist/components/meeting-scheduler/index.js +3 -3
  126. package/dist/components/navigation/index.cjs +2 -2
  127. package/dist/components/navigation/index.js +1 -1
  128. package/dist/components/onboarding-guides/index.cjs +5 -5
  129. package/dist/components/onboarding-guides/index.js +4 -4
  130. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  131. package/dist/components/related-content/index.cjs +3 -3
  132. package/dist/components/related-content/index.js +2 -2
  133. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  134. package/dist/components/tickets/index.cjs +6 -6
  135. package/dist/components/tickets/index.js +5 -5
  136. package/dist/components/ui/index.cjs +44 -2
  137. package/dist/components/ui/index.cjs.map +1 -1
  138. package/dist/components/ui/index.d.ts +1 -2
  139. package/dist/components/ui/index.d.ts.map +1 -1
  140. package/dist/components/ui/index.js +55 -13
  141. package/dist/components/ui/markdown/base-components.d.ts +51 -0
  142. package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
  143. package/dist/components/ui/markdown/engine.d.ts +99 -0
  144. package/dist/components/ui/markdown/engine.d.ts.map +1 -0
  145. package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
  146. package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
  147. package/dist/components/ui/markdown/index.d.ts +18 -0
  148. package/dist/components/ui/markdown/index.d.ts.map +1 -0
  149. package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
  150. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
  151. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
  152. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
  153. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
  154. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
  155. package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
  156. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
  157. package/dist/components/ui/markdown/sanitize.d.ts +153 -0
  158. package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
  159. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
  160. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
  161. package/dist/components/ui/markdown/streaming.d.ts +78 -0
  162. package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
  163. package/dist/components/ui/markdown/text-size.d.ts +34 -0
  164. package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
  165. package/dist/index.cjs +60 -2
  166. package/dist/index.cjs.map +1 -1
  167. package/dist/index.js +71 -13
  168. package/dist/utils/index.cjs +169 -40
  169. package/dist/utils/index.cjs.map +1 -1
  170. package/dist/utils/index.d.ts +1 -0
  171. package/dist/utils/index.d.ts.map +1 -1
  172. package/dist/utils/index.js +162 -41
  173. package/dist/utils/index.js.map +1 -1
  174. package/dist/utils/markdown-fences.d.ts +42 -0
  175. package/dist/utils/markdown-fences.d.ts.map +1 -0
  176. package/dist/utils/markdown-heading-id.d.ts +85 -0
  177. package/dist/utils/markdown-heading-id.d.ts.map +1 -0
  178. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  179. package/package.json +7 -1
  180. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +319 -0
  181. package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
  182. package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
  183. package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
  184. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +237 -0
  185. package/src/chat-protocol/decode.ts +278 -0
  186. package/src/chat-protocol/encode.ts +71 -0
  187. package/src/chat-protocol/env-flag.ts +39 -0
  188. package/src/chat-protocol/events.ts +254 -0
  189. package/src/chat-protocol/frames.ts +245 -0
  190. package/src/chat-protocol/index.ts +21 -0
  191. package/src/chat-protocol/ip-normalize.ts +146 -0
  192. package/src/chat-protocol/nats-decoder.ts +264 -0
  193. package/src/components/chat/.chat-message-list.md +10 -6
  194. package/src/components/chat/__tests__/chat-message-list.test.tsx +450 -9
  195. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
  196. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +250 -0
  197. package/src/components/chat/chat-message-enhanced.tsx +122 -20
  198. package/src/components/chat/chat-message-list.tsx +515 -133
  199. package/src/components/chat/embeddable-chat.tsx +6 -0
  200. package/src/components/chat/hooks/.index.md +30 -33
  201. package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
  202. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
  203. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  204. package/src/components/chat/hooks/__tests__/chunk-catchup-dialog-staleness.test.ts +92 -0
  205. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
  206. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
  207. package/src/components/chat/hooks/index.ts +0 -1
  208. package/src/components/chat/hooks/use-chat.ts +8 -0
  209. package/src/components/chat/hooks/use-chunk-catchup.ts +26 -1
  210. package/src/components/chat/hooks/use-nats-chat-adapter.ts +254 -825
  211. package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
  212. package/src/components/chat/index.ts +5 -0
  213. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +944 -0
  214. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +806 -0
  215. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +467 -0
  216. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +1330 -0
  217. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
  218. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +105 -0
  219. package/src/components/chat/stream/chat-dialog-store.ts +536 -0
  220. package/src/components/chat/stream/chat-stream-reducer.ts +1928 -0
  221. package/src/components/chat/stream/delta-batcher.ts +168 -0
  222. package/src/components/chat/stream/index.ts +55 -0
  223. package/src/components/chat/stream/message-mutations.ts +340 -0
  224. package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
  225. package/src/components/chat/thinking-display.tsx +1 -1
  226. package/src/components/chat/types/.api.types.md +39 -49
  227. package/src/components/chat/types/.processing.types.md +29 -58
  228. package/src/components/chat/types/.unified-chat-state.types.md +2 -2
  229. package/src/components/chat/types/api.types.ts +6 -73
  230. package/src/components/chat/types/processing.types.ts +11 -53
  231. package/src/components/chat/types/unified-chat-state.types.ts +16 -0
  232. package/src/components/chat/utils/.extract-incomplete-message-state.md +21 -3
  233. package/src/components/chat/utils/.history-merge.md +44 -3
  234. package/src/components/chat/utils/.index.md +31 -40
  235. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
  236. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
  237. package/src/components/chat/utils/__tests__/extract-incomplete-message-state.test.ts +106 -0
  238. package/src/components/chat/utils/__tests__/history-merge.test.ts +160 -0
  239. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +61 -0
  240. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
  241. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
  242. package/src/components/chat/utils/extract-incomplete-message-state.ts +93 -6
  243. package/src/components/chat/utils/history-merge.ts +85 -5
  244. package/src/components/chat/utils/index.ts +5 -8
  245. package/src/components/chat/utils/process-historical-messages.ts +361 -385
  246. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
  247. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  248. package/src/components/shared/product-release/release-detail-page.tsx +19 -4
  249. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
  250. package/src/components/ui/__tests__/markdown-parity.test.tsx +2540 -0
  251. package/src/components/ui/index.ts +1 -2
  252. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +176 -0
  253. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +151 -0
  254. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +354 -0
  255. package/src/components/ui/markdown/__tests__/sanitize-render.test.tsx +118 -0
  256. package/src/components/ui/markdown/__tests__/streaming.test.tsx +436 -0
  257. package/src/components/ui/markdown/base-components.tsx +360 -0
  258. package/src/components/ui/markdown/engine.tsx +315 -0
  259. package/src/components/ui/markdown/heading-ids.ts +239 -0
  260. package/src/components/ui/markdown/index.ts +49 -0
  261. package/src/components/ui/markdown/mermaid-diagram.tsx +323 -0
  262. package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
  263. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
  264. package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
  265. package/src/components/ui/markdown/sanitize.ts +3584 -0
  266. package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
  267. package/src/components/ui/markdown/streaming.ts +390 -0
  268. package/src/components/ui/markdown/text-size.ts +106 -0
  269. package/src/components/ui/release-changelog-section.tsx +1 -1
  270. package/src/components/ui/ticket-info-section.tsx +1 -1
  271. package/src/utils/index.ts +1 -0
  272. package/src/utils/markdown-fences.ts +127 -0
  273. package/src/utils/markdown-heading-id.ts +348 -0
  274. package/src/utils/markdown-section-extractor.ts +41 -56
  275. package/dist/chunk-OCEKO5CW.js.map +0 -1
  276. package/dist/chunk-QTYZMP6D.cjs.map +0 -1
  277. package/dist/chunk-TLMJHMXJ.js.map +0 -1
  278. package/dist/chunk-TWYNR4TZ.cjs.map +0 -1
  279. package/dist/chunk-WA7RR64F.js.map +0 -1
  280. package/dist/chunk-WK4N5VBX.cjs.map +0 -1
  281. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
  282. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
  283. package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
  284. package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
  285. package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
  286. package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
  287. package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
  288. package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
  289. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
  290. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -489
  291. package/src/components/chat/utils/.chunk-parser.md +0 -62
  292. package/src/components/chat/utils/chunk-parser.ts +0 -262
  293. package/src/components/ui/.simple-markdown-renderer.md +0 -52
  294. package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
  295. package/src/components/ui/simple-markdown-renderer.tsx +0 -964
  296. /package/dist/{chunk-ADPMHWOE.js.map → chunk-72XAID7Y.js.map} +0 -0
  297. /package/dist/{chunk-PAGKRNWK.js.map → chunk-7WZHBQ4J.js.map} +0 -0
  298. /package/dist/{chunk-TSZHM74B.js.map → chunk-DC2TKS7C.js.map} +0 -0
  299. /package/dist/{chunk-I64ABCDX.js.map → chunk-I2B6X77L.js.map} +0 -0
  300. /package/dist/{chunk-DTYRYB2N.js.map → chunk-K6QTZ7CZ.js.map} +0 -0
  301. /package/dist/{chunk-4LTMXDUS.js.map → chunk-KBN7PMFT.js.map} +0 -0
  302. /package/dist/{chunk-TBMV7I5N.js.map → chunk-KXZO2WZX.js.map} +0 -0
  303. /package/dist/{chunk-KEBYLU3U.js.map → chunk-LFUPI7VX.js.map} +0 -0
  304. /package/dist/{chunk-WJQHJD7J.js.map → chunk-TWSKTJHW.js.map} +0 -0
  305. /package/dist/{chunk-TZRUCD56.js.map → chunk-V3ZAAFE3.js.map} +0 -0
  306. /package/dist/{chunk-LRGHJPET.js.map → chunk-XGIFOHKE.js.map} +0 -0
@@ -1,964 +0,0 @@
1
- "use client";
2
-
3
- import React, { useEffect, useState, useRef, useMemo, useCallback, memo } from 'react';
4
- import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown';
5
- import type { PluggableList } from 'unified';
6
- import remarkGfm from 'remark-gfm';
7
- import remarkBreaks from 'remark-breaks';
8
- import rehypeHighlight from 'rehype-highlight';
9
- import rehypeRaw from 'rehype-raw';
10
- import { visit } from 'unist-util-visit';
11
- import Image from '../../embed-shims/next-image';
12
- import { useAuthedImageSrc } from '../../hooks/use-authed-image-src';
13
- import { AlertCircleIcon } from '../icons-v2-generated';
14
- import { cn } from '../../utils/cn';
15
- import type { ResolveLinkResult } from '../../types/doc-source';
16
-
17
- export type { ResolveLinkResult };
18
-
19
- // ---------------------------------------------------------------------------
20
- // rehype HAST sanitizer — runs AFTER rehype-raw to strip XSS vectors
21
- // ---------------------------------------------------------------------------
22
- /**
23
- * Minimal HAST sanitizer. Runs AFTER `rehype-raw` (which parses raw HTML
24
- * embedded in markdown) and BEFORE `rehype-highlight`. Strips the
25
- * attack surfaces that rehype-raw leaves wide open:
26
- *
27
- * - `on*` event handlers (onerror, onload, onclick, …) on ANY element
28
- * - href / src / formaction / xlink:href / poster pointing at
29
- * `javascript:` (case + whitespace tolerant)
30
- * - `iframe srcdoc` (full-document XSS)
31
- * - `<script>`, `<style>`, `<noscript>`, `<noembed>` elements (drop)
32
- * - `data:` URIs on src-ish attrs (SVG-with-embedded-JS class of bug)
33
- *
34
- * Why custom (vs `rehype-sanitize`): the OSS-lib build environment
35
- * doesn't have `rehype-sanitize` in its `node_modules` (sandbox
36
- * restriction), but `unist-util-visit` is already a transitive dep of
37
- * `rehype-raw`. This plugin is ~60 lines, ships nothing new, and is
38
- * tighter than the default sanitize schema for our threat model
39
- * (we want LLM-emitted markdown to be safe; we don't need full HTML5
40
- * fidelity).
41
- *
42
- * The text-level `escapeUnknownHtmlTags` pre-pass below is still useful
43
- * for catching `<their>`-style accidental tag emissions that React 19
44
- * rejects, but it is NOT a security boundary. THIS is.
45
- */
46
- const EVENT_HANDLER_ATTR_RE = /^on[a-z]+$/i
47
- const JAVASCRIPT_URL_RE = /^[\s\x00-\x1f]*javascript:/i
48
- const DATA_URL_RE = /^[\s\x00-\x1f]*data:/i
49
- const URL_ATTRS = new Set([
50
- 'href',
51
- 'src',
52
- // `srcset` accepts `javascript:` on legacy browsers — same guard
53
- // as the canonical hast-util-sanitize default schema's attr set.
54
- // Multi-candidate scanning lives in `srcsetHasUnsafeCandidate` below;
55
- // a single-URL check would miss a malicious second candidate like
56
- // `"https://safe.png 1x, javascript:alert(1) 2x"`.
57
- 'srcset',
58
- 'formaction',
59
- 'xlink:href',
60
- 'poster',
61
- 'data',
62
- 'action',
63
- 'background',
64
- ])
65
-
66
- /**
67
- * Returns true if any candidate in an `srcset` attribute has a dangerous
68
- * URL scheme. Per the HTML spec, srcset is a comma-separated list of
69
- * candidates; each candidate is `<url> <descriptor>?` (e.g.
70
- * `"https://x/a.png 2x"`). The single-URL `JAVASCRIPT_URL_RE.test(v)`
71
- * check only inspects the FIRST candidate — so a malicious second
72
- * candidate (`"https://safe 1x, javascript:alert(1) 2x"`) would slip
73
- * through. This helper splits on `,`, trims each candidate, takes the
74
- * leading whitespace-delimited token (the URL), and tests that.
75
- *
76
- * Splitting on every comma over-matches the rare-in-practice case of
77
- * commas inside URL paths. That's the correct error bias for a
78
- * sanitizer: over-strip is safe, under-strip is not.
79
- */
80
- function srcsetHasUnsafeCandidate(srcset: string): boolean {
81
- for (const candidate of srcset.split(',')) {
82
- const url = candidate.trim().split(/\s+/)[0] ?? ''
83
- if (JAVASCRIPT_URL_RE.test(url) || DATA_URL_RE.test(url)) return true
84
- }
85
- return false
86
- }
87
- // Element-level drop list. The text-pre-pass `escapeUnknownHtmlTags`
88
- // already escapes any `<tag>` whose name isn't on its allow-list, so
89
- // `<object>`, `<embed>`, `<applet>`, `<base>`, `<meta>` normally never
90
- // reach `rehype-raw` as elements. Re-stripping at the HAST layer
91
- // removes the cross-layer dependency: this sanitizer is sufficient
92
- // on its own even if the text pre-pass is bypassed.
93
- const STRIP_ELEMENTS = new Set([
94
- 'script',
95
- 'style',
96
- 'noscript',
97
- 'noembed',
98
- 'object',
99
- 'embed',
100
- 'applet',
101
- 'base',
102
- 'meta',
103
- ])
104
-
105
- function rehypeStripUnsafe() {
106
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
- return (tree: any) => {
108
- visit(tree, 'element', (node: any, index: number | undefined, parent: any) => {
109
- const tag = String(node.tagName ?? '').toLowerCase()
110
- if (STRIP_ELEMENTS.has(tag)) {
111
- if (parent && typeof index === 'number') {
112
- parent.children.splice(index, 1)
113
- // Return the numeric index (`unist-util-visit` treats it as
114
- // `[CONTINUE, index]`) so the walker resumes at the slot the
115
- // removed node vacated. Don't return `[SKIP, index]`:
116
- // skip-descendants is meaningless for a node we just removed,
117
- // and SKIP+index conflates two signals.
118
- return index
119
- }
120
- // Root-level strip element (parent undefined). Rare, but
121
- // possible if `rehype-raw` ever lifts one. Neutralize in
122
- // place by clearing children + retagging as a blank span.
123
- node.children = []
124
- node.tagName = 'span'
125
- node.properties = {}
126
- return
127
- }
128
- if (!node.properties || typeof node.properties !== 'object') return
129
- for (const key of Object.keys(node.properties)) {
130
- // 1. event handlers
131
- if (EVENT_HANDLER_ATTR_RE.test(key)) {
132
- delete node.properties[key]
133
- continue
134
- }
135
- // 2. dangerous URL schemes on URL-bearing attrs. `srcset` is
136
- // multi-candidate so it routes through `srcsetHasUnsafeCandidate`
137
- // which inspects every URL in the list (not just the first).
138
- if (URL_ATTRS.has(key.toLowerCase())) {
139
- const raw = node.properties[key]
140
- const v = Array.isArray(raw) ? raw[0] : raw
141
- if (typeof v === 'string') {
142
- const unsafe =
143
- key.toLowerCase() === 'srcset'
144
- ? srcsetHasUnsafeCandidate(v)
145
- : JAVASCRIPT_URL_RE.test(v) || DATA_URL_RE.test(v)
146
- if (unsafe) {
147
- delete node.properties[key]
148
- continue
149
- }
150
- }
151
- }
152
- // 3. iframe srcdoc — full-document XSS vector
153
- if (tag === 'iframe' && key.toLowerCase() === 'srcdoc') {
154
- delete node.properties[key]
155
- }
156
- }
157
- })
158
- }
159
- }
160
-
161
- /**
162
- * URL transformer that extends react-markdown's default safe-protocol
163
- * allowlist with the two internal schemes the chat remark plugins emit:
164
- * - `card://` — `remarkCardLinks`, for inline chat-card markers.
165
- * - `mention://` — `remarkMentionChips`, for inline `@marker:id` AI mentions.
166
- *
167
- * Without this, react-markdown 10's `defaultUrlTransform` strips the URL
168
- * to `""` before the `<a>` component override runs (the override's
169
- * `href.startsWith('card://')` / `'mention://'` check then fails and the
170
- * marker leaks through — as literal text, or as an empty-href link that the
171
- * host's `NavLinkAnchor` resolves to a base URL). All other URL schemes still
172
- * go through the default sanitizer — `javascript:`, `vbscript:`, `data:`
173
- * (non-image), etc. remain blocked.
174
- *
175
- * Scope: both schemes are allowed ONLY for `href` attributes. If the LLM
176
- * accidentally emits `![alt](card://type:id)` the URL still goes through
177
- * `defaultUrlTransform` (which strips it to `""`) so an `<img src="card://...">`
178
- * never reaches the DOM — broken request avoided. Per v6.1 §B.2.4: these
179
- * schemes are internal — never network-fetched, never written to attributes
180
- * other than `href` for renderer dispatch.
181
- */
182
- function cardAwareUrlTransform(url: string, key: string): string {
183
- if (key === 'href' && typeof url === 'string' && (url.startsWith('card://') || url.startsWith('mention://')))
184
- return url;
185
- return defaultUrlTransform(url);
186
- }
187
-
188
- // ---------------------------------------------------------------------------
189
- // LLM-output sanitizer — escape unknown HTML-style tags
190
- // ---------------------------------------------------------------------------
191
- /**
192
- * Tags `rehype-raw` is allowed to forward to React as-is. Anything
193
- * outside this set gets its angle brackets escaped so it renders as
194
- * plain text rather than reaching React as an unrecognized custom
195
- * element.
196
- *
197
- * Why this matters: chat output is LLM-generated markdown. An LLM that
198
- * accidentally wraps a word in angle brackets ("share <their> settings")
199
- * or echoes a system-prompt XML tag ("the <ticket> element above")
200
- * makes `rehype-raw` mint a `<their>` / `<ticket>` JSX element. React
201
- * 18 logs "tag is unrecognized in this browser" for every unknown tag;
202
- * React 19 throws on tags with reserved kebab-case forms. We pre-escape
203
- * to keep the renderer pristine without losing legitimate inline HTML
204
- * (details/summary, iframe, kbd, mark, etc.). `video` is deliberately
205
- * NOT allow-listed: chat strips <video> tags server-side, and all video
206
- * playback goes through the <Video> SSOT — a stray tag renders as
207
- * escaped literal text (visible + debuggable) rather than a native
208
- * player outside the unified pipeline.
209
- *
210
- * The allow-list mirrors HTML5 + the elements the chat shell wires
211
- * component overrides for. Kept lower-case; matched case-insensitively.
212
- */
213
- const SAFE_HTML_TAGS = new Set([
214
- // Block + inline text
215
- 'a', 'abbr', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'blockquote',
216
- 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'dd', 'del',
217
- 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer',
218
- 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'ins',
219
- 'kbd', 'li', 'main', 'mark', 'nav', 'ol', 'p', 'pre', 'q', 'rp', 'rt',
220
- 'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary',
221
- 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'u',
222
- 'ul', 'var', 'wbr',
223
- // Media ('video' intentionally excluded — see the header comment)
224
- 'img', 'picture', 'source', 'audio', 'iframe', 'track',
225
- // Forms (rehype-raw allows them; mostly harmless for chat output)
226
- 'button', 'input', 'label', 'select', 'option', 'optgroup', 'textarea', 'form', 'fieldset', 'legend',
227
- ])
228
-
229
- /**
230
- * Match an opening / closing HTML tag in markdown source. Captures:
231
- * 1 — optional `/` for closing tags
232
- * 2 — tag name (must start with a letter)
233
- * 3 — everything between the name and the closing `>` (attrs etc.)
234
- * 4 — optional `/` for void-element self-close
235
- */
236
- // ReDoS-safe shape (CodeQL polynomial-regex hardening):
237
- //
238
- // Every quantifier is hard-bounded so the engine has no possible
239
- // polynomial path. The original `[a-zA-Z0-9-]*` + `[^>]*?` admitted
240
- // O(n²) backtracking on `<A-----…` because both classes include `-`;
241
- // CodeQL flagged that as a polynomial regex. Bounding the tag-name
242
- // at 63 chars (covers every real HTML tag — `animateTransform` at 16
243
- // is the longest in practice) and the rest at 4096 chars (sane upper
244
- // bound for `<a href="…">` attribute strings) means matching is
245
- // constant-time per tag, NOT proportional to input length.
246
- //
247
- // Lazy `{0,4096}?` is deliberately lazy (not greedy) so the trailing
248
- // `(\/?)>` still captures the self-close `/` — greedy would swallow
249
- // the slash into the rest. Bounded lazy backtracking is constant-time
250
- // per match (capped at 4096 expansions), so the laziness is safe.
251
- //
252
- // Anything longer than 4159 chars per tag simply doesn't match —
253
- // falls through as plain text, the safe-degrade behavior for
254
- // HTML-in-markdown.
255
- //
256
- // Tag shapes still covered: `<a>`, `<a/>`, `<a class="x">`,
257
- // `<a class="x" />`, `</a>`, `<input type="text" />`,
258
- // `<custom-element data-foo="bar">`, `<animateTransform>`, etc.
259
- const TAG_LIKE_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9-]{0,63})((?:\s[^>]{0,4096}?)?)(\/?)>/g
260
-
261
- function escapeUnknownHtmlTags(text: string): string {
262
- if (!text || text.indexOf('<') === -1) return text
263
- // Carve out fenced code blocks AND inline-backtick spans so we don't
264
- // corrupt `<their>` examples that legitimately live inside code. The
265
- // regex matches in priority order: triple-fence first (greediest),
266
- // then inline single-backtick. Each protected span is preserved
267
- // verbatim; everything between/around them runs through
268
- // `escapeOutsideFences`.
269
- const parts: string[] = []
270
- let cursor = 0
271
- // Triple-fence ``` … ``` OR single-backtick `…` (one line, no
272
- // embedded newlines). Backtick group is non-greedy and forbids
273
- // inner newlines per CommonMark inline-code semantics.
274
- const PROTECTED_SPAN_RE = /```[\s\S]*?```|`[^`\n]+`/g
275
- let span: RegExpExecArray | null
276
- while ((span = PROTECTED_SPAN_RE.exec(text)) !== null) {
277
- if (span.index > cursor) {
278
- parts.push(escapeOutsideFences(text.slice(cursor, span.index)))
279
- }
280
- parts.push(span[0])
281
- cursor = span.index + span[0].length
282
- }
283
- if (cursor < text.length) {
284
- parts.push(escapeOutsideFences(text.slice(cursor)))
285
- }
286
- return parts.join('')
287
- }
288
-
289
- function escapeOutsideFences(segment: string): string {
290
- return segment.replace(TAG_LIKE_REGEX, (match, slash, tag, rest, selfClose) => {
291
- const lower = (tag as string).toLowerCase()
292
- if (SAFE_HTML_TAGS.has(lower)) return match
293
- // Unknown tag — escape so it renders as text instead of reaching
294
- // React as a custom element.
295
- return `&lt;${slash}${tag}${rest}${selfClose}&gt;`
296
- })
297
- }
298
-
299
- // ---------------------------------------------------------------------------
300
- // Mermaid styles (responsive)
301
- // ---------------------------------------------------------------------------
302
- const mermaidStyles = `
303
- .mermaid-svg-container svg {
304
- max-width: 100% !important;
305
- height: auto !important;
306
- min-height: 200px;
307
- font-family: 'DM Sans', sans-serif !important;
308
- font-size: 14px !important;
309
- }
310
- @media (min-width: 1520px) {
311
- .mermaid-svg-container svg {
312
- max-width: 900px !important;
313
- max-height: 700px !important;
314
- min-height: 300px;
315
- font-size: 16px !important;
316
- }
317
- }
318
- @media (min-width: 768px) and (max-width: 1519px) {
319
- .mermaid-svg-container svg {
320
- max-width: 700px !important;
321
- max-height: 600px !important;
322
- min-height: 250px;
323
- font-size: 15px !important;
324
- }
325
- }
326
- @media (max-width: 767px) {
327
- .mermaid-svg-container svg {
328
- max-width: 90vw !important;
329
- max-height: 400px !important;
330
- min-height: 200px;
331
- font-size: 13px !important;
332
- }
333
- }
334
- .mermaid-svg-container svg[width] { width: 100% !important; }
335
- .mermaid-svg-container .node rect,
336
- .mermaid-svg-container .node circle,
337
- .mermaid-svg-container .node ellipse,
338
- .mermaid-svg-container .node polygon { stroke-width: 2px !important; }
339
- .mermaid-svg-container .edgePath path { stroke-width: 2px !important; }
340
- @media (min-width: 768px) {
341
- .mermaid-svg-container .node text,
342
- .mermaid-svg-container .edgeLabel text { font-size: 14px !important; }
343
- }
344
- @media (min-width: 1520px) {
345
- .mermaid-svg-container .node text,
346
- .mermaid-svg-container .edgeLabel text { font-size: 16px !important; }
347
- }
348
- `;
349
-
350
- // ---------------------------------------------------------------------------
351
- // MermaidDiagram
352
- // ---------------------------------------------------------------------------
353
- const MermaidDiagram: React.FC<{ chart: string }> = ({ chart }) => {
354
- const [svg, setSvg] = useState<string>('');
355
- const [error, setError] = useState<string>('');
356
- const [isLoading, setIsLoading] = useState<boolean>(true);
357
- const [mounted, setMounted] = useState(false);
358
-
359
- useEffect(() => { setMounted(true); }, []);
360
-
361
- useEffect(() => {
362
- const renderMermaid = async () => {
363
- try {
364
- setIsLoading(true);
365
- // @ts-ignore -- mermaid is an optional runtime dependency, dynamically imported by the consumer
366
- const { default: mermaid } = await import('mermaid');
367
-
368
- mermaid.initialize({
369
- startOnLoad: false,
370
- theme: 'dark' as const,
371
- themeVariables: {
372
- primaryColor: '#FFC008',
373
- primaryTextColor: '#FAFAFA',
374
- primaryBorderColor: '#3A3A3A',
375
- lineColor: '#888888',
376
- secondaryColor: '#212121',
377
- tertiaryColor: '#2A2A2A',
378
- background: 'transparent',
379
- mainBkg: 'transparent',
380
- secondBkg: 'transparent',
381
- tertiaryBkg: 'transparent',
382
- cScale0: '#FFC008', cScale1: '#4ECDC4', cScale2: '#45B7D1',
383
- cScale3: '#96CEB4', cScale4: '#FFEAA7', cScale5: '#DDA0DD',
384
- cScale6: '#98D8C8', cScale7: '#F7DC6F', cScale8: '#BB8FCE',
385
- cScale9: '#85C1E9',
386
- taskTextColor: '#FAFAFA',
387
- taskTextOutsideColor: '#FAFAFA',
388
- activeTaskTextColor: '#1A1A1A',
389
- nodeTextColor: '#FAFAFA',
390
- },
391
- flowchart: { useMaxWidth: true, htmlLabels: true, rankSpacing: 50, nodeSpacing: 30, curve: 'basis' },
392
- sequence: { useMaxWidth: true, width: 150 },
393
- pie: { useMaxWidth: true, useWidth: undefined },
394
- fontFamily: 'DM Sans, sans-serif',
395
- fontSize: 14,
396
- securityLevel: 'loose',
397
- });
398
-
399
- const { svg: renderedSvg } = await mermaid.render(`mermaid-${Date.now()}`, chart);
400
- setSvg(renderedSvg);
401
- setIsLoading(false);
402
- } catch (err) {
403
- console.error('Mermaid rendering error:', err);
404
- setError(`Failed to render diagram: ${err instanceof Error ? err.message : 'Unknown error'}`);
405
- setIsLoading(false);
406
- }
407
- };
408
-
409
- if (mounted) { renderMermaid(); }
410
- }, [chart, mounted]);
411
-
412
- if (error) {
413
- return (
414
- <div className="error-state bg-ods-card border border-ods-border rounded-lg p-6 my-6">
415
- <div className="error-icon flex justify-center mb-4">
416
- <AlertCircleIcon className="w-12 h-12 text-ods-error" />
417
- </div>
418
- <div className="error-title text-center font-sans font-semibold text-lg text-ods-error mb-2">
419
- Diagram Error
420
- </div>
421
- <div className="error-description text-center font-sans text-sm text-ods-text-secondary mb-4 break-words overflow-hidden max-w-full">
422
- <div className="overflow-x-auto">
423
- <pre className="whitespace-pre-wrap break-words text-xs">{error}</pre>
424
- </div>
425
- </div>
426
- </div>
427
- );
428
- }
429
-
430
- if (isLoading || !svg) {
431
- return (
432
- <div className="skeleton-code bg-ods-card border border-ods-border rounded-lg p-6 min-h-[120px] flex items-center justify-center">
433
- <div className="animate-pulse text-ods-text-tertiary font-sans">
434
- {isLoading ? 'Loading diagram renderer...' : 'Rendering diagram...'}
435
- </div>
436
- </div>
437
- );
438
- }
439
-
440
- return (
441
- <div className="mermaid-container rounded-lg p-4 md:p-6 lg:p-8 my-6 overflow-x-auto bg-ods-card border border-ods-border">
442
- <div className="flex justify-center items-center w-full min-h-[200px] md:min-h-[250px] lg:min-h-[300px]">
443
- <div
444
- className="mermaid-svg-container w-full flex justify-center max-w-full"
445
- style={{ fontSize: '14px' }}
446
- dangerouslySetInnerHTML={{
447
- __html: svg.replace(/<svg[^>]*>/, (match) =>
448
- match.replace(/width="[^"]*"/, 'width="100%"').replace(/height="[^"]*"/, 'height="auto"')
449
- ),
450
- }}
451
- />
452
- </div>
453
- </div>
454
- );
455
- };
456
-
457
- // ---------------------------------------------------------------------------
458
- // MarkdownContentImage
459
- // ---------------------------------------------------------------------------
460
- // Inline content image renderer. Used by blog posts, docs, AND chat
461
- // messages (where users may attach screenshots / photos via the +
462
- // attachment button). A standalone component (rather than inline JSX in
463
- // the `img` renderer) because it needs `useAuthedImageSrc`: in
464
- // bearer-mode native shells a gateway-hosted image can't load via a
465
- // plain `<img>` (no Authorization header on native asset loads), so the
466
- // hook swaps in an authed blob URL; everywhere else it passes `src`
467
- // through untouched. While the blob fetch is in flight the component
468
- // renders nothing — same as the no-src case.
469
- //
470
- // Sizing rules (2025-2026 best practice — Claude.ai, ChatGPT,
471
- // iMessage, Slack, Discord inline image patterns):
472
- //
473
- // - CAP max-width at 400px so inline images don't blow out the
474
- // message column on wide panels. Click-to-expand opens a
475
- // full-resolution modal for users who need detail.
476
- // - CAP max-height at 400px so portrait-orientation images
477
- // don't dominate vertical space (a 1000x3000 phone screenshot
478
- // would otherwise push the next message off-screen).
479
- // - Small images render at NATURAL pixel size — a 64x64
480
- // thumbnail stays 64x64, not stretched to fill the column.
481
- // - `object-contain` preserves aspect ratio when both dimensions
482
- // are constrained (long landscape, tall portrait).
483
- //
484
- // Implementation: Next.js `<Image>` — the project's canonical
485
- // image primitive (WebP/AVIF conversion, responsive `srcset`, lazy
486
- // loading, async decode). `width={400} height={400}` are REQUIRED by
487
- // Next.js `<Image>` (non-`fill` mode throws without them) but they're
488
- // effectively a CEILING here, not the display size — the CSS overrides
489
- // (`w-auto h-auto max-w-full max-h-[400px]`) drive the actual rendered
490
- // size, and the inline `style={{ width: 'auto', height: 'auto' }}` is
491
- // belt-and-suspenders against the rendered `<img>`'s HTML width/height
492
- // attributes.
493
- const MarkdownContentImage: React.FC<{ src: string; alt?: string }> = ({ src, alt }) => {
494
- const resolvedSrc = useAuthedImageSrc(src);
495
- if (!resolvedSrc) return null;
496
- return (
497
- <Image
498
- src={resolvedSrc}
499
- alt={alt ?? 'No image available'}
500
- width={400}
501
- height={400}
502
- sizes="(max-width: 400px) 100vw, 400px"
503
- className="max-w-full max-h-[400px] w-auto h-auto rounded-lg object-contain"
504
- style={{ width: 'auto', height: 'auto' }}
505
- />
506
- );
507
- };
508
-
509
- // ---------------------------------------------------------------------------
510
- // Utility: extract plain text from React children
511
- // ---------------------------------------------------------------------------
512
- function extractText(node: any): string {
513
- if (typeof node === 'string') return node;
514
- if (Array.isArray(node)) return node.map(extractText).join('');
515
- if (node?.props?.children) return extractText(node.props.children);
516
- return '';
517
- }
518
-
519
- // ---------------------------------------------------------------------------
520
- // Text size configuration
521
- // ---------------------------------------------------------------------------
522
- export type TextSizeElement =
523
- | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
524
- | 'p' | 'li' | 'blockquote' | 'code' | 'th' | 'td';
525
-
526
- export type TextSizeClassMap = Partial<Record<TextSizeElement, string>>;
527
- export type TextSizePreset = 'default' | 'compact' | 'large';
528
- export type TextSizeConfig =
529
- | TextSizePreset
530
- | TextSizeClassMap
531
- | { preset: TextSizePreset; overrides: TextSizeClassMap };
532
-
533
- const TEXT_SIZE_PRESETS: Record<TextSizePreset, Record<TextSizeElement, string>> = {
534
- default: {
535
- h1: 'text-heading-1',
536
- h2: 'text-heading-2',
537
- h3: 'text-2xl md:text-3xl',
538
- h4: 'text-xl',
539
- h5: 'text-lg md:text-xl',
540
- h6: 'text-base md:text-lg',
541
- p: 'md:text-h4 lg:text-h4',
542
- li: 'text-base md:text-lg',
543
- blockquote: 'text-lg',
544
- code: 'text-[14px]',
545
- th: 'text-xs md:text-sm',
546
- td: 'text-xs md:text-sm',
547
- },
548
- compact: {
549
- h1: 'text-heading-2',
550
- h2: 'text-heading-3',
551
- h3: 'text-xl md:text-2xl',
552
- h4: 'text-lg md:text-xl',
553
- h5: 'text-base md:text-lg',
554
- h6: 'text-sm md:text-base',
555
- p: 'text-base md:text-lg',
556
- li: 'text-base md:text-lg',
557
- blockquote: 'text-base md:text-lg',
558
- code: 'text-[13px]',
559
- th: 'text-xs md:text-sm',
560
- td: 'text-xs md:text-sm',
561
- },
562
- large: {
563
- h1: 'text-heading-1',
564
- h2: 'text-heading-1',
565
- h3: 'text-heading-2',
566
- h4: 'text-2xl md:text-3xl',
567
- h5: 'text-xl md:text-2xl',
568
- h6: 'text-lg md:text-xl',
569
- p: 'text-h3',
570
- li: 'text-lg md:text-xl',
571
- blockquote: 'text-xl md:text-2xl',
572
- code: 'text-[16px]',
573
- th: 'text-sm md:text-base',
574
- td: 'text-sm md:text-base',
575
- },
576
- };
577
-
578
- function resolveTextSizeConfig(config?: TextSizeConfig): Record<TextSizeElement, string> {
579
- const defaultSizes = TEXT_SIZE_PRESETS.default;
580
- if (!config) return defaultSizes;
581
- if (typeof config === 'string') return TEXT_SIZE_PRESETS[config];
582
- if ('preset' in config) return { ...TEXT_SIZE_PRESETS[config.preset], ...config.overrides };
583
- return { ...defaultSizes, ...config };
584
- }
585
-
586
- // `ResolveLinkResult` lives in `../../types/doc-source` so it's the single
587
- // canonical home shared by `DocRenderHandlers.onResolveLink`, `DocViewer`'s
588
- // resolver, and this renderer's `onResolveLink` prop. Re-exported at the top
589
- // of this file for back-compat with callers that import from here.
590
-
591
- // ---------------------------------------------------------------------------
592
- // Props
593
- // ---------------------------------------------------------------------------
594
- export interface SimpleMarkdownRendererProps {
595
- content: string;
596
- className?: string;
597
- /** Backend-provided heading IDs for deep-link anchors */
598
- sectionIds?: Array<{ id: string; title: string; level: number }>;
599
- /** When the page already has an H1, render markdown `#` as `<h2>` */
600
- demoteMarkdownH1ToH2?: boolean;
601
- /** List of broken link hrefs detected server-side (shown with [BROKEN] badge) */
602
- brokenLinks?: string[];
603
- /** Callback for internal (non-http, non-anchor) link clicks */
604
- onInternalLinkClick?: (path: string, options?: { expandFolder?: boolean; fromInternalLink?: boolean }) => void;
605
- /** Current documentation path — enables internal-link mode when set */
606
- currentPath?: string;
607
- /**
608
- * Resolve an internal link href to a navigation path.
609
- * Called when a user clicks an internal doc link.
610
- */
611
- onResolveLink?: (href: string, currentPath: string) => Promise<ResolveLinkResult>;
612
- /** Pre-process the raw markdown string before rendering (e.g. shortcode expansion) */
613
- preprocessContent?: (content: string) => string;
614
- /** Merge additional or override react-markdown component renderers */
615
- componentOverrides?: Partial<Components>;
616
- /**
617
- * Extra remark plugins appended after the built-in `remarkGfm` and
618
- * `remarkBreaks`. Used by chat consumers to inject `remarkCardLinks`,
619
- * which converts `[card://<type>:<id>]` markers into synthetic `link`
620
- * nodes; the host's `componentOverrides.a` then swaps those for whatever
621
- * inline entity-card component it provides.
622
- *
623
- * Each entry is either a Plugin reference or a [Plugin, options] tuple
624
- * — same shape as react-markdown's own `remarkPlugins` prop. Pass an
625
- * empty array (or omit) to keep the default plugin set.
626
- */
627
- additionalRemarkPlugins?: PluggableList;
628
- /**
629
- * Configure text sizing for all rendered elements.
630
- * - `"default"` — current behavior (large article-style typography)
631
- * - `"compact"` — smaller sizes for sidebars, cards, changelogs
632
- * - `"large"` — extra-large sizes for hero/landing content
633
- * - `{ p: "text-sm", h1: "text-2xl" }` — per-element overrides (merged onto default)
634
- * - `{ preset: "compact", overrides: { h1: "text-heading-1" } }` — preset + tweaks
635
- */
636
- textSize?: TextSizeConfig;
637
- }
638
-
639
- // ---------------------------------------------------------------------------
640
- // Component
641
- // ---------------------------------------------------------------------------
642
- const SimpleMarkdownRendererImpl: React.FC<SimpleMarkdownRendererProps> = ({
643
- content,
644
- className = "",
645
- sectionIds,
646
- demoteMarkdownH1ToH2 = false,
647
- brokenLinks = [],
648
- onInternalLinkClick,
649
- currentPath: propCurrentPath,
650
- onResolveLink,
651
- preprocessContent,
652
- componentOverrides,
653
- textSize,
654
- additionalRemarkPlugins,
655
- }) => {
656
- const idCountsRef = useRef<Record<string, number>>({});
657
-
658
- // ---- text sizes ----
659
- const textSizes = useMemo(() => resolveTextSizeConfig(textSize), [textSize]);
660
-
661
- // ---- section ID map ----
662
- const sectionIdMap = useMemo(() => {
663
- const map = new Map<string, string>();
664
- if (sectionIds) {
665
- sectionIds.forEach(section => {
666
- const cleanTitle = section.title
667
- .replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '')
668
- .trim()
669
- .toLowerCase();
670
- map.set(section.title.toLowerCase(), section.id);
671
- map.set(cleanTitle, section.id);
672
- map.set(section.title, section.id);
673
- });
674
- }
675
- return map;
676
- }, [sectionIds]);
677
-
678
- // ---- heading ID generation ----
679
- const generateHeadingId = useCallback((text: string, level: number): string => {
680
- if (sectionIds && (level === 1 || level === 2)) {
681
- const variations = [
682
- text, text.toLowerCase(),
683
- text.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '').trim(),
684
- text.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '').trim().toLowerCase(),
685
- ];
686
- for (const v of variations) {
687
- const id = sectionIdMap.get(v);
688
- if (id) return id;
689
- }
690
- }
691
- const baseId = text
692
- .replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '')
693
- .trim().toLowerCase()
694
- .replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').replace(/^-+|-+$/g, '');
695
- const cleanId = baseId || `section-${Object.keys(idCountsRef.current).length + 1}`;
696
- if (idCountsRef.current[cleanId]) {
697
- idCountsRef.current[cleanId]++;
698
- return `${cleanId}-${idCountsRef.current[cleanId]}`;
699
- }
700
- idCountsRef.current[cleanId] = 1;
701
- return cleanId;
702
- }, [sectionIds, sectionIdMap]);
703
-
704
- // ---- preprocess ----
705
- // Run the optional caller-supplied transform first, then defensively
706
- // escape unknown HTML-style tags so an LLM that wrote `<their>` or
707
- // accidentally echoed a system-prompt XML tag like `<ticket>` doesn't
708
- // make `rehype-raw` hand React an unrecognized custom element (which
709
- // logs a noisy "tag is unrecognized in this browser" warning AND can
710
- // crash the SimpleMarkdownRenderer when the tag has a kebab-case
711
- // form React rejects outright). Allow-listed tags pass through
712
- // unchanged so legitimate inline HTML (details/summary, iframe,
713
- // etc.) still renders ('video' is excluded — see SAFE_HTML_TAGS).
714
- const processedContent = useMemo(
715
- () => escapeUnknownHtmlTags(preprocessContent ? preprocessContent(content) : content),
716
- [preprocessContent, content],
717
- );
718
-
719
- // ---- heading factory ----
720
- const makeHeading = useCallback(
721
- (Tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', level: number, headingClassName: string) =>
722
- ({ children }: any) => {
723
- const text = extractText(children);
724
- const effectiveLevel = Tag === 'h1' && demoteMarkdownH1ToH2 ? 2 : level;
725
- const id = generateHeadingId(text, effectiveLevel);
726
- const EffectiveTag = Tag === 'h1' && demoteMarkdownH1ToH2 ? 'h2' : Tag;
727
- return <EffectiveTag id={id} className={headingClassName}>{children}</EffectiveTag>;
728
- },
729
- [generateHeadingId, demoteMarkdownH1ToH2],
730
- );
731
-
732
- // ---- components ----
733
- const components: Components = useMemo(() => ({
734
- // --- code ---
735
- code: ({ node, inline, className: codeClassName, children, ...props }: any) => {
736
- const match = /language-(\w+)/.exec(codeClassName || '');
737
- const language = match ? match[1] : '';
738
-
739
- if (!inline && language === 'mermaid') {
740
- return <MermaidDiagram chart={String(children).replace(/\n$/, '')} />;
741
- }
742
-
743
- if (!inline && match) {
744
- return (
745
- <div className="code-block-container border rounded-lg my-6 overflow-hidden bg-ods-card border-ods-border">
746
- <div className="code-header border-b px-4 py-2 bg-ods-card border-ods-border">
747
- <span className="font-sans text-xs uppercase tracking-wide text-ods-text-tertiary">
748
- {language || 'code'}
749
- </span>
750
- </div>
751
- <div className="p-4">
752
- <pre className="overflow-x-auto">
753
- <code
754
- className={cn(`language-${language} hljs`, textSizes.code)}
755
- style={{
756
- fontFamily: "JetBrains Mono', 'SF Mono', Consolas, monospace",
757
- background: 'transparent',
758
- color: 'var(--color-text-primary)',
759
- }}
760
- {...props}
761
- >
762
- {children}
763
- </code>
764
- </pre>
765
- </div>
766
- </div>
767
- );
768
- }
769
-
770
- return (
771
- <code className="font-mono text-[0.9em] px-1.5 py-0.5 rounded border bg-ods-card text-ods-text-primary border-ods-border" {...props}>
772
- {children}
773
- </code>
774
- );
775
- },
776
-
777
- // --- div (pass-through, overridable for embeds) ---
778
- div: ({ node, className: divClassName, children, ...props }: any) => (
779
- <div className={divClassName} {...props}>{children}</div>
780
- ),
781
-
782
- // --- blockquote ---
783
- blockquote: ({ children }: any) => (
784
- <blockquote className="border-l-4 border-ods-accent ml-0 pl-6 my-8 py-4 rounded-r-lg bg-ods-bg-surface">
785
- <div className={cn('font-sans leading-relaxed text-ods-text-secondary', textSizes.blockquote)}>
786
- {children}
787
- </div>
788
- </blockquote>
789
- ),
790
-
791
- // --- headings ---
792
- h1: makeHeading('h1', 1, cn('font-sans font-bold mt-8 mb-4 first:mt-0 text-ods-text-primary', textSizes.h1)),
793
- h2: makeHeading('h2', 2, cn('font-sans font-semibold mt-8 mb-4 pb-2 border-b text-ods-text-primary border-ods-border', textSizes.h2)),
794
- h3: makeHeading('h3', 3, cn('font-sans font-semibold mt-6 mb-3 text-ods-text-primary', textSizes.h3)),
795
- h4: makeHeading('h4', 4, cn('font-sans font-semibold mt-4 mb-2 text-ods-text-primary', textSizes.h4)),
796
- h5: makeHeading('h5', 5, cn('font-sans font-semibold mt-3 mb-2 text-ods-text-primary', textSizes.h5)),
797
- h6: makeHeading('h6', 6, cn('font-sans font-semibold mt-3 mb-1 text-ods-text-primary', textSizes.h6)),
798
-
799
- // --- paragraph ---
800
- p: ({ children }: any) => (
801
- <p className={cn('leading-relaxed mb-4 first:mt-0 last:mb-0 text-ods-text-primary', textSizes.p)}>
802
- {children}
803
- </p>
804
- ),
805
-
806
- // --- links ---
807
- a: ({ href, children, className: linkClassName }: any) => {
808
- const isBroken = brokenLinks.includes(href);
809
- const isInternalDocLink =
810
- propCurrentPath !== undefined &&
811
- propCurrentPath !== null &&
812
- href &&
813
- !href.startsWith('http') &&
814
- !href.startsWith('#');
815
-
816
- if (isBroken) {
817
- return (
818
- <span className="text-ods-accent cursor-not-allowed">
819
- {children}
820
- <sup className="ml-1 text-xs font-bold text-ods-error">[BROKEN]</sup>
821
- </span>
822
- );
823
- }
824
-
825
- if (isInternalDocLink && onInternalLinkClick) {
826
- const currentPath = propCurrentPath ?? '';
827
- return (
828
- <span
829
- className="text-ods-accent no-underline relative transition-colors duration-200 hover:after:w-full after:content-[''] after:absolute after:w-0 after:h-0.5 after:-bottom-0.5 after:left-0 after:bg-ods-accent after:transition-all after:duration-300 cursor-pointer"
830
- onClick={async (e) => {
831
- e.preventDefault();
832
- e.stopPropagation();
833
- if (onResolveLink) {
834
- try {
835
- const result = await onResolveLink(href, currentPath);
836
- if (result.type === 'folder-no-readme' && result.action === 'expand_folder') {
837
- onInternalLinkClick(result.resolvedPath!, { expandFolder: true, fromInternalLink: true });
838
- } else if (result.type === 'not-found') {
839
- return;
840
- } else if (result.success && result.resolvedPath) {
841
- onInternalLinkClick(result.resolvedPath, { fromInternalLink: true });
842
- }
843
- } catch (error) {
844
- console.error('Error resolving link:', error);
845
- }
846
- } else {
847
- onInternalLinkClick(href, { fromInternalLink: true });
848
- }
849
- }}
850
- role="link"
851
- tabIndex={0}
852
- onKeyDown={(e) => {
853
- if (e.key === 'Enter' || e.key === ' ') {
854
- (e.currentTarget as HTMLElement).click();
855
- }
856
- }}
857
- >
858
- {children}
859
- </span>
860
- );
861
- }
862
-
863
- return (
864
- <a
865
- href={href}
866
- className={`text-ods-accent no-underline relative transition-colors duration-200 hover:after:w-full after:content-[''] after:absolute after:w-0 after:h-0.5 after:-bottom-0.5 after:left-0 after:bg-ods-accent after:transition-all after:duration-300 ${linkClassName || ''}`}
867
- target={href?.startsWith('http') ? '_blank' : undefined}
868
- rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}
869
- >
870
- {children}
871
- </a>
872
- );
873
- },
874
-
875
- // --- images ---
876
- // Delegates to MarkdownContentImage (module scope, above) — sizing
877
- // rules, Next.js <Image> rationale, and the bearer-mode authed-src
878
- // handling are documented there.
879
- img: ({ src, alt }: any) => {
880
- if (!src || typeof src !== 'string' || src.trim() === '') return null;
881
- return <MarkdownContentImage src={src} alt={alt} />;
882
- },
883
-
884
- // --- lists ---
885
- ul: ({ children }: any) => (
886
- <ul className="list-disc list-outside my-4 ml-8 space-y-2 text-ods-text-primary">{children}</ul>
887
- ),
888
- ol: ({ children }: any) => (
889
- <ol className="list-decimal list-outside my-4 ml-8 space-y-2 text-ods-text-primary">{children}</ol>
890
- ),
891
- li: ({ children }: any) => (
892
- <li className={cn('leading-relaxed pl-2', textSizes.li)}>{children}</li>
893
- ),
894
-
895
- // --- tables ---
896
- table: ({ children }: any) => (
897
- <div className="table-container my-6 overflow-x-auto">
898
- <div className="min-w-full border rounded-lg border-ods-border bg-ods-card">
899
- <table className="w-full table-fixed md:table-auto">{children}</table>
900
- </div>
901
- </div>
902
- ),
903
- thead: ({ children }: any) => (
904
- <thead className="bg-ods-bg-surface">{children}</thead>
905
- ),
906
- th: ({ children }: any) => (
907
- <th className={cn('px-2 md:px-4 py-3 text-left font-semibold text-ods-accent border-r last:border-r-0 break-words border-ods-border', textSizes.th)}>
908
- {children}
909
- </th>
910
- ),
911
- td: ({ children }: any) => (
912
- <td className={cn('px-2 md:px-4 py-3 border-r last:border-r-0 border-b break-words whitespace-normal text-ods-text-primary border-ods-border', textSizes.td)}>
913
- {children}
914
- </td>
915
- ),
916
-
917
- // --- horizontal rule ---
918
- hr: () => <hr className="border-0 border-t my-8 border-ods-border" />,
919
-
920
- // --- merge overrides ---
921
- ...componentOverrides,
922
- }), [makeHeading, brokenLinks, propCurrentPath, onInternalLinkClick, onResolveLink, componentOverrides, textSizes]);
923
-
924
- return (
925
- <div className={`simple-markdown-renderer ${className}`}>
926
- <style dangerouslySetInnerHTML={{ __html: mermaidStyles }} />
927
- {/* `overflow-wrap: anywhere` is inherited from `.simple-markdown-renderer`
928
- in app-globals.css — do NOT re-add `break-words` here, it would
929
- override the cascade with the weaker `break-word` for this subtree. */}
930
- <div className="content-wrapper max-w-none">
931
- <article className="prose prose-lg max-w-none">
932
- <ReactMarkdown
933
- remarkPlugins={[remarkGfm, remarkBreaks, ...(additionalRemarkPlugins ?? [])]}
934
- rehypePlugins={[
935
- // ORDER MATTERS: rehype-raw parses the raw HTML embedded
936
- // in the source markdown into HAST nodes; rehypeStripUnsafe
937
- // then walks the HAST tree and drops XSS vectors (on*
938
- // event handlers, javascript: URLs, script/style/iframe-srcdoc,
939
- // data: URIs). Reversing the order would have nothing to
940
- // sanitize (raw HTML would still be strings).
941
- rehypeRaw,
942
- rehypeStripUnsafe,
943
- [rehypeHighlight, { detect: true, ignoreMissing: true }],
944
- ]}
945
- urlTransform={cardAwareUrlTransform}
946
- components={components}
947
- >
948
- {processedContent}
949
- </ReactMarkdown>
950
- </article>
951
- </div>
952
- </div>
953
- );
954
- };
955
-
956
- /**
957
- * Memoized so a parent re-render with UNCHANGED props (same `content` string,
958
- * same memoized `componentOverrides`/plugins) does NOT re-parse the markdown.
959
- * Re-parsing rebuilds the entire react-markdown subtree, which RE-MOUNTS any
960
- * embedded inline entity cards — closing their open menus and re-triggering
961
- * their fetch on every chat re-render (streaming chunk AND scroll). With
962
- * stable props the renderer bails, so completed messages' cards stay mounted.
963
- */
964
- export const SimpleMarkdownRenderer = memo(SimpleMarkdownRendererImpl);