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,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
|