unmagic-components 0.3.0 → 0.4.0

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 (292) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -1
  3. data/README.md +506 -1
  4. data/app/assets/icons/lucide/LICENSE +43 -0
  5. data/app/assets/icons/lucide/arrow-down.svg +1 -0
  6. data/app/assets/icons/lucide/arrow-left.svg +1 -0
  7. data/app/assets/icons/lucide/arrow-up.svg +1 -0
  8. data/app/assets/icons/lucide/ban.svg +1 -0
  9. data/app/assets/icons/lucide/brain.svg +1 -0
  10. data/app/assets/icons/lucide/check.svg +1 -0
  11. data/app/assets/icons/lucide/chevron-down.svg +1 -0
  12. data/app/assets/icons/lucide/chevron-left.svg +1 -0
  13. data/app/assets/icons/lucide/chevron-right.svg +1 -0
  14. data/app/assets/icons/lucide/circle-check.svg +1 -0
  15. data/app/assets/icons/lucide/circle-dashed.svg +1 -0
  16. data/app/assets/icons/lucide/circle-question-mark.svg +1 -0
  17. data/app/assets/icons/lucide/circle-stop.svg +1 -0
  18. data/app/assets/icons/lucide/circle-x.svg +1 -0
  19. data/app/assets/icons/lucide/clock.svg +1 -0
  20. data/app/assets/icons/lucide/copy.svg +1 -0
  21. data/app/assets/icons/lucide/ellipsis-vertical.svg +1 -0
  22. data/app/assets/icons/lucide/file.svg +1 -0
  23. data/app/assets/icons/lucide/folder.svg +1 -0
  24. data/app/assets/icons/lucide/grip-vertical.svg +1 -0
  25. data/app/assets/icons/lucide/info.svg +1 -0
  26. data/app/assets/icons/lucide/lightbulb.svg +1 -0
  27. data/app/assets/icons/lucide/list-checks.svg +1 -0
  28. data/app/assets/icons/lucide/loader-circle.svg +1 -0
  29. data/app/assets/icons/lucide/messages-square.svg +1 -0
  30. data/app/assets/icons/lucide/paperclip.svg +1 -0
  31. data/app/assets/icons/lucide/pencil.svg +1 -0
  32. data/app/assets/icons/lucide/plus.svg +1 -0
  33. data/app/assets/icons/lucide/rotate-cw.svg +1 -0
  34. data/app/assets/icons/lucide/shield-alert.svg +1 -0
  35. data/app/assets/icons/lucide/timer.svg +1 -0
  36. data/app/assets/icons/lucide/triangle-alert.svg +1 -0
  37. data/app/assets/icons/lucide/wrench.svg +1 -0
  38. data/app/assets/icons/lucide/x.svg +1 -0
  39. data/app/assets/javascripts/unmagic/components/ai_chat.js +142 -0
  40. data/app/assets/javascripts/unmagic/components/autoscroll.js +103 -0
  41. data/app/assets/javascripts/unmagic/components/board.js +65 -0
  42. data/app/assets/javascripts/unmagic/components/dropzone.js +206 -0
  43. data/app/assets/javascripts/unmagic/components/elapsed.js +64 -0
  44. data/app/assets/javascripts/unmagic/components/optimistic.js +71 -0
  45. data/app/assets/javascripts/unmagic/components/placement.js +30 -0
  46. data/app/assets/javascripts/unmagic/components/slash_menu.js +186 -0
  47. data/app/assets/javascripts/unmagic/components/sortable.js +6 -0
  48. data/app/assets/javascripts/unmagic/components/sortable_item.js +122 -0
  49. data/app/assets/javascripts/unmagic/components/sortable_list.js +135 -0
  50. data/app/assets/javascripts/unmagic/components/sortable_param.js +14 -0
  51. data/app/assets/javascripts/unmagic/components/sortable_session.js +502 -0
  52. data/app/assets/javascripts/unmagic/components/streaming_markdown.js +269 -0
  53. data/app/assets/javascripts/unmagic/components/toasts.js +14 -4
  54. data/app/assets/javascripts/unmagic/components/toolbar.js +76 -0
  55. data/app/assets/javascripts/unmagic/components/upsert.js +4 -20
  56. data/app/assets/javascripts/unmagic/components.js +10 -0
  57. data/app/assets/tailwind/unmagic_components/engine.css +1515 -0
  58. data/lib/unmagic/components/action_view_helpers.rb +558 -3
  59. data/lib/unmagic/components/ai_chat/action_bar.rb +70 -0
  60. data/lib/unmagic/components/ai_chat/attachments.rb +119 -0
  61. data/lib/unmagic/components/ai_chat/branch_picker.rb +65 -0
  62. data/lib/unmagic/components/ai_chat/citation.rb +89 -0
  63. data/lib/unmagic/components/ai_chat/composer.rb +160 -0
  64. data/lib/unmagic/components/ai_chat/failure.rb +80 -0
  65. data/lib/unmagic/components/ai_chat/message.rb +142 -0
  66. data/lib/unmagic/components/ai_chat/payload.rb +93 -0
  67. data/lib/unmagic/components/ai_chat/permission.rb +143 -0
  68. data/lib/unmagic/components/ai_chat/plan.rb +91 -0
  69. data/lib/unmagic/components/ai_chat/proposal.rb +102 -0
  70. data/lib/unmagic/components/ai_chat/reasoning.rb +75 -0
  71. data/lib/unmagic/components/ai_chat/request.rb +206 -0
  72. data/lib/unmagic/components/ai_chat/section.rb +53 -0
  73. data/lib/unmagic/components/ai_chat/slash_menu.rb +68 -0
  74. data/lib/unmagic/components/ai_chat/tool_call.rb +181 -0
  75. data/lib/unmagic/components/ai_chat/transcript.rb +68 -0
  76. data/lib/unmagic/components/ai_chat/welcome.rb +73 -0
  77. data/lib/unmagic/components/ai_chat/workspace.rb +74 -0
  78. data/lib/unmagic/components/ai_chat.rb +31 -0
  79. data/lib/unmagic/components/avatar.rb +134 -0
  80. data/lib/unmagic/components/board.rb +182 -0
  81. data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/application_controller.rb +80 -0
  82. data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/demos_controller.rb +137 -0
  83. data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/pages_controller.rb +40 -0
  84. data/lib/unmagic/components/browser/app/helpers/unmagic/components/browser/application_helper.rb +52 -0
  85. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/board_store.rb +88 -0
  86. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/catalog.rb +65 -0
  87. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/pager.rb +11 -0
  88. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/profile.rb +25 -0
  89. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/reply.rb +72 -0
  90. data/lib/unmagic/components/browser/app/models/unmagic/components/browser/thing.rb +31 -0
  91. data/lib/unmagic/components/browser/app/views/layouts/unmagic/components/browser/application.html.erb +65 -0
  92. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_code.html.erb +7 -0
  93. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_crumb.html.erb +7 -0
  94. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_example.html.erb +37 -0
  95. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_nav.html.erb +17 -0
  96. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/demos/edit_profile.html.erb +9 -0
  97. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_conversation.html.erb +101 -0
  98. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_live.html.erb +53 -0
  99. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_thumbnail.html.erb +7 -0
  100. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_welcome.html.erb +12 -0
  101. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_always.html.erb +6 -0
  102. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_hover.html.erb +12 -0
  103. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_thumbnail.html.erb +5 -0
  104. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_dropzone.html.erb +17 -0
  105. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_sent.html.erb +11 -0
  106. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_thumbnail.html.erb +6 -0
  107. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_branch_picker/_positions.html.erb +5 -0
  108. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_branch_picker/_thumbnail.html.erb +3 -0
  109. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_compact.html.erb +10 -0
  110. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_quote.html.erb +11 -0
  111. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_thumbnail.html.erb +7 -0
  112. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_parts.html.erb +14 -0
  113. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_states.html.erb +8 -0
  114. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_thumbnail.html.erb +5 -0
  115. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_full.html.erb +11 -0
  116. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_plain.html.erb +3 -0
  117. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_thumbnail.html.erb +5 -0
  118. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_parts.html.erb +23 -0
  119. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_states.html.erb +9 -0
  120. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_thumbnail.html.erb +4 -0
  121. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_turns.html.erb +16 -0
  122. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_json.html.erb +4 -0
  123. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_text.html.erb +4 -0
  124. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_thumbnail.html.erb +3 -0
  125. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_answered.html.erb +12 -0
  126. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_thumbnail.html.erb +7 -0
  127. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_waiting.html.erb +12 -0
  128. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_empty.html.erb +3 -0
  129. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_states.html.erb +9 -0
  130. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_thumbnail.html.erb +7 -0
  131. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_decided.html.erb +11 -0
  132. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_inline.html.erb +18 -0
  133. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_thumbnail.html.erb +6 -0
  134. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_settled.html.erb +6 -0
  135. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_thinking.html.erb +5 -0
  136. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_thumbnail.html.erb +3 -0
  137. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_answered.html.erb +10 -0
  138. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_choices.html.erb +13 -0
  139. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_form.html.erb +9 -0
  140. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_thumbnail.html.erb +8 -0
  141. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_slash_menu/_composer.html.erb +15 -0
  142. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_slash_menu/_thumbnail.html.erb +9 -0
  143. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_made.html.erb +12 -0
  144. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_run.html.erb +25 -0
  145. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_states.html.erb +22 -0
  146. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_thumbnail.html.erb +7 -0
  147. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_welcome/_suggestions.html.erb +11 -0
  148. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_welcome/_thumbnail.html.erb +6 -0
  149. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_workspace/_files.html.erb +12 -0
  150. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_workspace/_thumbnail.html.erb +7 -0
  151. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_group.html.erb +5 -0
  152. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_sizes.html.erb +8 -0
  153. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_thumbnail.html.erb +5 -0
  154. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_tints.html.erb +7 -0
  155. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_in_context.html.erb +14 -0
  156. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_thumbnail.html.erb +5 -0
  157. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_tones.html.erb +8 -0
  158. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_handles.html.erb +13 -0
  159. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_live.html.erb +34 -0
  160. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_thumbnail.html.erb +10 -0
  161. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_sizes.html.erb +12 -0
  162. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_thumbnail.html.erb +4 -0
  163. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_variants.html.erb +6 -0
  164. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_neutral.html.erb +3 -0
  165. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_thumbnail.html.erb +5 -0
  166. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_tones.html.erb +14 -0
  167. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_actions_and_footer.html.erb +7 -0
  168. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_flush_table.html.erb +6 -0
  169. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_link.html.erb +6 -0
  170. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_thumbnail.html.erb +6 -0
  171. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_danger.html.erb +3 -0
  172. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_plain.html.erb +2 -0
  173. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_thumbnail.html.erb +7 -0
  174. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_from_element.html.erb +4 -0
  175. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_thumbnail.html.erb +4 -0
  176. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_with_label.html.erb +3 -0
  177. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_inline.html.erb +5 -0
  178. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_stacked.html.erb +7 -0
  179. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_thumbnail.html.erb +5 -0
  180. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_dialog_tag.html.erb +8 -0
  181. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_modal.html.erb +10 -0
  182. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_thumbnail.html.erb +4 -0
  183. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_countdown.html.erb +3 -0
  184. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_running.html.erb +8 -0
  185. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_thumbnail.html.erb +4 -0
  186. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/empty_state/_default.html.erb +1 -0
  187. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/empty_state/_thumbnail.html.erb +3 -0
  188. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_autogrow.html.erb +4 -0
  189. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_control_classes.html.erb +54 -0
  190. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_fields.html.erb +28 -0
  191. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_states.html.erb +11 -0
  192. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_thumbnail.html.erb +8 -0
  193. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_uuid_field.html.erb +9 -0
  194. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_absolute.html.erb +5 -0
  195. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_relative.html.erb +14 -0
  196. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_thumbnail.html.erb +14 -0
  197. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_actions.html.erb +10 -0
  198. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_labelled.html.erb +6 -0
  199. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_thumbnail.html.erb +14 -0
  200. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_default.html.erb +7 -0
  201. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_thumbnail.html.erb +5 -0
  202. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_with_leading.html.erb +14 -0
  203. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_blocking_out.html.erb +25 -0
  204. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_card.html.erb +7 -0
  205. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_detail_list.html.erb +15 -0
  206. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_page_header.html.erb +7 -0
  207. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_text_and_button.html.erb +17 -0
  208. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_thumbnail.html.erb +10 -0
  209. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_connected.html.erb +17 -0
  210. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_grid.html.erb +20 -0
  211. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_handles.html.erb +22 -0
  212. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_thumbnail.html.erb +7 -0
  213. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_reveal.html.erb +19 -0
  214. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_stopped.html.erb +31 -0
  215. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_thumbnail.html.erb +5 -0
  216. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_deferred.html.erb +6 -0
  217. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_empty.html.erb +5 -0
  218. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_sortable.html.erb +8 -0
  219. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_table_tag.html.erb +4 -0
  220. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_thumbnail.html.erb +4 -0
  221. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_links.html.erb +5 -0
  222. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_panels.html.erb +18 -0
  223. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_thumbnail.html.erb +5 -0
  224. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_above_dialog.html.erb +10 -0
  225. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_flash.html.erb +13 -0
  226. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_stream.html.erb +6 -0
  227. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_thumbnail.html.erb +10 -0
  228. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_placement.html.erb +8 -0
  229. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_term.html.erb +5 -0
  230. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_thumbnail.html.erb +5 -0
  231. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/component.html.erb +58 -0
  232. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/installation.html.erb +47 -0
  233. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/overview.html.erb +49 -0
  234. data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/theming.html.erb +52 -0
  235. data/lib/unmagic/components/browser/assets/browser.css +2 -0
  236. data/lib/unmagic/components/browser/catalog/ai_chat.rb +21 -0
  237. data/lib/unmagic/components/browser/catalog/ai_chat_action_bar.rb +15 -0
  238. data/lib/unmagic/components/browser/catalog/ai_chat_attachments.rb +15 -0
  239. data/lib/unmagic/components/browser/catalog/ai_chat_branch_picker.rb +12 -0
  240. data/lib/unmagic/components/browser/catalog/ai_chat_citation.rb +15 -0
  241. data/lib/unmagic/components/browser/catalog/ai_chat_composer.rb +16 -0
  242. data/lib/unmagic/components/browser/catalog/ai_chat_failure.rb +15 -0
  243. data/lib/unmagic/components/browser/catalog/ai_chat_message.rb +17 -0
  244. data/lib/unmagic/components/browser/catalog/ai_chat_payload.rb +16 -0
  245. data/lib/unmagic/components/browser/catalog/ai_chat_permission.rb +15 -0
  246. data/lib/unmagic/components/browser/catalog/ai_chat_plan.rb +14 -0
  247. data/lib/unmagic/components/browser/catalog/ai_chat_proposal.rb +15 -0
  248. data/lib/unmagic/components/browser/catalog/ai_chat_reasoning.rb +14 -0
  249. data/lib/unmagic/components/browser/catalog/ai_chat_request.rb +17 -0
  250. data/lib/unmagic/components/browser/catalog/ai_chat_slash_menu.rb +14 -0
  251. data/lib/unmagic/components/browser/catalog/ai_chat_tool_call.rb +17 -0
  252. data/lib/unmagic/components/browser/catalog/ai_chat_welcome.rb +13 -0
  253. data/lib/unmagic/components/browser/catalog/ai_chat_workspace.rb +12 -0
  254. data/lib/unmagic/components/browser/catalog/avatar.rb +15 -0
  255. data/lib/unmagic/components/browser/catalog/badge.rb +11 -0
  256. data/lib/unmagic/components/browser/catalog/board.rb +18 -0
  257. data/lib/unmagic/components/browser/catalog/button.rb +14 -0
  258. data/lib/unmagic/components/browser/catalog/callout.rb +13 -0
  259. data/lib/unmagic/components/browser/catalog/card.rb +16 -0
  260. data/lib/unmagic/components/browser/catalog/confirm.rb +14 -0
  261. data/lib/unmagic/components/browser/catalog/copy_button.rb +15 -0
  262. data/lib/unmagic/components/browser/catalog/detail_list.rb +14 -0
  263. data/lib/unmagic/components/browser/catalog/dialog.rb +15 -0
  264. data/lib/unmagic/components/browser/catalog/elapsed.rb +14 -0
  265. data/lib/unmagic/components/browser/catalog/empty_state.rb +11 -0
  266. data/lib/unmagic/components/browser/catalog/forms.rb +21 -0
  267. data/lib/unmagic/components/browser/catalog/local_time.rb +13 -0
  268. data/lib/unmagic/components/browser/catalog/menu.rb +15 -0
  269. data/lib/unmagic/components/browser/catalog/page_header.rb +13 -0
  270. data/lib/unmagic/components/browser/catalog/skeleton.rb +18 -0
  271. data/lib/unmagic/components/browser/catalog/sortable_list.rb +17 -0
  272. data/lib/unmagic/components/browser/catalog/streaming_markdown.rb +17 -0
  273. data/lib/unmagic/components/browser/catalog/table.rb +17 -0
  274. data/lib/unmagic/components/browser/catalog/tabs.rb +15 -0
  275. data/lib/unmagic/components/browser/catalog/toast.rb +17 -0
  276. data/lib/unmagic/components/browser/catalog/tooltip.rb +14 -0
  277. data/lib/unmagic/components/browser/config/routes.rb +41 -0
  278. data/lib/unmagic/components/browser/tailwind/browser.css +33 -0
  279. data/lib/unmagic/components/browser.rb +39 -0
  280. data/lib/unmagic/components/configuration.rb +31 -1
  281. data/lib/unmagic/components/duration.rb +35 -0
  282. data/lib/unmagic/components/elapsed.rb +46 -0
  283. data/lib/unmagic/components/engine.rb +9 -0
  284. data/lib/unmagic/components/icons.rb +34 -26
  285. data/lib/unmagic/components/modal.rb +1 -1
  286. data/lib/unmagic/components/page_header.rb +8 -0
  287. data/lib/unmagic/components/sortable.rb +104 -0
  288. data/lib/unmagic/components/streaming_markdown.rb +34 -0
  289. data/lib/unmagic/components/turbo_stream_actions.rb +16 -0
  290. data/lib/unmagic/components/version.rb +1 -1
  291. data/lib/unmagic/components.rb +30 -1
  292. metadata +308 -4
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Unmagic
6
+ module Components
7
+ module AIChat
8
+ # What went into a tool call or came back out of it, to be read exactly as it
9
+ # was written. See ActionViewHelpers#ai_chat_payload.
10
+ class Payload
11
+ def initialize(view, payload, label: nil, language: nil, duration: nil, copy: false, **options)
12
+ @view = view
13
+ @payload = payload
14
+ @label = label
15
+ @language = language
16
+ @duration = duration
17
+ @copy = copy
18
+ @options = options
19
+ end
20
+
21
+ # An absent payload renders nothing, not a dash: a response that hasn't
22
+ # arrived isn't an empty one.
23
+ def render
24
+ return "".html_safe if @payload.nil?
25
+
26
+ source, language = formatted
27
+
28
+ view.content_tag(:div, **@options, class: view.class_names("UnmagicAIChatPayload", @options[:class])) do
29
+ safe_join [ head(source), code(source, language) ].compact
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :view
36
+
37
+ delegate :tag, :safe_join, to: :view, private: true
38
+
39
+ # Structured data is laid out a key to a line; a string that turns out to be
40
+ # JSON is too. Anything else is shown as the text it is rather than being
41
+ # mis-tokenised.
42
+ def formatted
43
+ return [ @payload.to_s, @language ] if @language && !structured?(@payload)
44
+
45
+ value = @payload.is_a?(String) ? parse(@payload) : @payload
46
+ if structured?(value)
47
+ [ JSON.pretty_generate(value), @language || :json ]
48
+ else
49
+ [ @payload.to_s, @language || :plaintext ]
50
+ end
51
+ end
52
+
53
+ def structured?(value) = value.is_a?(Hash) || value.is_a?(Array)
54
+
55
+ def parse(string)
56
+ JSON.parse(string)
57
+ rescue JSON::ParserError
58
+ string
59
+ end
60
+
61
+ def head(source)
62
+ return if @label.blank? && @duration.nil? && !@copy
63
+
64
+ tag.div(class: "UnmagicAIChatPayload__head") do
65
+ safe_join [
66
+ tag.span(@label, class: "UnmagicAIChatPayload__label"),
67
+ (timing if @duration),
68
+ (view.copy_button(source, class: "UnmagicAIChatPayload__copy") if @copy)
69
+ ].compact
70
+ end
71
+ end
72
+
73
+ def timing
74
+ tag.span(class: "UnmagicAIChatPayload__timing") do
75
+ safe_join [
76
+ Icons.svg(view, :timer),
77
+ tag.span(AIChat.t("payload.took", default: "Took"), class: "UnmagicVisuallyHidden"),
78
+ Duration.format(@duration)
79
+ ]
80
+ end
81
+ end
82
+
83
+ # Scrollable, so reachable by keyboard and named for it: a region nobody can
84
+ # scroll without a mouse is a trap in reverse.
85
+ def code(source, language)
86
+ tag.div(Components.configuration.code_block.call(view, source, language),
87
+ class: "UnmagicAIChatPayload__code", tabindex: 0, role: "region",
88
+ "aria-label": @label.presence || AIChat.t("payload.label", default: "Payload"))
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unmagic
4
+ module Components
5
+ module AIChat
6
+ # An agent asking to be allowed something, with the answer under the
7
+ # reasoning rather than in a dialog that arrived without it. See
8
+ # ActionViewHelpers#ai_chat_permission.
9
+ class Permission
10
+ STATES = %i[waiting granted refused lapsed].freeze
11
+
12
+ OUTCOMES = { granted: "Allowed.", refused: "Refused.",
13
+ lapsed: "Answered in the chat — nothing was granted." }.freeze
14
+
15
+ def initialize(view, tool:, state:, url: nil, live: false, outcome: nil, **options)
16
+ AIChat.validate!("ai_chat_permission", :state, state, STATES)
17
+ raise ArgumentError, "a waiting ai_chat_permission needs a url: to answer to" if state == :waiting && url.blank?
18
+
19
+ @view = view
20
+ @tool = tool
21
+ @state = state
22
+ @url = url
23
+ @live = live
24
+ @outcome = outcome
25
+ @options = options
26
+ @arguments = []
27
+ @summary = nil
28
+ @reason = nil
29
+ @examples = []
30
+ @actions = []
31
+ end
32
+
33
+ # A name and value the grant is for, beside the tool in the ask line.
34
+ def argument(name, value)
35
+ @arguments << [ name, value ]
36
+ nil
37
+ end
38
+
39
+ def summary(content = nil, &block)
40
+ @summary = block ? view.capture(&block) : content
41
+ nil
42
+ end
43
+
44
+ def reason(content = nil, &block)
45
+ @reason = block ? view.capture(&block) : content
46
+ nil
47
+ end
48
+
49
+ def example(text)
50
+ @examples << text
51
+ nil
52
+ end
53
+
54
+ # Repeatable: the second is the wider grant, offered only when there is
55
+ # something left to widen. The host decides that; the gem can't know.
56
+ def allow(label = nil, params: {}, confirm: nil, method: :post)
57
+ @actions << [ :allow, label || AIChat.t("permission.allow", default: "Allow"), params, confirm, method ]
58
+ nil
59
+ end
60
+
61
+ def refuse(label = nil, params: {}, confirm: nil, method: :delete)
62
+ @actions << [ :refuse, label || AIChat.t("permission.refuse", default: "Refuse"), params, confirm, method ]
63
+ nil
64
+ end
65
+
66
+ def render
67
+ waiting = @state == :waiting
68
+
69
+ # Focus lands on the card, never on Allow, so a stray Enter grants nothing.
70
+ view.content_tag(:div, **@options,
71
+ role: ("alert" if @live && waiting),
72
+ tabindex: ("-1" if waiting),
73
+ class: view.class_names("UnmagicAIChatPermission", "UnmagicAIChatPermission--#{@state}", @options[:class])) do
74
+ safe_join [
75
+ head(waiting),
76
+ ask,
77
+ (tag.div(@summary, class: "UnmagicAIChatPermission__summary UnmagicProse") if @summary.present?),
78
+ (tag.div(@reason, class: "UnmagicAIChatPermission__reason UnmagicProse") if @reason.present?),
79
+ examples,
80
+ waiting ? actions : outcome
81
+ ].compact
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ attr_reader :view
88
+
89
+ delegate :tag, :safe_join, to: :view, private: true
90
+
91
+ def head(waiting)
92
+ tag.div(class: "UnmagicAIChatPermission__head") do
93
+ safe_join [
94
+ Icons.svg(view, :shield_alert),
95
+ waiting ? AIChat.t("permission.waiting", default: "Wants permission") : AIChat.t("permission.asked", default: "Wanted permission")
96
+ ]
97
+ end
98
+ end
99
+
100
+ # The tool's own name, untranslated: it is what will be called and what the
101
+ # permission is written down against.
102
+ def ask
103
+ tag.p(class: "UnmagicAIChatPermission__ask") do
104
+ safe_join [
105
+ tag.code(@tool, class: "UnmagicAIChatPermission__tool"),
106
+ *@arguments.map { |name, value| tag.span("#{name}: #{value}", class: "UnmagicAIChatPermission__argument") }
107
+ ], " "
108
+ end
109
+ end
110
+
111
+ def examples
112
+ return if @examples.empty?
113
+
114
+ tag.ul(safe_join(@examples.map { |text| tag.li(text) }), class: "UnmagicAIChatPermission__examples")
115
+ end
116
+
117
+ def actions
118
+ allows = 0
119
+
120
+ tag.div(class: "UnmagicAIChatPermission__actions") do
121
+ safe_join(@actions.map do |kind, label, params, confirm, method|
122
+ modifier =
123
+ if kind == :refuse then nil
124
+ elsif (allows += 1) == 1 then "UnmagicAIChatPermission__allow"
125
+ else "UnmagicAIChatPermission__widen"
126
+ end
127
+
128
+ # The block form, so it's a <button> whatever the host's Rails defaults.
129
+ view.button_to(@url, method: method, params: params,
130
+ form: { data: { turbo_confirm: confirm }.compact },
131
+ class: view.class_names(Button.classes, modifier)) { label }
132
+ end)
133
+ end
134
+ end
135
+
136
+ def outcome
137
+ text = @outcome || AIChat.t("permission.#{@state}", default: OUTCOMES.fetch(@state))
138
+ tag.p(text, class: "UnmagicAIChatPermission__outcome")
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unmagic
4
+ module Components
5
+ module AIChat
6
+ # The checklist an agent is working through. See
7
+ # ActionViewHelpers#ai_chat_plan.
8
+ class Plan
9
+ STATES = %i[pending in_progress waiting completed].freeze
10
+
11
+ # The glyph each state leads with. Waiting is the one in colour: the only
12
+ # state that won't move on its own.
13
+ GLYPHS = { pending: :circle_dashed, in_progress: :loader_circle, waiting: :circle_question_mark,
14
+ completed: :circle_check }.freeze
15
+
16
+ LABELS = { pending: "To do", in_progress: "In progress", waiting: "Waiting on you", completed: "Done" }.freeze
17
+
18
+ Step = Struct.new(:title, :state, :detail)
19
+
20
+ def initialize(view, title: nil, completed: nil, total: nil, open: true, empty: nil, collapsible: true,
21
+ title_tag: :h2, **options)
22
+ @view = view
23
+ @title = title || AIChat.t("plan.title", default: "Plan")
24
+ @completed = completed
25
+ @total = total
26
+ @open = open
27
+ @empty = empty
28
+ @collapsible = collapsible
29
+ @title_tag = title_tag
30
+ @options = options
31
+ @steps = []
32
+ end
33
+
34
+ def step(title, state: :pending, &block)
35
+ AIChat.validate!("ai_chat_plan step", :state, state, STATES)
36
+
37
+ @steps << Step.new(title, state, (view.capture(&block) if block))
38
+ nil
39
+ end
40
+
41
+ def render
42
+ Section.new(view, block: "UnmagicAIChatPlan", icon: :list_checks, title: @title, count: count,
43
+ open: @open, collapsible: @collapsible, title_tag: @title_tag, options: @options).render(body)
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :view
49
+
50
+ delegate :tag, :safe_join, to: :view, private: true
51
+
52
+ # Given rather than counted when a panel shows only some of the steps, so the
53
+ # reading isn't worked out from the visible few.
54
+ def count
55
+ total = @total || @steps.size
56
+ return if total.zero?
57
+
58
+ "#{@completed || @steps.count { |step| step.state == :completed }}/#{total}"
59
+ end
60
+
61
+ def body
62
+ if @steps.empty?
63
+ tag.p(@empty || AIChat.t("plan.empty", default: "Nothing planned yet."), class: "UnmagicAIChatPlan__empty")
64
+ else
65
+ tag.ol(safe_join(@steps.map { |step| item(step) }), class: "UnmagicAIChatPlan__steps")
66
+ end
67
+ end
68
+
69
+ def item(step)
70
+ tag.li(class: "UnmagicAIChatPlan__step", data: { state: step.state }) do
71
+ safe_join [
72
+ tag.span(class: "UnmagicAIChatPlan__glyph") do
73
+ safe_join [
74
+ Icons.svg(view, GLYPHS.fetch(step.state),
75
+ class: ("UnmagicAIChatSpinner" if step.state == :in_progress)),
76
+ tag.span(AIChat.t("plan.#{step.state}", default: LABELS.fetch(step.state)), class: "UnmagicVisuallyHidden")
77
+ ]
78
+ end,
79
+ tag.div(class: "UnmagicAIChatPlan__text") do
80
+ safe_join [
81
+ tag.span(step.title, class: "UnmagicAIChatPlan__label"),
82
+ (tag.div(step.detail, class: "UnmagicAIChatPlan__detail") if step.detail.present?)
83
+ ].compact
84
+ end
85
+ ]
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unmagic
4
+ module Components
5
+ module AIChat
6
+ # An offer the agent made in passing, with its actions while pending, settling
7
+ # into a quiet decided state. It doesn't stop the work. See
8
+ # ActionViewHelpers#ai_chat_proposal.
9
+ class Proposal
10
+ STATES = %i[pending accepted rejected].freeze
11
+
12
+ OUTCOMES = { accepted: "Saved", rejected: "Dismissed" }.freeze
13
+
14
+ def initialize(view, state: :pending, icon: :lightbulb, id: nil, **options)
15
+ AIChat.validate!("ai_chat_proposal", :state, state, STATES)
16
+
17
+ @view = view
18
+ @state = state
19
+ @icon = icon
20
+ @id = id
21
+ @options = options
22
+ @claim = nil
23
+ @meta = nil
24
+ @accept = nil
25
+ @reject = nil
26
+ @outcome = nil
27
+ end
28
+
29
+ def claim(content = nil, &block)
30
+ @claim = block ? view.capture(&block) : content
31
+ nil
32
+ end
33
+
34
+ def meta(text)
35
+ @meta = text
36
+ nil
37
+ end
38
+
39
+ def accept(content = nil, &block)
40
+ @accept = block ? view.capture(&block) : content
41
+ nil
42
+ end
43
+
44
+ def reject(content = nil, &block)
45
+ @reject = block ? view.capture(&block) : content
46
+ nil
47
+ end
48
+
49
+ def outcome(text)
50
+ @outcome = text
51
+ nil
52
+ end
53
+
54
+ # An <aside> named by its claim, so a reader tabbing through a reply hears
55
+ # what each card offers before reaching its buttons.
56
+ def render
57
+ claim_id = "#{@id || AIChat.random_id("proposal")}_claim"
58
+
59
+ view.content_tag(:aside, **@options,
60
+ id: @id,
61
+ "aria-labelledby": claim_id,
62
+ class: view.class_names("UnmagicAIChatProposal", "UnmagicAIChatProposal--#{@state}", "not-prose", @options[:class])) do
63
+ safe_join [
64
+ tag.div(class: "UnmagicAIChatProposal__body") do
65
+ safe_join [
66
+ Icons.svg(view, @icon, class: "UnmagicAIChatProposal__glyph"),
67
+ tag.div do
68
+ safe_join [
69
+ tag.p(@claim, id: claim_id, class: "UnmagicAIChatProposal__claim"),
70
+ (tag.p(@meta, class: "UnmagicAIChatProposal__meta") if @meta.present?)
71
+ ].compact
72
+ end
73
+ ]
74
+ end,
75
+ side
76
+ ].compact
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ attr_reader :view
83
+
84
+ delegate :tag, :safe_join, to: :view, private: true
85
+
86
+ def side
87
+ if @state == :pending
88
+ actions = [ @accept, @reject ].select(&:present?)
89
+ tag.div(safe_join(actions), class: "UnmagicAIChatProposal__actions") if actions.any?
90
+ else
91
+ tag.span(class: "UnmagicAIChatProposal__outcome") do
92
+ safe_join [
93
+ Icons.svg(view, @state == :accepted ? :check : :x),
94
+ @outcome || AIChat.t("proposal.#{@state}", default: OUTCOMES.fetch(@state))
95
+ ]
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unmagic
4
+ module Components
5
+ module AIChat
6
+ # The model's thinking, collapsed above the reply it led to. See
7
+ # ActionViewHelpers#ai_chat_reasoning.
8
+ class Reasoning
9
+ def initialize(view, title: nil, streaming: false, duration: nil, open: false, id: nil, **options)
10
+ @view = view
11
+ @title = title
12
+ @streaming = streaming
13
+ @duration = duration
14
+ @open = open
15
+ @id = id
16
+ @options = options
17
+ @blocks = []
18
+ end
19
+
20
+ def block(content = nil, &block)
21
+ @blocks << (block ? view.capture(&block) : content)
22
+ nil
23
+ end
24
+
25
+ # Nothing at all when there is no reasoning, unless it is still arriving: a
26
+ # turn that didn't think should leave no chrome behind.
27
+ def render(content = nil)
28
+ blocks = [ content, *@blocks ].select(&:present?)
29
+ return "".html_safe if blocks.empty? && !@streaming
30
+
31
+ tag.details(**@options, open: @open, class: view.class_names("UnmagicAIChatReasoning", @options[:class])) do
32
+ safe_join [ summary, *blocks.each_with_index.map { |markup, index| body(markup, index) } ]
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :view
39
+
40
+ delegate :tag, :safe_join, to: :view, private: true
41
+
42
+ def summary
43
+ tag.summary(class: "UnmagicAIChatReasoning__head", "aria-busy": ("true" if @streaming)) do
44
+ safe_join [
45
+ (Icons.svg(view, :loader_circle, class: "UnmagicAIChatSpinner") if @streaming),
46
+ tag.span(title, class: "UnmagicAIChatReasoning__title"),
47
+ Icons.svg(view, :chevron_right, class: "UnmagicAIChatChevron")
48
+ ].compact
49
+ end
50
+ end
51
+
52
+ def title
53
+ if @streaming
54
+ AIChat.t("reasoning.thinking", default: "Thinking…")
55
+ elsif @duration
56
+ AIChat.t("reasoning.duration", duration: Duration.format(@duration), default: "Thought for %{duration}")
57
+ else
58
+ @title || AIChat.t("reasoning.title", default: "Thought process")
59
+ end
60
+ end
61
+
62
+ # A block that is still arriving reveals at the reply's pace, so it needs
63
+ # an id to stream into; the first block takes the caller's.
64
+ def body(markup, index)
65
+ classes = "UnmagicAIChatReasoning__block UnmagicProse"
66
+ if @streaming && @id
67
+ StreamingMarkdown.new(view, id: index.zero? ? @id : "#{@id}_#{index}", streaming: true, class: classes).render(markup)
68
+ else
69
+ tag.div(markup, class: classes)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end