/ Custom Assistant .container-fluid data-controller="question-assistant2" .row .col-md-12.expanded = render_breadcrumbs([ \ { name: 'Home', path: projects_path }, \ { name: 'Custom Assistants', path: assistants_path }, \ { name: @assistant.name } \ ]) #questionAssistant2.position-relative.border.rounded.shadow-sm.bg-white.d-flex.flex-row.overflow-hidden style="height: calc(85svh);" / Left Column (Acts as the dedicated track and the push/pull sidebar) .bg-light.border-end.d-flex.flex-column.h-100 style="flex-shrink: 0;" .p-2.d-flex.align-items-center.justify-content-start style="z-index: 5;" span data-bs-toggle="tooltip" data-bs-trigger="hover" data-bs-placement="right" title="Toggle Chats" button.btn.btn-sm.bg-white.border.shadow-sm.text-dark.d-flex.align-items-center.justify-content-center data-bs-toggle="collapse" data-bs-target="#chatHistorySidebar" aria-expanded="true" aria-controls="chatHistorySidebar" style="border-radius: 8px; width: 36px; height: 36px; padding: 0;" i.bi.bi-list.fs-5 #chatHistorySidebar.collapse.collapse-horizontal.show.flex-grow-1 .d-flex.flex-column.h-100 style="width: 260px;" .d-flex.align-items-center.justify-content-between.px-3.pb-3.pt-1 strong.mb-0 Recent Chats = link_to assistant_conversations_path(assistant_id: @assistant.id), class: "btn btn-sm btn-outline-primary border-0 rounded-circle bg-white shadow-sm", data: { turbo_method: :post }, title: "New Chat" do i.feather-16 data-feather="plus" ul.list-group.list-group-flush.flex-grow-1.p-2.border-top style="overflow-y: auto;" - @conversations.each do |conversation| li class="assistant list-group-item p-1 mb-1 border-0 rounded-3 #{'active shadow-sm' if @conversation && conversation.id == @conversation.id}" style="background-color: transparent;" .d-flex.justify-content-between.align-items-center = link_to assistant_v2_assistant_path(@assistant, conversation_id: conversation.id), class: "text-decoration-none text-dark flex-grow-1 p-2 rounded-2 list-item-link" do div.fw-medium = "Chat ##{conversation.id}" small.text-muted.d-block style="font-size: 0.75rem;" i.bi.bi-person-fill.me-1 = conversation.user.name = link_to assistant_conversation_path(conversation, assistant_id: @assistant.id), class: "btn btn-sm text-muted border-0 hover-danger", data: { turbo_confirm: "Are you sure?", turbo_method: :delete }, title: "Delete" do i.bi.bi-trash3 ruby: is_empty = @conversation.assistant_messages.none? / Main Chat Container - if !@conversation.outdated .flex-grow-1.d-flex.flex-column.position-relative.p-3 data-question-assistant2-target="chatContainer" class=("empty-state" if is_empty) style="min-width: 0;" / Select-mode toggle - top-right of the message pane .d-flex.justify-content-end.mb-2 class=("d-none" if is_empty) button.btn.btn-sm.btn-outline-primary.d-flex.align-items-center.gap-1 type="button" data-question-assistant2-target="selectModeBtn" data-action="click->question-assistant2#toggleSelectionMode" title="Select messages" i.bi.bi-check2-square span Select / Messages Area / Wrapped in its own form so checked messages can be POSTed to bulk_action. / The bulk-actions-bar buttons below live outside this form and reference / it via the HTML `form="bulkActionsForm"` attribute. = form_with url: bulk_action_assistant_conversation_path(@conversation), method: :patch, html: { id: "bulkActionsForm", class: "d-flex flex-column flex-grow-1", style: "min-height: 0;", data: { "question-assistant2-target": "bulkForm", action: "turbo:submit-end->question-assistant2#handleBulkSubmitEnd" } } do |bf| .chat-messages.flex-grow-1.px-3.pb-3.mb-3#v2messages data-question-assistant2-target="messages" style="overflow-y: auto; scroll-behavior: smooth; border-radius: 12px; #{'display: none;' if is_empty}" = turbo_stream_from [@conversation, "messages"] - @conversation.assistant_messages.order(:created_at).each do |message| = render partial: "assistant_conversations/message", locals: { message: } / Bulk actions bar - hidden until selection mode is active, pinned above the input .bulk-actions-bar.d-none.align-items-center.justify-content-between.bg-white.border.rounded-3.shadow-sm.px-3.py-2.mb-2 data-question-assistant2-target="bulkActionsBar" .d-flex.align-items-center.gap-3 span.fw-semibold.small data-question-assistant2-target="selectionCount" 0 selected button.btn.btn-sm.btn-link.text-decoration-none.p-0 type="button" data-action="click->question-assistant2#cancelSelection" Cancel button.btn.btn-sm.btn-danger.d-flex.align-items-center.gap-1 type="submit" form="bulkActionsForm" name="commit_action" value="delete" disabled="disabled" data-turbo-confirm="Delete the selected messages? This can't be undone." data-question-assistant2-target="bulkDeleteBtn" i.bi.bi-trash3 | Delete Selected .chat-input-section.mt-auto data-question-assistant2-target="inputSection" = form_with(url: message_assistant_conversation_path(@conversation), html: { data: { "question-assistant2-target": "form" }, class: "mx-auto w-100", style: "max-width: 800px;" }) do |form| .unified-input-wrapper.position-relative.shadow-sm.bg-white.border.rounded-4.mb-3 style="z-index: 5; transition: box-shadow 0.2s ease, border-color 0.2s ease;" .position-relative = form.text_area :question, class: "form-control border-0 bg-transparent shadow-none", rows: "1", placeholder: "Ask anything about the respondents...", style: "resize: none; padding: 1rem 3.5rem 1rem 1.25rem; min-height: 56px; max-height: 200px; overflow-y: auto;", data: { "question-assistant2-target": "input", action: "input->question-assistant2#autoResize keydown->question-assistant2#handleKeydown" } button.btn.btn-success.position-absolute.rounded-circle.d-flex.align-items-center.justify-content-center.p-0.border-0.shadow-none type="submit" data-question-assistant2-target="submitBtn" style="width: 36px; height: 36px; bottom: 10px; right: 10px; background-color: #8DD262; transition: all 0.2s ease;" disabled="disabled" i.bi.bi-arrow-up.fs-5.text-white .px-3.pb-2 class=("d-none" if is_empty) data-question-assistant2-target="mainFiltersHeader" button.btn.btn-sm.btn-link.text-decoration-none.text-muted.p-0 type="button" data-bs-toggle="collapse" data-bs-target="#mainFilters" data-action="click->question-assistant2#toggleMainFilters" style="font-size: 0.85rem;" i.bi.bi-chevron-down.me-1 data-question-assistant2-target="mainToggleIcon" span data-question-assistant2-target="mainToggleText" Show Filters .collapse#mainFilters class=("show" if is_empty) data-question-assistant2-target="mainFilters" .border-top.p-3.bg-light.rounded-bottom-4 .row.g-2.align-items-center .col-md-5 label.form-label.text-muted.small.fw-semibold.mb-1 i.bi.bi-person.me-1 | Respondents select id="respondentSelect" multiple="multiple" name="respondent_ids[]" class="form-select" data-question-assistant2-target="respondentSelect" option value="all" selected="true" All optgroup label="Specific Respondents" - @assistant.respondents.each do |r| option value=r.id = r.name .col-md-2.text-center.d-flex.align-items-center.justify-content-center.pt-3.mt-3 span.badge.bg-white.text-primary.border OR .col-md-5 label.form-label.text-muted.small.fw-semibold.mb-1 i.bi.bi-tags.me-1 | Tags select#tagsSelect multiple="multiple" name="tags[]" class="form-control" data-question-assistant2-target="tagSelect" - tags_by_category = @assistant.respondents.flat_map(&:tags).uniq.group_by(&:tag_category) - tags_by_category.each do |category, tags| optgroup label=(category&.name || 'Uncategorized') - tags.each do |tag| option value=tag.id = tag.name .d-flex.justify-content-between.align-items-center.mt-3.pt-2.border-top button.btn.btn-sm.btn-link.text-decoration-none.text-muted.p-0 type="button" data-bs-toggle="collapse" data-bs-target="#additionalFilters" data-action="click->question-assistant2#toggleAdditionalFilters" i.bi.bi-chevron-down.me-1 data-question-assistant2-target="toggleIcon" span Show Additional Filters small.text-muted i.bi.bi-info-circle.me-1 | Filter context before asking .collapse#additionalFilters.mt-3 .border-top.pt-3 label.form-label.text-muted.small.fw-semibold.mb-1 i.bi.bi-list.me-1 | Section Limit select#sectionSelect name="section" class="form-control" data-question-assistant2-target="sectionSelect" option value="" disabled="" selected="" hidden="" Select a section / Fetch the specific respondent categories tied to this assistant - category_ids = @assistant.respondents.select(:respondent_category_id).distinct - categories = RespondentCategory.includes(:project, :master_sections).where(id: category_ids).order('projects.title', 'respondent_categories.name') / Group the sections by their parent Project and Category - categories.each do |category| - if category.master_sections.any? optgroup label="#{category.project.title} - #{category.name}" - category.master_sections.order(:position).each do |section| option value="#{section.id}" = section.name - else .flex-grow-1.d-flex.flex-column.position-relative.p-3 style="min-width: 0;" .chat-messages.flex-grow-1.border.rounded.p-3.pb-3.mb-3#v2messages style="overflow-y: auto; background-color: #F9FAFB;" - @conversation.assistant_messages.order(:created_at).each do |message| = render partial: "assistant_conversations/message", locals: { message: } .card.border-warning.bg-light.shadow-sm.rounded-4 .card-body.p-3 .d-flex.align-items-center.justify-content-between .d-flex.align-items-center .icon-stack.me-3 i.bi.bi-shield-exclamation.text-warning.fs-3 div h6.mb-0.fw-bold Read-Only Mode small.text-muted Source data has changed. Start a new chat to continue. = link_to assistant_conversations_path(assistant_id: @assistant.id), class: "btn btn-primary btn-sm px-4 rounded-pill shadow-sm", data: { turbo_method: :post } do | New Chat #citation.modal.fade data-bs-backdrop="static" data-bs-keyboard="true" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true" .modal-dialog.modal-lg .modal-content .modal-header.bg-light h5.modal-title.text-primary Source button type="button" class="btn-close btn-close-black" data-bs-dismiss="modal" aria-label="Close" .modal-body style="max-height: 70vh; overflow-y: auto;" #citationContent