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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -1
- data/README.md +506 -1
- data/app/assets/icons/lucide/LICENSE +43 -0
- data/app/assets/icons/lucide/arrow-down.svg +1 -0
- data/app/assets/icons/lucide/arrow-left.svg +1 -0
- data/app/assets/icons/lucide/arrow-up.svg +1 -0
- data/app/assets/icons/lucide/ban.svg +1 -0
- data/app/assets/icons/lucide/brain.svg +1 -0
- data/app/assets/icons/lucide/check.svg +1 -0
- data/app/assets/icons/lucide/chevron-down.svg +1 -0
- data/app/assets/icons/lucide/chevron-left.svg +1 -0
- data/app/assets/icons/lucide/chevron-right.svg +1 -0
- data/app/assets/icons/lucide/circle-check.svg +1 -0
- data/app/assets/icons/lucide/circle-dashed.svg +1 -0
- data/app/assets/icons/lucide/circle-question-mark.svg +1 -0
- data/app/assets/icons/lucide/circle-stop.svg +1 -0
- data/app/assets/icons/lucide/circle-x.svg +1 -0
- data/app/assets/icons/lucide/clock.svg +1 -0
- data/app/assets/icons/lucide/copy.svg +1 -0
- data/app/assets/icons/lucide/ellipsis-vertical.svg +1 -0
- data/app/assets/icons/lucide/file.svg +1 -0
- data/app/assets/icons/lucide/folder.svg +1 -0
- data/app/assets/icons/lucide/grip-vertical.svg +1 -0
- data/app/assets/icons/lucide/info.svg +1 -0
- data/app/assets/icons/lucide/lightbulb.svg +1 -0
- data/app/assets/icons/lucide/list-checks.svg +1 -0
- data/app/assets/icons/lucide/loader-circle.svg +1 -0
- data/app/assets/icons/lucide/messages-square.svg +1 -0
- data/app/assets/icons/lucide/paperclip.svg +1 -0
- data/app/assets/icons/lucide/pencil.svg +1 -0
- data/app/assets/icons/lucide/plus.svg +1 -0
- data/app/assets/icons/lucide/rotate-cw.svg +1 -0
- data/app/assets/icons/lucide/shield-alert.svg +1 -0
- data/app/assets/icons/lucide/timer.svg +1 -0
- data/app/assets/icons/lucide/triangle-alert.svg +1 -0
- data/app/assets/icons/lucide/wrench.svg +1 -0
- data/app/assets/icons/lucide/x.svg +1 -0
- data/app/assets/javascripts/unmagic/components/ai_chat.js +142 -0
- data/app/assets/javascripts/unmagic/components/autoscroll.js +103 -0
- data/app/assets/javascripts/unmagic/components/board.js +65 -0
- data/app/assets/javascripts/unmagic/components/dropzone.js +206 -0
- data/app/assets/javascripts/unmagic/components/elapsed.js +64 -0
- data/app/assets/javascripts/unmagic/components/optimistic.js +71 -0
- data/app/assets/javascripts/unmagic/components/placement.js +30 -0
- data/app/assets/javascripts/unmagic/components/slash_menu.js +186 -0
- data/app/assets/javascripts/unmagic/components/sortable.js +6 -0
- data/app/assets/javascripts/unmagic/components/sortable_item.js +122 -0
- data/app/assets/javascripts/unmagic/components/sortable_list.js +135 -0
- data/app/assets/javascripts/unmagic/components/sortable_param.js +14 -0
- data/app/assets/javascripts/unmagic/components/sortable_session.js +502 -0
- data/app/assets/javascripts/unmagic/components/streaming_markdown.js +269 -0
- data/app/assets/javascripts/unmagic/components/toasts.js +14 -4
- data/app/assets/javascripts/unmagic/components/toolbar.js +76 -0
- data/app/assets/javascripts/unmagic/components/upsert.js +4 -20
- data/app/assets/javascripts/unmagic/components.js +10 -0
- data/app/assets/tailwind/unmagic_components/engine.css +1515 -0
- data/lib/unmagic/components/action_view_helpers.rb +558 -3
- data/lib/unmagic/components/ai_chat/action_bar.rb +70 -0
- data/lib/unmagic/components/ai_chat/attachments.rb +119 -0
- data/lib/unmagic/components/ai_chat/branch_picker.rb +65 -0
- data/lib/unmagic/components/ai_chat/citation.rb +89 -0
- data/lib/unmagic/components/ai_chat/composer.rb +160 -0
- data/lib/unmagic/components/ai_chat/failure.rb +80 -0
- data/lib/unmagic/components/ai_chat/message.rb +142 -0
- data/lib/unmagic/components/ai_chat/payload.rb +93 -0
- data/lib/unmagic/components/ai_chat/permission.rb +143 -0
- data/lib/unmagic/components/ai_chat/plan.rb +91 -0
- data/lib/unmagic/components/ai_chat/proposal.rb +102 -0
- data/lib/unmagic/components/ai_chat/reasoning.rb +75 -0
- data/lib/unmagic/components/ai_chat/request.rb +206 -0
- data/lib/unmagic/components/ai_chat/section.rb +53 -0
- data/lib/unmagic/components/ai_chat/slash_menu.rb +68 -0
- data/lib/unmagic/components/ai_chat/tool_call.rb +181 -0
- data/lib/unmagic/components/ai_chat/transcript.rb +68 -0
- data/lib/unmagic/components/ai_chat/welcome.rb +73 -0
- data/lib/unmagic/components/ai_chat/workspace.rb +74 -0
- data/lib/unmagic/components/ai_chat.rb +31 -0
- data/lib/unmagic/components/avatar.rb +134 -0
- data/lib/unmagic/components/board.rb +182 -0
- data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/application_controller.rb +80 -0
- data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/demos_controller.rb +137 -0
- data/lib/unmagic/components/browser/app/controllers/unmagic/components/browser/pages_controller.rb +40 -0
- data/lib/unmagic/components/browser/app/helpers/unmagic/components/browser/application_helper.rb +52 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/board_store.rb +88 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/catalog.rb +65 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/pager.rb +11 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/profile.rb +25 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/reply.rb +72 -0
- data/lib/unmagic/components/browser/app/models/unmagic/components/browser/thing.rb +31 -0
- data/lib/unmagic/components/browser/app/views/layouts/unmagic/components/browser/application.html.erb +65 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_code.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_crumb.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_example.html.erb +37 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/application/_nav.html.erb +17 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/demos/edit_profile.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_conversation.html.erb +101 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_live.html.erb +53 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat/_welcome.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_always.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_hover.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_action_bar/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_dropzone.html.erb +17 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_sent.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_attachments/_thumbnail.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_branch_picker/_positions.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_branch_picker/_thumbnail.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_compact.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_quote.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_citation/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_parts.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_states.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_composer/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_full.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_plain.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_failure/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_parts.html.erb +23 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_states.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_message/_turns.html.erb +16 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_json.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_text.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_payload/_thumbnail.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_answered.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_permission/_waiting.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_empty.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_states.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_plan/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_decided.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_inline.html.erb +18 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_proposal/_thumbnail.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_settled.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_thinking.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_reasoning/_thumbnail.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_answered.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_choices.html.erb +13 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_form.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_request/_thumbnail.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_slash_menu/_composer.html.erb +15 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_slash_menu/_thumbnail.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_made.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_run.html.erb +25 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_states.html.erb +22 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_tool_call/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_welcome/_suggestions.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_welcome/_thumbnail.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_workspace/_files.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/ai_chat_workspace/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_group.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_sizes.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/avatar/_tints.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_in_context.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/badge/_tones.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_handles.html.erb +13 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_live.html.erb +34 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/board/_thumbnail.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_sizes.html.erb +12 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/button/_variants.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_neutral.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/callout/_tones.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_actions_and_footer.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_flush_table.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_link.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/card/_thumbnail.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_danger.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_plain.html.erb +2 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/confirm/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_from_element.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/copy_button/_with_label.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_inline.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_stacked.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/detail_list/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_dialog_tag.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_modal.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/dialog/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_countdown.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_running.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/elapsed/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/empty_state/_default.html.erb +1 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/empty_state/_thumbnail.html.erb +3 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_autogrow.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_control_classes.html.erb +54 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_fields.html.erb +28 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_states.html.erb +11 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_thumbnail.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/forms/_uuid_field.html.erb +9 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_absolute.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_relative.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/local_time/_thumbnail.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_actions.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_labelled.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/menu/_thumbnail.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_default.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/page_header/_with_leading.html.erb +14 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_blocking_out.html.erb +25 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_card.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_detail_list.html.erb +15 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_page_header.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_text_and_button.html.erb +17 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/skeleton/_thumbnail.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_connected.html.erb +17 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_grid.html.erb +20 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_handles.html.erb +22 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/sortable_list/_thumbnail.html.erb +7 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_reveal.html.erb +19 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_stopped.html.erb +31 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/streaming_markdown/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_deferred.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_empty.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_sortable.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_table_tag.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/table/_thumbnail.html.erb +4 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_links.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_panels.html.erb +18 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tabs/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_above_dialog.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_flash.html.erb +13 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_stream.html.erb +6 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/toast/_thumbnail.html.erb +10 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_placement.html.erb +8 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_term.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/examples/tooltip/_thumbnail.html.erb +5 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/component.html.erb +58 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/installation.html.erb +47 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/overview.html.erb +49 -0
- data/lib/unmagic/components/browser/app/views/unmagic/components/browser/pages/theming.html.erb +52 -0
- data/lib/unmagic/components/browser/assets/browser.css +2 -0
- data/lib/unmagic/components/browser/catalog/ai_chat.rb +21 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_action_bar.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_attachments.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_branch_picker.rb +12 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_citation.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_composer.rb +16 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_failure.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_message.rb +17 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_payload.rb +16 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_permission.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_plan.rb +14 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_proposal.rb +15 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_reasoning.rb +14 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_request.rb +17 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_slash_menu.rb +14 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_tool_call.rb +17 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_welcome.rb +13 -0
- data/lib/unmagic/components/browser/catalog/ai_chat_workspace.rb +12 -0
- data/lib/unmagic/components/browser/catalog/avatar.rb +15 -0
- data/lib/unmagic/components/browser/catalog/badge.rb +11 -0
- data/lib/unmagic/components/browser/catalog/board.rb +18 -0
- data/lib/unmagic/components/browser/catalog/button.rb +14 -0
- data/lib/unmagic/components/browser/catalog/callout.rb +13 -0
- data/lib/unmagic/components/browser/catalog/card.rb +16 -0
- data/lib/unmagic/components/browser/catalog/confirm.rb +14 -0
- data/lib/unmagic/components/browser/catalog/copy_button.rb +15 -0
- data/lib/unmagic/components/browser/catalog/detail_list.rb +14 -0
- data/lib/unmagic/components/browser/catalog/dialog.rb +15 -0
- data/lib/unmagic/components/browser/catalog/elapsed.rb +14 -0
- data/lib/unmagic/components/browser/catalog/empty_state.rb +11 -0
- data/lib/unmagic/components/browser/catalog/forms.rb +21 -0
- data/lib/unmagic/components/browser/catalog/local_time.rb +13 -0
- data/lib/unmagic/components/browser/catalog/menu.rb +15 -0
- data/lib/unmagic/components/browser/catalog/page_header.rb +13 -0
- data/lib/unmagic/components/browser/catalog/skeleton.rb +18 -0
- data/lib/unmagic/components/browser/catalog/sortable_list.rb +17 -0
- data/lib/unmagic/components/browser/catalog/streaming_markdown.rb +17 -0
- data/lib/unmagic/components/browser/catalog/table.rb +17 -0
- data/lib/unmagic/components/browser/catalog/tabs.rb +15 -0
- data/lib/unmagic/components/browser/catalog/toast.rb +17 -0
- data/lib/unmagic/components/browser/catalog/tooltip.rb +14 -0
- data/lib/unmagic/components/browser/config/routes.rb +41 -0
- data/lib/unmagic/components/browser/tailwind/browser.css +33 -0
- data/lib/unmagic/components/browser.rb +39 -0
- data/lib/unmagic/components/configuration.rb +31 -1
- data/lib/unmagic/components/duration.rb +35 -0
- data/lib/unmagic/components/elapsed.rb +46 -0
- data/lib/unmagic/components/engine.rb +9 -0
- data/lib/unmagic/components/icons.rb +34 -26
- data/lib/unmagic/components/modal.rb +1 -1
- data/lib/unmagic/components/page_header.rb +8 -0
- data/lib/unmagic/components/sortable.rb +104 -0
- data/lib/unmagic/components/streaming_markdown.rb +34 -0
- data/lib/unmagic/components/turbo_stream_actions.rb +16 -0
- data/lib/unmagic/components/version.rb +1 -1
- data/lib/unmagic/components.rb +30 -1
- metadata +308 -4
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unmagic
|
|
4
|
+
module Components
|
|
5
|
+
module AIChat
|
|
6
|
+
# The agent asking something it can't work out on its own, with the answers
|
|
7
|
+
# right there. One component, two faces: choices, or a form the caller
|
|
8
|
+
# builds. See ActionViewHelpers#ai_chat_request.
|
|
9
|
+
class Request
|
|
10
|
+
STATES = %i[waiting accepted declined cancelled timed_out].freeze
|
|
11
|
+
|
|
12
|
+
OUTCOMES = { accepted: "Submitted", declined: "Declined", cancelled: "Cancelled",
|
|
13
|
+
timed_out: "Timed out" }.freeze
|
|
14
|
+
|
|
15
|
+
OUTCOME_ICONS = { accepted: :circle_check, declined: :ban, cancelled: :circle_x, timed_out: :clock }.freeze
|
|
16
|
+
|
|
17
|
+
Question = Struct.new(:text, :header, :multiple, :options, :picked)
|
|
18
|
+
Option = Struct.new(:label, :description)
|
|
19
|
+
|
|
20
|
+
# Collects a question's options.
|
|
21
|
+
class QuestionBuilder
|
|
22
|
+
attr_reader :options
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@options = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def option(label, description: nil)
|
|
29
|
+
@options << Option.new(label, description)
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(view, state:, url: nil, method: :patch, prompt: nil, label: nil, scope: :response,
|
|
35
|
+
live: false, **options)
|
|
36
|
+
AIChat.validate!("ai_chat_request", :state, state, STATES)
|
|
37
|
+
raise ArgumentError, "a waiting ai_chat_request needs a url: to answer to" if state == :waiting && url.blank?
|
|
38
|
+
|
|
39
|
+
@view = view
|
|
40
|
+
@state = state
|
|
41
|
+
@url = url
|
|
42
|
+
@method = method
|
|
43
|
+
@prompt = prompt
|
|
44
|
+
@label = label
|
|
45
|
+
@scope = scope
|
|
46
|
+
@live = live
|
|
47
|
+
@options = options
|
|
48
|
+
@questions = []
|
|
49
|
+
@form = nil
|
|
50
|
+
@answers = []
|
|
51
|
+
@decline = nil
|
|
52
|
+
@aside = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# picked: is what was chosen, for an answered request; the options go, and
|
|
56
|
+
# the question stays, because it is most of what the card is worth later.
|
|
57
|
+
def question(text, header: nil, multiple: false, picked: nil, &block)
|
|
58
|
+
builder = QuestionBuilder.new
|
|
59
|
+
view.capture(builder, &block) if block # capture, so the block's own whitespace stays out of the page
|
|
60
|
+
@questions << Question.new(text, header, multiple, builder.options, Array(picked))
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The caller's own fields, given the form builder while waiting.
|
|
65
|
+
def form(&block)
|
|
66
|
+
@form = block
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# What was sent back, for an answered form.
|
|
71
|
+
def answer(label, value)
|
|
72
|
+
@answers << [ label, value ]
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def decline(label = nil)
|
|
77
|
+
@decline = label || AIChat.t("request.decline", default: "Decline")
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def aside(text)
|
|
82
|
+
@aside = text
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def render
|
|
87
|
+
raise ArgumentError, "an ai_chat_request asks with questions or a form, not both" if @questions.any? && @form
|
|
88
|
+
|
|
89
|
+
view.content_tag(:div, **@options,
|
|
90
|
+
role: ("alert" if @live && waiting?),
|
|
91
|
+
class: view.class_names("UnmagicAIChatRequest", "UnmagicAIChatRequest--#{@state}", @options[:class])) do
|
|
92
|
+
safe_join [ head, (tag.p(@prompt, class: "UnmagicAIChatRequest__prompt") if @prompt.present?), body ].compact
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
attr_reader :view
|
|
99
|
+
|
|
100
|
+
delegate :tag, :safe_join, to: :view, private: true
|
|
101
|
+
|
|
102
|
+
def waiting? = @state == :waiting
|
|
103
|
+
|
|
104
|
+
# Past tense once answered: a heading still saying somebody wants something
|
|
105
|
+
# would be the one line in the card that's no longer true.
|
|
106
|
+
def head
|
|
107
|
+
tag.div(class: "UnmagicAIChatRequest__head") do
|
|
108
|
+
safe_join [
|
|
109
|
+
Icons.svg(view, :circle_question_mark),
|
|
110
|
+
tag.span(waiting? ? AIChat.t("request.waiting", default: "Wants to know") : AIChat.t("request.asked", default: "Wanted to know")),
|
|
111
|
+
(outcome unless waiting?)
|
|
112
|
+
].compact
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def outcome
|
|
117
|
+
tag.span(class: "UnmagicAIChatRequest__outcome") do
|
|
118
|
+
safe_join [ Icons.svg(view, OUTCOME_ICONS.fetch(@state)), AIChat.t("request.#{@state}", default: OUTCOMES.fetch(@state)) ]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def body
|
|
123
|
+
if waiting?
|
|
124
|
+
view.form_with(url: @url, method: @method, scope: @scope, builder: Components::FormBuilder,
|
|
125
|
+
class: "UnmagicAIChatRequest__form") do |form|
|
|
126
|
+
safe_join [ questions, (view.capture(form, &@form) if @form), actions ].compact
|
|
127
|
+
end
|
|
128
|
+
else
|
|
129
|
+
safe_join [ questions, answers ].compact
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def questions
|
|
134
|
+
return if @questions.empty?
|
|
135
|
+
|
|
136
|
+
safe_join(@questions.each_with_index.map { |question, index| fieldset(question, index) })
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def fieldset(question, index)
|
|
140
|
+
tag.fieldset(class: "UnmagicAIChatRequest__question") do
|
|
141
|
+
safe_join [
|
|
142
|
+
tag.legend(class: "UnmagicAIChatRequest__legend") do
|
|
143
|
+
safe_join [
|
|
144
|
+
(tag.span(question.header, class: "UnmagicAIChatRequest__header") if question.header.present?),
|
|
145
|
+
tag.span(question.text, class: "UnmagicAIChatRequest__label")
|
|
146
|
+
].compact
|
|
147
|
+
end,
|
|
148
|
+
waiting? ? choices(question, index) : picked(question)
|
|
149
|
+
]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# A single choice is required, so a question skipped by accident isn't one
|
|
154
|
+
# the agent has to ask again.
|
|
155
|
+
def choices(question, index)
|
|
156
|
+
kind = question.multiple ? :check : :radio
|
|
157
|
+
|
|
158
|
+
tag.div(class: "UnmagicAIChatRequest__options") do
|
|
159
|
+
safe_join(question.options.map do |option|
|
|
160
|
+
tag.label(class: "UnmagicAIChatRequest__option") do
|
|
161
|
+
safe_join [
|
|
162
|
+
tag.input(type: question.multiple ? "checkbox" : "radio", name: "answers[#{index}][]",
|
|
163
|
+
value: option.label, required: !question.multiple, class: Control.classes(view, kind)),
|
|
164
|
+
tag.span(class: "UnmagicAIChatRequest__choice") do
|
|
165
|
+
safe_join [
|
|
166
|
+
tag.span(option.label, class: "UnmagicAIChatRequest__choiceLabel"),
|
|
167
|
+
(tag.span(option.description, class: "UnmagicAIChatRequest__choiceDescription") if option.description.present?)
|
|
168
|
+
].compact
|
|
169
|
+
end
|
|
170
|
+
]
|
|
171
|
+
end
|
|
172
|
+
end)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def picked(question)
|
|
177
|
+
text = question.picked.presence&.to_sentence || AIChat.t("request.unanswered", default: "No answer.")
|
|
178
|
+
tag.p(text, class: view.class_names("UnmagicAIChatRequest__picked",
|
|
179
|
+
"UnmagicAIChatRequest__picked--none" => question.picked.empty?))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def answers
|
|
183
|
+
return if @answers.empty?
|
|
184
|
+
|
|
185
|
+
tag.dl(class: "UnmagicAIChatRequest__answers") do
|
|
186
|
+
safe_join(@answers.map do |label, value|
|
|
187
|
+
tag.div(safe_join([ tag.dt(label), tag.dd(value) ]), class: "UnmagicAIChatRequest__answer")
|
|
188
|
+
end)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Decline posts the same form with a flag and skips validation, so it is one
|
|
193
|
+
# form with no nested button_to and needs nothing filled in.
|
|
194
|
+
def actions
|
|
195
|
+
tag.div(class: "UnmagicAIChatRequest__actions") do
|
|
196
|
+
safe_join [
|
|
197
|
+
tag.button(@label || AIChat.t("request.answer", default: "Answer"), type: "submit", class: Button.classes(:primary)),
|
|
198
|
+
(tag.button(@decline, type: "submit", name: "decline", value: "1", formnovalidate: true, class: Button.classes) if @decline),
|
|
199
|
+
(tag.p(@aside, class: "UnmagicAIChatRequest__aside") if @aside.present?)
|
|
200
|
+
].compact
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unmagic
|
|
4
|
+
module Components
|
|
5
|
+
module AIChat
|
|
6
|
+
# The collapsible panel section a plan and a workspace share: a glyph, a
|
|
7
|
+
# title, a count and a chevron over a body. Always rendered, empty or not,
|
|
8
|
+
# because both are broadcast targets.
|
|
9
|
+
class Section
|
|
10
|
+
def initialize(view, block:, icon:, title:, count:, open:, collapsible:, title_tag:, options:)
|
|
11
|
+
@view = view
|
|
12
|
+
@block = block
|
|
13
|
+
@icon = icon
|
|
14
|
+
@title = title
|
|
15
|
+
@count = count
|
|
16
|
+
@open = open
|
|
17
|
+
@collapsible = collapsible
|
|
18
|
+
@title_tag = title_tag
|
|
19
|
+
@options = options
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render(body)
|
|
23
|
+
classes = view.class_names(@block, { "#{@block}--static" => !@collapsible }, @options[:class])
|
|
24
|
+
|
|
25
|
+
if @collapsible
|
|
26
|
+
tag.details(**@options, open: @open, class: classes) do
|
|
27
|
+
safe_join [ tag.summary(head(chevron: true), class: "#{@block}__head"), body ]
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
tag.section(**@options, class: classes) do
|
|
31
|
+
safe_join [ tag.div(head(chevron: false), class: "#{@block}__head"), body ]
|
|
32
|
+
end
|
|
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 head(chevron:)
|
|
43
|
+
safe_join [
|
|
44
|
+
Icons.svg(view, @icon, class: "#{@block}__icon"),
|
|
45
|
+
view.content_tag(@title_tag, @title, class: "#{@block}__title"),
|
|
46
|
+
(Icons.svg(view, :chevron_right, class: "UnmagicAIChatChevron") if chevron),
|
|
47
|
+
(tag.span(@count, class: "#{@block}__count") if @count.present?)
|
|
48
|
+
].compact
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unmagic
|
|
4
|
+
module Components
|
|
5
|
+
module AIChat
|
|
6
|
+
# Commands offered on a slash, rendered with every item and filtered in the
|
|
7
|
+
# browser, so what can be offered is only ever what exists. See
|
|
8
|
+
# ActionViewHelpers#ai_chat_slash_menu.
|
|
9
|
+
class SlashMenu
|
|
10
|
+
Item = Struct.new(:name, :description, :arguments)
|
|
11
|
+
|
|
12
|
+
def initialize(view, for: nil, id: nil, trigger: "/", above: false, insert: nil, **options)
|
|
13
|
+
raise ArgumentError, "ai_chat_slash_menu trigger must be one character" unless trigger.to_s.length == 1
|
|
14
|
+
|
|
15
|
+
@view = view
|
|
16
|
+
@for = binding.local_variable_get(:for)
|
|
17
|
+
@id = id || AIChat.random_id("slash_menu")
|
|
18
|
+
@trigger = trigger
|
|
19
|
+
@above = above
|
|
20
|
+
@insert = insert
|
|
21
|
+
@options = options
|
|
22
|
+
@items = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def item(name, description: nil, arguments: [])
|
|
26
|
+
@items << Item.new(name.to_s, description, Array(arguments))
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# An agent with no commands has no menu, not an empty one.
|
|
31
|
+
def render
|
|
32
|
+
return "".html_safe if @items.empty?
|
|
33
|
+
|
|
34
|
+
label = AIChat.t("slash_menu.label", default: "Commands")
|
|
35
|
+
|
|
36
|
+
view.content_tag("unmagic-slash-menu", **@options,
|
|
37
|
+
for: @for, trigger: @trigger, insert: @insert, hidden: true,
|
|
38
|
+
class: view.class_names("UnmagicAIChatSlashMenu", { "UnmagicAIChatSlashMenu--above" => @above }, @options[:class])) do
|
|
39
|
+
tag.ul(id: "#{@id}_listbox", role: "listbox", "aria-label": label, class: "UnmagicAIChatSlashMenu__list") do
|
|
40
|
+
safe_join(@items.each_with_index.map { |item, index| option(item, index) })
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
attr_reader :view
|
|
48
|
+
|
|
49
|
+
delegate :tag, :safe_join, to: :view, private: true
|
|
50
|
+
|
|
51
|
+
def option(item, index)
|
|
52
|
+
tag.li(id: "#{@id}_#{index}", role: "option", "aria-selected": index.zero?.to_s, data: { name: item.name },
|
|
53
|
+
class: "UnmagicAIChatSlashMenu__item") do
|
|
54
|
+
safe_join [
|
|
55
|
+
tag.span(class: "UnmagicAIChatSlashMenu__name") do
|
|
56
|
+
safe_join [
|
|
57
|
+
"#{@trigger}#{item.name}",
|
|
58
|
+
*item.arguments.map { |argument| tag.span("[#{argument}]", class: "UnmagicAIChatSlashMenu__argument") }
|
|
59
|
+
], " "
|
|
60
|
+
end,
|
|
61
|
+
(tag.span(item.description, class: "UnmagicAIChatSlashMenu__description") if item.description.present?)
|
|
62
|
+
].compact
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unmagic
|
|
4
|
+
module Components
|
|
5
|
+
module AIChat
|
|
6
|
+
# One reach for a tool, from the ask through to the answer, as a row in a
|
|
7
|
+
# timeline. See ActionViewHelpers#ai_chat_tool_call.
|
|
8
|
+
class ToolCall
|
|
9
|
+
STATES = %i[queued running waiting done failed].freeze
|
|
10
|
+
|
|
11
|
+
GLYPHS = { queued: :circle_dashed, running: :loader_circle, waiting: :circle_question_mark,
|
|
12
|
+
done: :circle_check, failed: :circle_x }.freeze
|
|
13
|
+
|
|
14
|
+
LABELS = { queued: "Queued", running: "Running", waiting: "Waiting on you", done: "Done",
|
|
15
|
+
failed: "Failed" }.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(view, name:, state:, id: nil, icon: nil, open: false, timeline: true, **options)
|
|
18
|
+
AIChat.validate!("ai_chat_tool_call", :state, state, STATES)
|
|
19
|
+
|
|
20
|
+
@view = view
|
|
21
|
+
@name = name
|
|
22
|
+
@state = state
|
|
23
|
+
@id = id
|
|
24
|
+
@icon = icon
|
|
25
|
+
@open = open
|
|
26
|
+
@timeline = timeline
|
|
27
|
+
@options = options
|
|
28
|
+
@summary = nil
|
|
29
|
+
@timing = nil
|
|
30
|
+
@failures = nil
|
|
31
|
+
@progress = nil
|
|
32
|
+
@payloads = []
|
|
33
|
+
@made = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# What the call was actually about, beside the name: three identical labels
|
|
37
|
+
# say nothing, "search_messages · car seat" does.
|
|
38
|
+
def summary(text)
|
|
39
|
+
@summary = text
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A live clock while running, what it took once done, nothing if neither is
|
|
44
|
+
# known — a zero would be a reading, and there isn't one.
|
|
45
|
+
def timing(started_at: nil, duration: nil)
|
|
46
|
+
@timing = { started_at: started_at, duration: duration }
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def asked(payload, **options)
|
|
51
|
+
@payloads << Payload.new(view, payload, label: AIChat.t("tool_call.asked", default: "Asked"), **options)
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def answered(payload, **options)
|
|
56
|
+
@payloads << Payload.new(view, payload, label: AIChat.t("tool_call.answered", default: "Answered"), **options)
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# How much of a batch didn't work, for a call that otherwise did. Nothing for
|
|
61
|
+
# zero, and nothing for a call that failed outright, which already says so.
|
|
62
|
+
def failures(count)
|
|
63
|
+
@failures = count
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# What the tool is saying while it works. Rendered with its own id so the host
|
|
68
|
+
# can replace just this line as reports come in.
|
|
69
|
+
def progress(text)
|
|
70
|
+
@progress = text
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# What the call produced. It stays outside the fold: it is the reason the
|
|
75
|
+
# call happened, and hiding it behind a chevron is the wrong way round.
|
|
76
|
+
def made(content = nil, &block)
|
|
77
|
+
@made = block ? view.capture(&block) : content
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render
|
|
82
|
+
payloads = @payloads.map(&:render).select(&:present?)
|
|
83
|
+
|
|
84
|
+
view.content_tag(:div, **@options,
|
|
85
|
+
id: @id,
|
|
86
|
+
"aria-busy": ("true" if @state == :running),
|
|
87
|
+
data: { ai_chat_timeline: ("row" if @timeline) }.merge(@options[:data] || {}),
|
|
88
|
+
class: view.class_names("UnmagicAIChatToolCall", "UnmagicAIChatToolCall--#{@state}", @options[:class])) do
|
|
89
|
+
safe_join [
|
|
90
|
+
(tag.span(class: "UnmagicAIChatToolCall__join", "aria-hidden": "true") if @timeline),
|
|
91
|
+
payloads.any? ? disclosure(payloads) : tag.div(row(chevron: false), class: "UnmagicAIChatToolCall__row"),
|
|
92
|
+
(tag.div(safe_join([ rail, @made ]), class: "UnmagicAIChatToolCall__made") if @made.present?)
|
|
93
|
+
].compact
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
attr_reader :view
|
|
100
|
+
|
|
101
|
+
delegate :tag, :safe_join, to: :view, private: true
|
|
102
|
+
|
|
103
|
+
# <details> so the disclosure needs no script and no state to restore after
|
|
104
|
+
# a broadcast replaces the row, which happens on every change of state.
|
|
105
|
+
def disclosure(payloads)
|
|
106
|
+
tag.details(open: @open, class: "UnmagicAIChatToolCall__disclosure") do
|
|
107
|
+
safe_join [
|
|
108
|
+
tag.summary(row(chevron: true), class: "UnmagicAIChatToolCall__row"),
|
|
109
|
+
tag.div(safe_join([ rail, *payloads ]), class: "UnmagicAIChatToolCall__body")
|
|
110
|
+
]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def row(chevron:)
|
|
115
|
+
safe_join [
|
|
116
|
+
glyph,
|
|
117
|
+
tag.code(@name, class: "UnmagicAIChatToolCall__name"),
|
|
118
|
+
(tag.span(@summary, class: "UnmagicAIChatToolCall__summary") if @summary.present?),
|
|
119
|
+
(Icons.svg(view, :chevron_right, class: "UnmagicAIChatChevron") if chevron),
|
|
120
|
+
readings,
|
|
121
|
+
(progress_line if @state == :running && @progress.present?)
|
|
122
|
+
].compact
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# A call that simply worked is the common case, so its glyph says what it was
|
|
126
|
+
# about (icon:) rather than repeating a tick down the whole timeline.
|
|
127
|
+
def glyph
|
|
128
|
+
name = @state == :done && @icon ? @icon : GLYPHS.fetch(@state)
|
|
129
|
+
|
|
130
|
+
tag.span(class: "UnmagicAIChatToolCall__glyph") do
|
|
131
|
+
safe_join [
|
|
132
|
+
Icons.svg(view, name, class: ("UnmagicAIChatSpinner" if @state == :running)),
|
|
133
|
+
tag.span(AIChat.t("tool_call.#{@state}", default: LABELS.fetch(@state)), class: "UnmagicVisuallyHidden")
|
|
134
|
+
]
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def readings
|
|
139
|
+
items = [ failures_reading, timing_reading ].compact
|
|
140
|
+
tag.span(safe_join(items), class: "UnmagicAIChatToolCall__readings") if items.any?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def failures_reading
|
|
144
|
+
return if @failures.to_i.zero? || @state == :failed
|
|
145
|
+
|
|
146
|
+
reading(:triangle_alert, AIChat.t("tool_call.partly_failed", default: "Partly failed"),
|
|
147
|
+
AIChat.t("tool_call.failures", count: @failures, default: "%{count} failed"), modifier: "warn")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def timing_reading
|
|
151
|
+
return unless @timing
|
|
152
|
+
|
|
153
|
+
if @state == :running && @timing[:started_at]
|
|
154
|
+
reading(:timer, AIChat.t("tool_call.running_for", default: "Running for"),
|
|
155
|
+
Elapsed.new(view, @timing[:started_at], direction: :up).render)
|
|
156
|
+
elsif @timing[:duration]
|
|
157
|
+
reading(:timer, AIChat.t("tool_call.took", default: "Took"), Duration.format(@timing[:duration]))
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Each reading leads with a glyph, so a run of small grey numbers can be told
|
|
162
|
+
# apart before the digits are read.
|
|
163
|
+
def reading(icon, label, value, modifier: nil)
|
|
164
|
+
tag.span(class: view.class_names("UnmagicAIChatToolCall__reading",
|
|
165
|
+
"UnmagicAIChatToolCall__reading--#{modifier}" => modifier)) do
|
|
166
|
+
safe_join [ Icons.svg(view, icon), tag.span(label, class: "UnmagicVisuallyHidden"), value ]
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def progress_line
|
|
171
|
+
tag.span(@progress, id: ("#{@id}_progress" if @id), class: "UnmagicAIChatToolCall__progress")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The timeline carried down the side of whatever hangs under a row.
|
|
175
|
+
def rail
|
|
176
|
+
tag.span(class: "UnmagicAIChatToolCall__rail", "aria-hidden": "true")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unmagic
|
|
4
|
+
module Components
|
|
5
|
+
module AIChat
|
|
6
|
+
# The scrolling region a conversation's turns are rendered into, and the
|
|
7
|
+
# spacing between them. It renders no entries of its own: the host keeps a
|
|
8
|
+
# partial per entry type. See ActionViewHelpers#ai_chat.
|
|
9
|
+
class Transcript
|
|
10
|
+
def initialize(view, id:, scroller: nil, follow: true, scroll_to_latest: true, **options)
|
|
11
|
+
raise ArgumentError, "ai_chat needs an id: for entries to be broadcast into" if id.blank?
|
|
12
|
+
|
|
13
|
+
@view = view
|
|
14
|
+
@id = id
|
|
15
|
+
@scroller = scroller
|
|
16
|
+
@follow = follow
|
|
17
|
+
@scroll_to_latest = scroll_to_latest
|
|
18
|
+
@options = options
|
|
19
|
+
@welcome = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Shown while the conversation is empty. It renders inside the log, so the
|
|
23
|
+
# first entry streamed in hides it (see the CSS) without another request.
|
|
24
|
+
def welcome(content = nil, &block)
|
|
25
|
+
@welcome = block ? view.capture(&block) : content
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Always renders the log, empty or not: a broadcast can only land in an
|
|
30
|
+
# element that is already on the page.
|
|
31
|
+
def render(entries)
|
|
32
|
+
log = view.content_tag(:div, **@options,
|
|
33
|
+
id: @id,
|
|
34
|
+
role: "log",
|
|
35
|
+
"aria-live": "polite",
|
|
36
|
+
"aria-relevant": "additions",
|
|
37
|
+
"aria-label": AIChat.t("transcript.label", default: "Conversation"),
|
|
38
|
+
class: view.class_names("UnmagicAIChat", @options[:class])) do
|
|
39
|
+
safe_join [ (tag.div(@welcome, class: "UnmagicAIChat__welcome") if @welcome && entries.blank?), entries ].compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return log unless @follow
|
|
43
|
+
|
|
44
|
+
view.content_tag("unmagic-autoscroll", scroller: @scroller, class: "UnmagicAutoscroll") do
|
|
45
|
+
safe_join [ log, (latest if @scroll_to_latest) ].compact
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
attr_reader :view
|
|
52
|
+
|
|
53
|
+
delegate :tag, :safe_join, to: :view, private: true
|
|
54
|
+
|
|
55
|
+
# A real button in the tab order, shown by <unmagic-autoscroll> only while
|
|
56
|
+
# the reader has scrolled away from the bottom.
|
|
57
|
+
def latest
|
|
58
|
+
label = AIChat.t("transcript.latest", default: "Jump to latest")
|
|
59
|
+
|
|
60
|
+
tag.div(class: "UnmagicAIChat__latest") do
|
|
61
|
+
tag.button(Icons.svg(view, :arrow_down), type: "button", hidden: true, "data-autoscroll-latest": "",
|
|
62
|
+
"aria-label": label, title: label, class: "UnmagicAIChat__latestButton")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unmagic
|
|
4
|
+
module Components
|
|
5
|
+
module AIChat
|
|
6
|
+
# What an empty conversation says, and the suggestions that start one. See
|
|
7
|
+
# ActionViewHelpers#ai_chat_welcome.
|
|
8
|
+
class Welcome
|
|
9
|
+
Suggestion = Struct.new(:label, :icon, :fill, :value)
|
|
10
|
+
|
|
11
|
+
def initialize(view, heading:, heading_tag: :h2, composer: "composer", field: nil, **options)
|
|
12
|
+
@view = view
|
|
13
|
+
@heading = heading
|
|
14
|
+
@heading_tag = heading_tag
|
|
15
|
+
@composer = composer
|
|
16
|
+
@field = field
|
|
17
|
+
@options = options
|
|
18
|
+
@body = nil
|
|
19
|
+
@suggestions = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def body(text)
|
|
23
|
+
@body = text
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# fill: true puts the text in the box without sending it, for a prompt that
|
|
28
|
+
# can't be sent as it stands.
|
|
29
|
+
def suggestion(label, icon: nil, fill: false, value: nil)
|
|
30
|
+
@suggestions << Suggestion.new(label, icon, fill, value)
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render
|
|
35
|
+
view.content_tag(:div, **@options, class: view.class_names("UnmagicAIChatWelcome", @options[:class])) do
|
|
36
|
+
safe_join [
|
|
37
|
+
view.content_tag(@heading_tag, @heading, class: "UnmagicAIChatWelcome__heading"),
|
|
38
|
+
(tag.p(@body, class: "UnmagicAIChatWelcome__body") if @body.present?),
|
|
39
|
+
suggestions
|
|
40
|
+
].compact
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
attr_reader :view
|
|
47
|
+
|
|
48
|
+
delegate :tag, :safe_join, to: :view, private: true
|
|
49
|
+
|
|
50
|
+
# Buttons, not links: a suggestion puts text in a box on this page.
|
|
51
|
+
def suggestions
|
|
52
|
+
return if @suggestions.empty?
|
|
53
|
+
|
|
54
|
+
tag.ul(class: "UnmagicAIChatWelcome__suggestions") do
|
|
55
|
+
safe_join(@suggestions.map do |suggestion|
|
|
56
|
+
tag.li do
|
|
57
|
+
tag.button(type: "button", class: "UnmagicAIChatWelcome__suggestion",
|
|
58
|
+
data: {
|
|
59
|
+
ai_chat_suggestion: suggestion.value || suggestion.label,
|
|
60
|
+
ai_chat_suggestion_form: @composer,
|
|
61
|
+
ai_chat_suggestion_field: @field,
|
|
62
|
+
ai_chat_suggestion_fill: ("" if suggestion.fill)
|
|
63
|
+
}.compact) do
|
|
64
|
+
safe_join [ (Icons.svg(view, suggestion.icon) if suggestion.icon), suggestion.label ].compact
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|