@worktango/ai-assistant 0.0.21 → 0.0.24

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 (209) hide show
  1. package/README.md +18 -2
  2. package/dist/{Absenteeism-CedNvSxC.mjs → Absenteeism-BlfJuY7Q.mjs} +1 -1
  3. package/dist/{ActivityFeed-wI9wBx6H.mjs → ActivityFeed-CdbbvPCr.mjs} +1 -1
  4. package/dist/{AddUser-DE-2WrlL.mjs → AddUser-Dm5tN1Cr.mjs} +1 -1
  5. package/dist/{Admin-Cfl1AW_P.mjs → Admin-Cv5LVBbB.mjs} +1 -1
  6. package/dist/{Admin-C15wLDAT.mjs → Admin-GJZ14xCg.mjs} +1 -1
  7. package/dist/{Anniversary-CsvnVDOS.mjs → Anniversary-CxsqeD8j.mjs} +1 -1
  8. package/dist/{Appreciation-CYMmLsU_.mjs → Appreciation-CplBRYpa.mjs} +1 -1
  9. package/dist/{ArrowDown-Dubu_Ec7.mjs → ArrowDown-DCYs9jbi.mjs} +1 -1
  10. package/dist/{ArrowLeft-A1yp5aqr.mjs → ArrowLeft-DlFDpsqu.mjs} +1 -1
  11. package/dist/{ArrowRight-BhAh3I-x.mjs → ArrowRight-CDqrZvCr.mjs} +1 -1
  12. package/dist/{ArrowUp-bVYNtMgx.mjs → ArrowUp-DtWnNyRL.mjs} +1 -1
  13. package/dist/{Attachment-CWcY2CqV.mjs → Attachment-DEzcZHI7.mjs} +1 -1
  14. package/dist/{BeQuickDontHurry-DbSFImq2.mjs → BeQuickDontHurry-DoCKS3D0.mjs} +1 -1
  15. package/dist/{Birthday-C5IrWOxl.mjs → Birthday-COe-42mW.mjs} +1 -1
  16. package/dist/{BrandPromise-C_LmWwe6.mjs → BrandPromise-DT7BpKTq.mjs} +1 -1
  17. package/dist/{BrandVision-DYq6uUoU.mjs → BrandVision-CsF5PHr9.mjs} +1 -1
  18. package/dist/{Calendar-BYVlbm4s.mjs → Calendar-CbVaJVcV.mjs} +1 -1
  19. package/dist/{CaretDn-BeyN8HcH.mjs → CaretDn-DfXkDzAH.mjs} +1 -1
  20. package/dist/{CaretDown-BS3WGRra.mjs → CaretDown-DKwSpoJj.mjs} +1 -1
  21. package/dist/{CaretLeft-BXRjchoM.mjs → CaretLeft-DjIuIw4E.mjs} +1 -1
  22. package/dist/{CaretRight-CTegVEEE.mjs → CaretRight-Blv3CCWa.mjs} +1 -1
  23. package/dist/{CaretUp-B4tN3uVW.mjs → CaretUp-CEfq2pon.mjs} +1 -1
  24. package/dist/{ChallengeAccepted-DzEX4vek.mjs → ChallengeAccepted-BrB08i_g.mjs} +1 -1
  25. package/dist/{CheckIn-BsEJXvpg.mjs → CheckIn-DHloFCTi.mjs} +1 -1
  26. package/dist/{CheckIns-CeDSmtsH.mjs → CheckIns-BokF9D3N.mjs} +1 -1
  27. package/dist/{CheckSeal-BPPpnqgj.mjs → CheckSeal-3RBoC7kt.mjs} +1 -1
  28. package/dist/{Checkmark-DPwoD2cc.mjs → Checkmark-Cvb9TKPn.mjs} +1 -1
  29. package/dist/{Clock-C9YN2UE6.mjs → Clock-CRQ3vRb2.mjs} +1 -1
  30. package/dist/{Close-CuB41vwe.mjs → Close-CgKO-GmV.mjs} +1 -1
  31. package/dist/{CloudDownload-CxbXkl-A.mjs → CloudDownload-h8jFkmQV.mjs} +1 -1
  32. package/dist/{Comment-B70VK5iQ.mjs → Comment-CGL9Fj78.mjs} +1 -1
  33. package/dist/{Comments-B1eVtuj_.mjs → Comments-DnQeuBWu.mjs} +1 -1
  34. package/dist/{Connections-CgHfsXFl.mjs → Connections-GHPqdXk3.mjs} +1 -1
  35. package/dist/{Copy-BT-yQJw7.mjs → Copy-DSXLX28f.mjs} +1 -1
  36. package/dist/{CustomQuestion-DAwKynPQ.mjs → CustomQuestion-CYiCPJX8.mjs} +1 -1
  37. package/dist/{CustomerSuccess-1U9T-T1n.mjs → CustomerSuccess-FdMSyQLg.mjs} +1 -1
  38. package/dist/{DatePicker-BP1Bpv1b.mjs → DatePicker-0RNUxWxN.mjs} +1 -1
  39. package/dist/{Dot-CfD9KJ6k.mjs → Dot-CRWfXlj2.mjs} +1 -1
  40. package/dist/{Download-G0LOA1A3.mjs → Download-Be5-WujX.mjs} +1 -1
  41. package/dist/{Drag-C4fCOqTJ.mjs → Drag-wZ_mXU-D.mjs} +1 -1
  42. package/dist/{Earn-_iBKtfla.mjs → Earn-06RGCgPA.mjs} +1 -1
  43. package/dist/{Email-Cmm1RvmL.mjs → Email-8o3h5VSI.mjs} +1 -1
  44. package/dist/{EmojiDisplay-BZzD6ibf.mjs → EmojiDisplay-D3d36Nrl.mjs} +2 -2
  45. package/dist/{EmojiPicker-DMtVAC8n.mjs → EmojiPicker-BzkYk2Yv.mjs} +2 -2
  46. package/dist/{EnpsSurvey-DEMQkTOy.mjs → EnpsSurvey-wpqDe9Xq.mjs} +1 -1
  47. package/dist/{Expired-BflOG-Fm.mjs → Expired-DbhcQ39l.mjs} +1 -1
  48. package/dist/{Eye-B6MG7oI1.mjs → Eye-Dceyvp5y.mjs} +1 -1
  49. package/dist/{Feedback-qpfEKs95.mjs → Feedback-BQpvKfzY.mjs} +1 -1
  50. package/dist/{Feedback-DBmAcA2m.mjs → Feedback-DTwwTQN3.mjs} +3 -3
  51. package/dist/{Filter-BN9dK5kD.mjs → Filter-CKJYpt6s.mjs} +1 -1
  52. package/dist/{Flag-jKSpbvn6.mjs → Flag-Bex2EvDE.mjs} +1 -1
  53. package/dist/{Giphy-CXDFjiTX.mjs → Giphy-CFMqJSYK.mjs} +1 -1
  54. package/dist/{GoalCount-KUyaRVck.mjs → GoalCount-Bi0NMucg.mjs} +1 -1
  55. package/dist/{GoalPercent-DDsVzhO3.mjs → GoalPercent-B-kxyps2.mjs} +1 -1
  56. package/dist/{GoalTask-TDXXVUhs.mjs → GoalTask-IZwR0ZSf.mjs} +1 -1
  57. package/dist/{GoalTask-C3AKmm5p.mjs → GoalTask-Xjgp_Wo0.mjs} +1 -1
  58. package/dist/{Goals-a3Gd4p1n.mjs → Goals-BBGtKkJ1.mjs} +1 -1
  59. package/dist/{Goals-BnrsLd9r.mjs → Goals-COoucVqq.mjs} +1 -1
  60. package/dist/{Growth-CCeyxRJc.mjs → Growth-D8A13g-Q.mjs} +1 -1
  61. package/dist/{Hamburger-2QlPY7MK.mjs → Hamburger-DInD_hK4.mjs} +1 -1
  62. package/dist/{Help-BcXc8ez6.mjs → Help-BcQznZaK.mjs} +1 -1
  63. package/dist/{HighFive-UG0NAADf.mjs → HighFive-DqVD1Say.mjs} +1 -1
  64. package/dist/{HighFive-DCjp43gs.mjs → HighFive-kxxxx1a1.mjs} +1 -1
  65. package/dist/{Home-yMgsJvNt.mjs → Home-CE6c4US8.mjs} +1 -1
  66. package/dist/{Home-C5WodEqM.mjs → Home-Dzhzxm5B.mjs} +1 -1
  67. package/dist/{HorizontalLine-JD_RqEgw.mjs → HorizontalLine-Dyh1cfN-.mjs} +1 -1
  68. package/dist/{Hourglass-C3_IVNJa.mjs → Hourglass-CZ_gs0L5.mjs} +1 -1
  69. package/dist/{Image-CFKQTicl.mjs → Image-D8RpGjUr.mjs} +1 -1
  70. package/dist/{ImageFile-DfNJ9qhq.mjs → ImageFile-CpJ6rqlm.mjs} +1 -1
  71. package/dist/{InGoodCompany-6xQLozP6.mjs → InGoodCompany-F_ltthqt.mjs} +1 -1
  72. package/dist/{Incentives-DFFQFZih.mjs → Incentives-CFe-oLpH.mjs} +1 -1
  73. package/dist/{Info-DwqI5ZcJ.mjs → Info-DNU4voqh.mjs} +1 -1
  74. package/dist/{Insights-Cy8kUifj.mjs → Insights-C0x9QzfX.mjs} +1 -1
  75. package/dist/{Insights-Dx0mt_sq.mjs → Insights-D9LemXYM.mjs} +1 -1
  76. package/dist/{Integrations-BYYqLmX_.mjs → Integrations-D4MNE3-i.mjs} +1 -1
  77. package/dist/{KazooLogo-I7BBjIWv.mjs → KazooLogo-DaVNTRlw.mjs} +1 -1
  78. package/dist/{KnowYourAudience-CVAchlPu.mjs → KnowYourAudience-D9qjKO2J.mjs} +1 -1
  79. package/dist/{Launch-Da51LF1k.mjs → Launch-xm296JGJ.mjs} +1 -1
  80. package/dist/{Link-Bi1JqO19.mjs → Link-DUJoL5SM.mjs} +1 -1
  81. package/dist/{LinkArrow-a7xg2uw5.mjs → LinkArrow-C53WYuP9.mjs} +1 -1
  82. package/dist/{Loading-C1i-_OMU.mjs → Loading-YoU3Ro5C.mjs} +1 -1
  83. package/dist/{Lock-CU7TWBei.mjs → Lock-Ig2rvmxz.mjs} +1 -1
  84. package/dist/{Management-DcaxdfRe.mjs → Management-CSk1I3Vu.mjs} +1 -1
  85. package/dist/{MeatballMenu-CXyQkQTY.mjs → MeatballMenu-akus2Lt6.mjs} +1 -1
  86. package/dist/{Messages-BaRHnj7R.mjs → Messages-CdHryWCZ.mjs} +1 -1
  87. package/dist/{Mobile-C82mnkS8.mjs → Mobile-DIs7zF33.mjs} +1 -1
  88. package/dist/{Nomination-Clb3B7rU.mjs → Nomination-DtyERYRo.mjs} +1 -1
  89. package/dist/{NominationsAndAwards-LizaS-lj.mjs → NominationsAndAwards-DUlt_REW.mjs} +1 -1
  90. package/dist/{Notification-C-hvbQgF.mjs → Notification-yHY4Q-iO.mjs} +1 -1
  91. package/dist/{Okrs-Dz7iz6Y9.mjs → Okrs-BVR2trfg.mjs} +1 -1
  92. package/dist/{PeerToPeer-BvKH26DO.mjs → PeerToPeer-Dw7054su.mjs} +1 -1
  93. package/dist/{Pencil-CXJlOW6Z.mjs → Pencil--PMTRUju.mjs} +1 -1
  94. package/dist/{People--NThL2WE.mjs → People-01fveD3Z.mjs} +1 -1
  95. package/dist/{PerformanceManagement-Bb1JvSj_.mjs → PerformanceManagement-BO6JMhw0.mjs} +1 -1
  96. package/dist/{Pin-cuzusAoP.mjs → Pin-DrqWw6Ik.mjs} +1 -1
  97. package/dist/PinnedIconAward-CfI7QvmM.mjs +20 -0
  98. package/dist/PinnedIconBrightIdea-C-jLsEme.mjs +20 -0
  99. package/dist/PinnedIconBullseye-DHzPwFNh.mjs +36 -0
  100. package/dist/PinnedIconChampagne-DqhhML6p.mjs +20 -0
  101. package/dist/PinnedIconCheer-Dr-LynHf.mjs +20 -0
  102. package/dist/PinnedIconCrown-BjZF_Us9.mjs +20 -0
  103. package/dist/PinnedIconDiamond-mR8xcC_8.mjs +20 -0
  104. package/dist/PinnedIconHoleInOne-D0YlpLT_.mjs +20 -0
  105. package/dist/PinnedIconMedal-CsbWDOeO.mjs +20 -0
  106. package/dist/PinnedIconMetrics-FZwXRcdT.mjs +20 -0
  107. package/dist/PinnedIconPin-DQYaYwDa.mjs +20 -0
  108. package/dist/PinnedIconRockAndRoll-2pp2LlIt.mjs +23 -0
  109. package/dist/PinnedIconRocket-BIrfZPOc.mjs +20 -0
  110. package/dist/PinnedIconStrength-B2Jlgh2J.mjs +20 -0
  111. package/dist/PinnedIconTeddyBear-CywM8WU1.mjs +43 -0
  112. package/dist/{PlatformLogo-CK09vVsJ.mjs → PlatformLogo-D2gEkUsw.mjs} +1 -1
  113. package/dist/{Plus-DJaLCs4t.mjs → Plus-hKSiCxQa.mjs} +1 -1
  114. package/dist/{Popper-DTEvAyBx.mjs → Popper-D6_fRDiW.mjs} +1 -1
  115. package/dist/{PrivateEye-WzWeEjb1.mjs → PrivateEye-DOcfeBbQ.mjs} +1 -1
  116. package/dist/{PrivatePushPin-DnNGaLNq.mjs → PrivatePushPin-BORYdpxi.mjs} +1 -1
  117. package/dist/{Processing-BDUBy4UH.mjs → Processing-C_zBVZIK.mjs} +1 -1
  118. package/dist/{Productivity-D5BVYtaN.mjs → Productivity-CLljIpDf.mjs} +1 -1
  119. package/dist/{ProgressCircle-CFTLbp-e.mjs → ProgressCircle-B9baqpgb.mjs} +1 -1
  120. package/dist/{PulseSurvey-B1TvHyDt.mjs → PulseSurvey-BQdfWo9A.mjs} +1 -1
  121. package/dist/{PushPin-CvluYJjM.mjs → PushPin-DkTVqfKk.mjs} +1 -1
  122. package/dist/{Recognition-DmfePy_z.mjs → Recognition-D3KBTtXQ.mjs} +1 -1
  123. package/dist/{Recognition-BDpYBUvN.mjs → Recognition-Dz_nZRDX.mjs} +1 -1
  124. package/dist/{Retention-BkJo5gos.mjs → Retention-ChOlEHZk.mjs} +1 -1
  125. package/dist/{Rewards-CtGT7qtN.mjs → Rewards-CVe8-44F.mjs} +1 -1
  126. package/dist/{Rewards-D_TyBfCv.mjs → Rewards-DU-Ho2dk.mjs} +1 -1
  127. package/dist/{RewardsCatalog-CyMbCiki.mjs → RewardsCatalog-BV1YY9Iq.mjs} +1 -1
  128. package/dist/{Roadmap-djDY2KOo.mjs → Roadmap-zfvbEbqx.mjs} +1 -1
  129. package/dist/{Search-N21SDOKm.mjs → Search-BhJp-JJi.mjs} +1 -1
  130. package/dist/{Security-CExbN6aN.mjs → Security-DRtNtKPc.mjs} +1 -1
  131. package/dist/{SendRecognition-gKLXXcO-.mjs → SendRecognition-Byajzteg.mjs} +1 -1
  132. package/dist/{ShareLink-Bn_3CvlX.mjs → ShareLink-D5S-TmIH.mjs} +1 -1
  133. package/dist/{Skill-BMD-jOQ8.mjs → Skill-CimhEzRw.mjs} +1 -1
  134. package/dist/{Skills-3QOhEpbd.mjs → Skills-DaQLD2Bb.mjs} +1 -1
  135. package/dist/{Smiley-BVpBA18A.mjs → Smiley-BmRWMKju.mjs} +1 -1
  136. package/dist/{Stop-C1787JfZ.mjs → Stop-jEo5XX0_.mjs} +1 -1
  137. package/dist/{Survey-CKqWVVEo.mjs → Survey-D7Lx8jsj.mjs} +1 -1
  138. package/dist/{Surveys-DvOszZdj.mjs → Surveys-Bnwc-nKF.mjs} +1 -1
  139. package/dist/{SyncUps-WBKKUyeT.mjs → SyncUps-DRTsoQ40.mjs} +1 -1
  140. package/dist/{Syncs-C_fskEeo.mjs → Syncs-CND3di67.mjs} +1 -1
  141. package/dist/{TalentReview-Dyg4wS7c.mjs → TalentReview-BCPucb1o.mjs} +1 -1
  142. package/dist/{TalentReview-RkspiqWg.mjs → TalentReview-CnEteZI6.mjs} +1 -1
  143. package/dist/{TalentReviewInsights-BnGxBe_H.mjs → TalentReviewInsights-COIEM4Dx.mjs} +1 -1
  144. package/dist/{TaskCompleted-BzlFAImO.mjs → TaskCompleted-CVVIklMl.mjs} +1 -1
  145. package/dist/{Team-Dxwjirjk.mjs → Team-DG9N4ucC.mjs} +1 -1
  146. package/dist/{Teams-2ntLFtej.mjs → Teams-BksxxA72.mjs} +1 -1
  147. package/dist/{TextBold-O-BU1YXO.mjs → TextBold-JCoCqG2E.mjs} +1 -1
  148. package/dist/{TextBulletList-BqZE7Exr.mjs → TextBulletList-wdvz7YF6.mjs} +1 -1
  149. package/dist/{TextInput-CvxuF1db.mjs → TextInput-BOcHdYfi.mjs} +1 -1
  150. package/dist/{TextItalic-DvGZwAEM.mjs → TextItalic-DS-EVK3i.mjs} +1 -1
  151. package/dist/{TextNumberList-BjEHBnDb.mjs → TextNumberList-DN5rq8Yj.mjs} +1 -1
  152. package/dist/{TextUnderline-BztlEu7E.mjs → TextUnderline-BFQg7hSu.mjs} +1 -1
  153. package/dist/{Toggle-BwrTz2fn.mjs → Toggle-CFvYpFN8.mjs} +1 -1
  154. package/dist/{Trash-6WVc1Zo7.mjs → Trash-CS1BAv8d.mjs} +1 -1
  155. package/dist/{TrendDown-C6wcWlmc.mjs → TrendDown-C8RVuMbz.mjs} +1 -1
  156. package/dist/{TrendUp-BWejhArt.mjs → TrendUp-eYJN1A6n.mjs} +1 -1
  157. package/dist/{Unplugged-0SGS9CKk.mjs → Unplugged-j7jw4iIL.mjs} +1 -1
  158. package/dist/{UpDownCaret-ClBkt-ma.mjs → UpDownCaret-DdXKQT69.mjs} +1 -1
  159. package/dist/{VerticalLine-DjZxhqxX.mjs → VerticalLine-DU1-BZfR.mjs} +1 -1
  160. package/dist/{Warning-Bj_gmfqP.mjs → Warning-C1kQzf8A.mjs} +1 -1
  161. package/dist/{WarningNoOutline-DBvoW0_1.mjs → WarningNoOutline-CXC1MVzS.mjs} +1 -1
  162. package/dist/{YesAnd-COl6p1Q_.mjs → YesAnd-C18ssrgG.mjs} +1 -1
  163. package/dist/ai-assistant.css +645 -0
  164. package/dist/{emoji-BYYHYzOu.mjs → emoji-CJuut-ic.mjs} +1 -88
  165. package/dist/react.es.js +5 -0
  166. package/dist/{ai-assistant.umd.js → react.umd.js} +15685 -1695
  167. package/dist/{reactComponent-DigPLrfy.mjs → reactComponent-B4VA_e0q.mjs} +15712 -1646
  168. package/express.js +2 -2
  169. package/package.json +10 -5
  170. package/react.js +1 -1
  171. package/src/backend/express.test.ts +177 -10
  172. package/src/backend/express.ts +93 -12
  173. package/src/frontend/AiAssistant.test.tsx +1 -1
  174. package/src/frontend/AiAssistant.tsx +9 -2
  175. package/src/frontend/components/AiAssistantComponent.scss +24 -0
  176. package/src/frontend/components/AiAssistantComponent.test.tsx +262 -3
  177. package/src/frontend/components/AiAssistantComponent.tsx +238 -10
  178. package/src/frontend/components/messageRenderers/AssistantMessage.test.tsx +154 -0
  179. package/src/frontend/components/messageRenderers/AssistantMessage.tsx +104 -0
  180. package/src/frontend/components/messageRenderers/UserMessage.test.tsx +103 -0
  181. package/src/frontend/components/messageRenderers/UserMessage.tsx +43 -0
  182. package/src/frontend/reactComponent.ts +2 -0
  183. package/src/frontend/tools/generatedSdks.ts +43 -0
  184. package/src/frontend/tools/llm-tools/getCurrentUser.test.ts +100 -0
  185. package/src/frontend/tools/llm-tools/getCurrentUser.ts +158 -0
  186. package/src/frontend/tools/llm-tools/index.ts +6 -0
  187. package/src/frontend/tools/llm-tools/kazooPlatform/getRewardsAndRecognitionCurrentUser.test.ts +76 -0
  188. package/src/frontend/tools/llm-tools/kazooPlatform/getRewardsAndRecognitionCurrentUser.ts +36 -0
  189. package/src/frontend/tools/llm-tools/pulsePlatform/.gitkeep +0 -0
  190. package/src/frontend/tools/llm-tools/shared/.gitkeep +0 -0
  191. package/src/frontend/tools/useAiAssistantSession.test.ts +768 -0
  192. package/src/frontend/tools/useAiAssistantSession.ts +374 -0
  193. package/types.d.ts +67 -1
  194. package/dist/PinnedIconAward-CWgNNAPU.mjs +0 -20
  195. package/dist/PinnedIconBrightIdea-Cj1UUoKb.mjs +0 -20
  196. package/dist/PinnedIconBullseye-SN7Ps5fC.mjs +0 -20
  197. package/dist/PinnedIconChampagne-CMj1TZw7.mjs +0 -20
  198. package/dist/PinnedIconCheer-osAldHSN.mjs +0 -20
  199. package/dist/PinnedIconCrown-u0dSUhVZ.mjs +0 -20
  200. package/dist/PinnedIconDiamond-Byiswgk3.mjs +0 -20
  201. package/dist/PinnedIconHoleInOne-DzSbNQ5B.mjs +0 -20
  202. package/dist/PinnedIconMedal-YQYBs9V1.mjs +0 -20
  203. package/dist/PinnedIconMetrics-D2Li4Bnf.mjs +0 -20
  204. package/dist/PinnedIconPin-Xxp0coP8.mjs +0 -20
  205. package/dist/PinnedIconRockAndRoll-DlbejnZU.mjs +0 -23
  206. package/dist/PinnedIconRocket-B9dqD0uv.mjs +0 -42
  207. package/dist/PinnedIconStrength-BXA19IfC.mjs +0 -20
  208. package/dist/PinnedIconTeddyBear-D2n83xPZ.mjs +0 -20
  209. package/dist/ai-assistant.es.js +0 -4
@@ -1,11 +1,270 @@
1
1
  import React from "react";
2
2
 
3
- import { render } from "@kazoohr/test";
3
+ import { ToastProvider } from "@kazoohr/confetti";
4
+ import { act, fireEvent, render, screen } from "@kazoohr/test";
5
+
6
+ import { useAiAssistantSession } from "../tools/useAiAssistantSession";
4
7
 
5
8
  import { AiAssistantComponent } from "./AiAssistantComponent";
6
9
 
10
+ jest.mock("../tools/useAiAssistantSession", () => ({
11
+ useAiAssistantSession: jest.fn(),
12
+ }));
13
+
14
+ // Need to mock micromark because it uses ES modules
15
+ jest.mock("micromark", () => ({
16
+ micromark: (text: string) => `<div>${text}</div>`,
17
+ }));
18
+
7
19
  describe("AiAssistantComponent", () => {
8
- it("should render", () => {
9
- render(<AiAssistantComponent />);
20
+ const mockSendUserMessage = jest.fn();
21
+ const mockSetMessages = jest.fn();
22
+
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ jest.useFakeTimers();
26
+
27
+ (useAiAssistantSession as jest.Mock).mockReturnValue({
28
+ messages: [],
29
+ sendUserMessage: mockSendUserMessage,
30
+ isLoading: false,
31
+ debugInfo: {},
32
+ setMessages: mockSetMessages,
33
+ });
34
+ });
35
+
36
+ afterEach(() => {
37
+ jest.useRealTimers();
38
+ });
39
+
40
+ const renderWithToast = (ui: React.ReactNode) => {
41
+ return render(<ToastProvider>{ui}</ToastProvider>);
42
+ };
43
+
44
+ it("renders initial empty state", () => {
45
+ renderWithToast(<AiAssistantComponent />);
46
+
47
+ expect(
48
+ screen.getByText(
49
+ "No messages yet! Get started by asking a question below."
50
+ )
51
+ ).toBeInTheDocument();
52
+ expect(screen.getByText("Send")).toBeInTheDocument();
53
+ expect(screen.getByText("Clear current chat")).toBeInTheDocument();
54
+ });
55
+
56
+ it("handles message sending", async () => {
57
+ renderWithToast(<AiAssistantComponent />);
58
+
59
+ const textarea = screen.getByRole("textbox");
60
+ const sendButton = screen.getByText("Send");
61
+
62
+ // Clear the default text first
63
+ fireEvent.change(textarea, { target: { value: "" } });
64
+ expect(sendButton).toBeDisabled();
65
+
66
+ fireEvent.change(textarea, { target: { value: "This is a test message" } });
67
+ expect(sendButton).not.toBeDisabled();
68
+
69
+ // Send the message
70
+ fireEvent.click(sendButton);
71
+ expect(mockSendUserMessage).toHaveBeenCalledWith("This is a test message");
72
+ });
73
+
74
+ it("handles ctrl+enter submission", () => {
75
+ renderWithToast(<AiAssistantComponent />);
76
+
77
+ const textarea = screen.getByRole("textbox");
78
+ const sendButton = screen.getByText("Send");
79
+
80
+ fireEvent.change(textarea, { target: { value: "This is a test message" } });
81
+ expect(sendButton).not.toBeDisabled();
82
+
83
+ fireEvent.keyDown(textarea, { key: "Space", ctrlKey: false });
84
+
85
+ expect(mockSendUserMessage).not.toHaveBeenCalled();
86
+
87
+ fireEvent.keyDown(textarea, { key: "Enter", ctrlKey: true });
88
+
89
+ expect(mockSendUserMessage).toHaveBeenCalledWith("This is a test message");
90
+ });
91
+
92
+ it("displays loading state", () => {
93
+ (useAiAssistantSession as jest.Mock).mockReturnValue({
94
+ messages: [
95
+ {
96
+ role: "user",
97
+ parts: [{ type: "text", text: "Hello" }],
98
+ },
99
+ ],
100
+ sendUserMessage: mockSendUserMessage,
101
+ isLoading: true,
102
+ debugInfo: {},
103
+ setMessages: mockSetMessages,
104
+ });
105
+
106
+ const { baseElement } = renderWithToast(<AiAssistantComponent />);
107
+
108
+ expect(baseElement.innerHTML).toContain("loading");
109
+ });
110
+
111
+ it("display the LLM loading state", () => {
112
+ (useAiAssistantSession as jest.Mock).mockReturnValue({
113
+ messages: [
114
+ {
115
+ role: "user",
116
+ parts: [{ type: "text", text: "Hello" }],
117
+ },
118
+ ],
119
+ sendUserMessage: mockSendUserMessage,
120
+ isLoading: true,
121
+ debugInfo: {},
122
+ setMessages: mockSetMessages,
123
+ });
124
+
125
+ renderWithToast(<AiAssistantComponent />);
126
+
127
+ expect(screen.getByText("Thinking...")).toBeInTheDocument();
128
+ });
129
+
130
+ it("displays messages correctly", () => {
131
+ (useAiAssistantSession as jest.Mock).mockReturnValue({
132
+ messages: [
133
+ {
134
+ role: "user",
135
+ parts: [{ type: "text", text: "Hello" }],
136
+ },
137
+ {
138
+ role: "assistant",
139
+ parts: [{ type: "text", text: "Hi there!" }],
140
+ },
141
+ {
142
+ role: "assistant",
143
+ parts: [
144
+ { type: "toolCall", toolName: "testTool", args: { test: true } },
145
+ {
146
+ type: "toolResult",
147
+ toolName: "testTool",
148
+ result: { success: true },
149
+ },
150
+ ],
151
+ },
152
+ {
153
+ role: "tool",
154
+ parts: [
155
+ {
156
+ type: "UNKNOWN!",
157
+ toolName: "testTool",
158
+ result: { success: true },
159
+ },
160
+ ],
161
+ },
162
+ ],
163
+ sendUserMessage: mockSendUserMessage,
164
+ isLoading: false,
165
+ debugInfo: {},
166
+ setMessages: mockSetMessages,
167
+ });
168
+
169
+ const { baseElement } = renderWithToast(<AiAssistantComponent />);
170
+ const html = baseElement.innerHTML;
171
+
172
+ expect(html).toContain("Hi there!");
173
+ });
174
+
175
+ it("handles debug mode toggle", () => {
176
+ (useAiAssistantSession as jest.Mock).mockReturnValue({
177
+ messages: [
178
+ { role: "user", parts: [{ type: "text", text: "Hello world!" }] },
179
+ ],
180
+ sendUserMessage: mockSendUserMessage,
181
+ isLoading: false,
182
+ debugInfo: { test: true },
183
+ setMessages: mockSetMessages,
184
+ });
185
+
186
+ renderWithToast(<AiAssistantComponent />);
187
+
188
+ // Initially debug info is hidden
189
+ const debugButton = screen.getByTitle("Show debug info");
190
+ expect(debugButton).toBeInTheDocument();
191
+
192
+ // Show debug info
193
+ fireEvent.click(debugButton);
194
+
195
+ // Find the tabs
196
+ const tabs = screen.getAllByRole("tab");
197
+ expect(tabs[0]).toHaveTextContent("History");
198
+ expect(tabs[1]).toHaveTextContent("Debug");
199
+
200
+ // Click the messages tab
201
+ fireEvent.click(tabs[0]);
202
+
203
+ expect(screen.getByText("Hello world!")).toBeInTheDocument();
204
+
205
+ // Click the debug tab
206
+ fireEvent.click(tabs[1]);
207
+
208
+ // Check that the debug info is displayed
209
+ expect(screen.getByTestId("debug-textarea")).toHaveTextContent("test");
210
+ });
211
+
212
+ it("handles expand/collapse functionality", () => {
213
+ (useAiAssistantSession as jest.Mock).mockReturnValue({
214
+ messages: [{ role: "user", parts: [{ type: "text", text: "test" }] }],
215
+ sendUserMessage: mockSendUserMessage,
216
+ isLoading: false,
217
+ debugInfo: {},
218
+ setMessages: mockSetMessages,
219
+ });
220
+
221
+ renderWithToast(<AiAssistantComponent />);
222
+
223
+ const collapseButton = screen.getByTitle("Collapse chat window");
224
+ expect(collapseButton).toBeInTheDocument();
225
+ fireEvent.click(collapseButton);
226
+
227
+ const expandButton = screen.getByTitle("Expand chat window");
228
+ expect(expandButton).toBeInTheDocument();
229
+ fireEvent.click(expandButton);
230
+
231
+ expect(screen.getByTitle("Collapse chat window")).toBeInTheDocument();
232
+ });
233
+
234
+ it("handles clearing chat", () => {
235
+ renderWithToast(<AiAssistantComponent />);
236
+
237
+ fireEvent.click(screen.getByText("Clear current chat"));
238
+ expect(mockSetMessages).toHaveBeenCalledWith([]);
239
+ });
240
+
241
+ it("handles errors", () => {
242
+ renderWithToast(<AiAssistantComponent />);
243
+
244
+ const error = new Error("Test error");
245
+ const { onError } = (useAiAssistantSession as jest.Mock).mock.calls[0][0];
246
+
247
+ act(() => {
248
+ onError(error);
249
+ });
250
+
251
+ expect(screen.getByText("Test error")).toBeInTheDocument();
252
+ });
253
+
254
+ it("handles scroll to bottom on message send", () => {
255
+ const mockRef = { current: { scrollTop: 0, scrollHeight: 100 } };
256
+ jest.spyOn(React, "useRef").mockReturnValue(mockRef);
257
+
258
+ renderWithToast(<AiAssistantComponent />);
259
+
260
+ const { onBeforeLlmCall } = (useAiAssistantSession as jest.Mock).mock
261
+ .calls[0][0];
262
+
263
+ act(() => {
264
+ onBeforeLlmCall();
265
+ jest.advanceTimersByTime(10); // Advance past the setTimeout
266
+ });
267
+
268
+ expect(mockRef.current.scrollTop).toBe(mockRef.current.scrollHeight);
10
269
  });
11
270
  });
@@ -1,18 +1,246 @@
1
1
  import React from "react";
2
2
 
3
- import { Card, TextArea } from "@kazoohr/confetti";
4
- import { setState } from "@kazoohr/helpers";
3
+ import {
4
+ BRANDING_CLASSES,
5
+ Button,
6
+ Card,
7
+ Flex,
8
+ Illustration,
9
+ Loader,
10
+ PageHeader,
11
+ Spacer,
12
+ Tab,
13
+ Tabs,
14
+ Text,
15
+ TextArea,
16
+ useToast,
17
+ } from "@kazoohr/confetti";
18
+ import {
19
+ composeSimpleFunctions,
20
+ getClosureFor,
21
+ setState,
22
+ setStateValue,
23
+ } from "@kazoohr/helpers";
24
+ import { ToolDeclaration } from "@kazoohr/llm/types";
25
+
26
+ import { useAiAssistantSession } from "../tools/useAiAssistantSession";
27
+
28
+ import styles from "./AiAssistantComponent.scss";
29
+ import { AssistantMessage } from "./messageRenderers/AssistantMessage";
30
+ import { UserMessage } from "./messageRenderers/UserMessage";
5
31
 
6
32
  /**
7
- * This is a placeholder 😎
33
+ * Our AI Assistant component, for managing the LLM session and displaying a
34
+ * single chat session with the LLM.
8
35
  */
9
- export function AiAssistantComponent() {
10
- // TODO: AI assistant go here.
11
- const [value, setValue] = React.useState("");
36
+ // eslint-disable-next-line complexity
37
+ export function AiAssistantComponent({ tools }: { tools?: ToolDeclaration[] }) {
38
+ const { error: errorToast } = useToast();
39
+ const [showDebugInfo, setShowDebugInfo] = React.useState(false);
40
+ const [prompt, setPrompt] = React.useState(
41
+ 'This is a test! Give me a "Hello world! From <your model>". Then, use your tool calls to tell me about myself. Be comprehensive in your answer.'
42
+ );
43
+ const textareaRef = React.useRef<HTMLTextAreaElement>(null);
44
+ const scrollWindowRef = React.useRef<HTMLDivElement>(null);
45
+ const scrollWindowToBottom = React.useCallback(
46
+ // istanbul ignore next - UI-related stuff
47
+ () => {
48
+ setTimeout(() => {
49
+ if (scrollWindowRef.current) {
50
+ scrollWindowRef.current.scrollTop =
51
+ scrollWindowRef.current.scrollHeight;
52
+ }
53
+ }, 10);
54
+ },
55
+ []
56
+ );
57
+ const { messages, sendUserMessage, isLoading, debugInfo, setMessages } =
58
+ useAiAssistantSession({
59
+ onBeforeLlmCall: scrollWindowToBottom,
60
+ onAfterLlmCall: composeSimpleFunctions(
61
+ setStateValue(setPrompt, ""),
62
+ scrollWindowToBottom,
63
+ textareaRef.current?.focus
64
+ ),
65
+ onError: (error) => errorToast(error.message),
66
+ onChunkAdded: scrollWindowToBottom,
67
+ tools,
68
+ });
69
+ const [fullHeight, setFullHeight] = React.useState(true);
70
+ const [tab, setTab] = React.useState<"messages" | "debug">("messages");
71
+ React.useEffect(() => {
72
+ // istanbul ignore else - noop
73
+ if (textareaRef.current) {
74
+ // Ctrl + enter submits the form
75
+ const handleKeyDown = (event: KeyboardEvent) => {
76
+ if (event.ctrlKey && event.key === "Enter") {
77
+ event.preventDefault();
78
+ sendUserMessage(prompt);
79
+ }
80
+ };
81
+ textareaRef.current.addEventListener("keydown", handleKeyDown);
82
+ return () => {
83
+ textareaRef.current?.removeEventListener("keydown", handleKeyDown);
84
+ };
85
+ }
86
+ }, [prompt, sendUserMessage]);
87
+
88
+ const canSubmit = prompt.trim().length > 5;
89
+
12
90
  return (
13
- <Card background="confetti">
14
- Hello world! This is the AI assistant.
15
- <TextArea id="id" value={value} onChange={setState(setValue)} />
16
- </Card>
91
+ <div>
92
+ <style type="text/css">
93
+ {`
94
+ .streaming-text-resizing-container {
95
+ overflow-y: hidden;
96
+ transition: height 0.1s ease-in-out;
97
+ }
98
+
99
+ .markdown {
100
+ line-height: 1.4;
101
+ }
102
+
103
+ .markdown * + p {
104
+ margin-top: 12px;
105
+ }
106
+
107
+ .markdown ul, .markdown ol {
108
+ margin-top: 12px;
109
+ margin-left: 24px;
110
+ }
111
+
112
+ .scroll-window {
113
+ padding: 12px 4px 16px 0;
114
+ overflow-y: scroll;
115
+ overflow-x: hidden;
116
+ height: min(100vh, max(50vh, 400px));
117
+ }
118
+ `}
119
+ </style>
120
+ <PageHeader
121
+ header="AI Assistant"
122
+ withBottomBorder={false}
123
+ renderRightContent={() => (
124
+ <Flex gap="12px">
125
+ <Button
126
+ icon={fullHeight ? "caret_up" : "caret_dn"}
127
+ onClick={setStateValue(setFullHeight, !fullHeight)}
128
+ title={fullHeight ? "Collapse chat window" : "Expand chat window"}
129
+ variant="transparent"
130
+ size="extra_small"
131
+ >
132
+ {fullHeight ? "Collapse" : "Expand"}
133
+ </Button>
134
+ <Button
135
+ icon="eye"
136
+ onClick={composeSimpleFunctions(
137
+ setStateValue(setShowDebugInfo, !showDebugInfo),
138
+ setStateValue(setTab, "messages")
139
+ )}
140
+ title={showDebugInfo ? "Hide debug info" : "Show debug info"}
141
+ variant="transparent"
142
+ size="extra_small"
143
+ >
144
+ {showDebugInfo ? "Debugging" : "Debug"}
145
+ </Button>
146
+ <Button
147
+ icon="close"
148
+ variant="transparent"
149
+ onClick={setStateValue(setMessages, [])}
150
+ size="extra_small"
151
+ >
152
+ Clear current chat
153
+ </Button>
154
+ </Flex>
155
+ )}
156
+ />
157
+ {showDebugInfo && (
158
+ <>
159
+ <Tabs value={tab} onChange={setState(setTab)}>
160
+ <Tab value="messages" data-testid="messages-tab">
161
+ History
162
+ </Tab>
163
+ <Tab value="debug" data-testid="debug-tab">
164
+ Debug
165
+ </Tab>
166
+ </Tabs>
167
+ <Spacer />
168
+ </>
169
+ )}
170
+ {tab === "debug" && (
171
+ <Card>
172
+ <textarea
173
+ value={JSON.stringify(debugInfo, null, 2)}
174
+ className={styles["ai-assistant-debug-textarea"]}
175
+ readOnly
176
+ data-testid="debug-textarea"
177
+ />
178
+ </Card>
179
+ )}
180
+ {tab === "messages" &&
181
+ (messages.length === 0 ? (
182
+ <Flex direction="column" alignItems="center" gap="12px">
183
+ <Illustration illustration="feedback" size={196} />
184
+ <Text slim size="large">
185
+ No messages yet! Get started by asking a question below.
186
+ </Text>
187
+ </Flex>
188
+ ) : (
189
+ <>
190
+ <div
191
+ className={fullHeight ? "" : "scroll-window"}
192
+ ref={scrollWindowRef}
193
+ >
194
+ <Flex direction="column" gap="16px" justifyContent="flex-end">
195
+ {messages.map((message, index) => {
196
+ if (message.role === "user") {
197
+ return <UserMessage key={index} message={message} />;
198
+ }
199
+ return <AssistantMessage key={index} message={message} />;
200
+ })}
201
+ {isLoading && (
202
+ <div style={{ height: "100px" }}>
203
+ <Loader />
204
+ </div>
205
+ )}
206
+ </Flex>
207
+ </div>
208
+ <Spacer size="large" />
209
+ </>
210
+ ))}
211
+ <Spacer />
212
+ <Card
213
+ className={BRANDING_CLASSES.PRIMARY_COLOR_BACKGROUND}
214
+ background="two-tone"
215
+ data-testid="toggle-panel-controls"
216
+ >
217
+ <TextArea
218
+ id="id"
219
+ value={prompt}
220
+ onChange={setState(setPrompt)}
221
+ fullWidth
222
+ className={styles["ai-assistant-textarea"]}
223
+ placeholder="Ask a question..."
224
+ inputRef={textareaRef}
225
+ autoFocus
226
+ disabled={isLoading}
227
+ />
228
+ <Spacer size="small" />
229
+ <Flex justifyContent="flex-end" gap="8px">
230
+ <Button
231
+ onClick={getClosureFor(sendUserMessage, prompt)}
232
+ disabled={!canSubmit || isLoading}
233
+ variant="feature_inverted"
234
+ size="large"
235
+ icon="people"
236
+ style={{
237
+ cursor: !canSubmit || isLoading ? "not-allowed" : "pointer",
238
+ }}
239
+ >
240
+ {isLoading ? "Thinking..." : "Send"}
241
+ </Button>
242
+ </Flex>
243
+ </Card>
244
+ </div>
17
245
  );
18
246
  }
@@ -0,0 +1,154 @@
1
+ import React from "react";
2
+
3
+ import { render, screen } from "@kazoohr/test";
4
+
5
+ import { type ChatItemWithTimestamp } from "../../tools/useAiAssistantSession";
6
+
7
+ import { AssistantMessage } from "./AssistantMessage";
8
+
9
+ jest.mock("micromark", () => ({
10
+ micromark: (text: string) => text,
11
+ }));
12
+
13
+ describe("AssistantMessage", () => {
14
+ const mockTimestamp = new Date("2024-01-01T12:00:00Z");
15
+
16
+ it("renders text message correctly", () => {
17
+ const message: ChatItemWithTimestamp = {
18
+ role: "model",
19
+ timestamp: mockTimestamp,
20
+ parts: [
21
+ {
22
+ type: "text",
23
+ text: "Hello world",
24
+ },
25
+ ],
26
+ };
27
+
28
+ render(<AssistantMessage message={message} />);
29
+
30
+ expect(screen.getByText("assistant")).toBeInTheDocument();
31
+ expect(screen.getByText("Hello world")).toBeInTheDocument();
32
+ expect(
33
+ screen.getByText(mockTimestamp.toLocaleString())
34
+ ).toBeInTheDocument();
35
+ });
36
+
37
+ it("renders markdown text correctly", () => {
38
+ const message: ChatItemWithTimestamp = {
39
+ role: "model",
40
+ timestamp: mockTimestamp,
41
+ parts: [
42
+ {
43
+ type: "text",
44
+ text: "# Hello\n\n**Bold text**",
45
+ },
46
+ ],
47
+ };
48
+
49
+ render(<AssistantMessage message={message} />);
50
+
51
+ const content = document.querySelector(".markdown");
52
+ expect(content).toBeInTheDocument();
53
+ expect(content?.innerHTML).toContain("# Hello\n\n**Bold text**");
54
+ });
55
+
56
+ it("renders tool calls correctly", () => {
57
+ const message: ChatItemWithTimestamp = {
58
+ role: "model",
59
+ timestamp: mockTimestamp,
60
+ parts: [
61
+ {
62
+ type: "toolCall",
63
+ toolName: "testTool",
64
+ args: { test: true },
65
+ },
66
+ ],
67
+ };
68
+
69
+ render(<AssistantMessage message={message} />);
70
+
71
+ expect(screen.getByTitle("Tool call: testTool")).toBeInTheDocument();
72
+ expect(screen.getByText("Fetch data")).toBeInTheDocument();
73
+ });
74
+
75
+ it("renders both text and tool calls together", () => {
76
+ const message: ChatItemWithTimestamp = {
77
+ role: "model",
78
+ timestamp: mockTimestamp,
79
+ parts: [
80
+ {
81
+ type: "text",
82
+ text: "Hello world",
83
+ },
84
+ {
85
+ type: "toolCall",
86
+ toolName: "testTool",
87
+ args: { test: true },
88
+ },
89
+ ],
90
+ };
91
+
92
+ render(<AssistantMessage message={message} />);
93
+
94
+ expect(screen.getByText("Hello world")).toBeInTheDocument();
95
+ expect(screen.getByTitle("Tool call: testTool")).toBeInTheDocument();
96
+ expect(screen.getByText("Fetch data")).toBeInTheDocument();
97
+ });
98
+
99
+ it("renders multiple tool calls", () => {
100
+ const message: ChatItemWithTimestamp = {
101
+ role: "model",
102
+ timestamp: mockTimestamp,
103
+ parts: [
104
+ {
105
+ type: "toolCall",
106
+ toolName: "tool1",
107
+ args: { test: true },
108
+ },
109
+ {
110
+ type: "toolCall",
111
+ toolName: "tool2",
112
+ args: { test: true },
113
+ },
114
+ ],
115
+ };
116
+
117
+ render(<AssistantMessage message={message} />);
118
+
119
+ expect(screen.getByTitle("Tool call: tool1")).toBeInTheDocument();
120
+ expect(screen.getByTitle("Tool call: tool2")).toBeInTheDocument();
121
+ expect(screen.getAllByText("Fetch data")).toHaveLength(2);
122
+ });
123
+
124
+ it("returns null for empty message parts", () => {
125
+ const message: ChatItemWithTimestamp = {
126
+ role: "model",
127
+ timestamp: mockTimestamp,
128
+ parts: [],
129
+ };
130
+
131
+ const { container } = render(<AssistantMessage message={message} />);
132
+ expect(container.firstChild).toBeNull();
133
+ });
134
+
135
+ it("aggregates multiple text parts", () => {
136
+ const message: ChatItemWithTimestamp = {
137
+ role: "model",
138
+ timestamp: mockTimestamp,
139
+ parts: [
140
+ {
141
+ type: "text",
142
+ text: "Hello",
143
+ },
144
+ {
145
+ type: "text",
146
+ text: " world",
147
+ },
148
+ ],
149
+ };
150
+
151
+ render(<AssistantMessage message={message} />);
152
+ expect(screen.getByText("Hello world")).toBeInTheDocument();
153
+ });
154
+ });