diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue index 9d6d574ec..8ff38b2eb 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue @@ -66,6 +66,10 @@ const props = defineProps({ type: Number, default: null, }, + responsesCount: { + type: Number, + default: 0, + }, isSelected: { type: Boolean, default: false, @@ -112,10 +116,10 @@ const showSyncStatus = computed(() => !isPdf.value); const menuItems = computed(() => { const allOptions = [ { - label: t('CAPTAIN.DOCUMENTS.OPTIONS.VIEW_RELATED_RESPONSES'), - value: 'viewRelatedQuestions', - action: 'viewRelatedQuestions', - icon: 'i-ph-tree-view-duotone', + label: t('CAPTAIN.DOCUMENTS.OPTIONS.VIEW_DETAILS'), + value: 'viewDetails', + action: 'viewDetails', + icon: 'i-lucide-eye', }, ]; @@ -143,6 +147,9 @@ const menuItems = computed(() => { }); const createdAtLabel = computed(() => dynamicTime(props.createdAt)); +const responsesCountLabel = computed(() => + t('CAPTAIN.DOCUMENTS.FAQ_COUNT', { n: props.responsesCount }) +); const displayLink = computed(() => isPdf.value @@ -158,6 +165,10 @@ const handleAction = ({ action, value }) => { emit('action', { action, value, id: props.id }); }; +const handleViewDetails = () => { + emit('action', { action: 'viewDetails', id: props.id }); +}; + const handleRetry = () => { emit('action', { action: 'sync', id: props.id }); }; @@ -177,9 +188,13 @@ const handleRetry = () => {
- +
{ {{ displayLink }} + + {{ responsesCountLabel }} + ({ + dispatch: vi.fn(), + getterValues: { + 'captainResponses/getUIFlags': { value: { fetchingList: false } }, + 'captainResponses/getRecords': { value: [] }, + 'captainResponses/getMeta': { value: { totalCount: 26, page: 1 } }, + }, +})); + +vi.mock('dashboard/composables/store', () => ({ + useStore: () => ({ dispatch }), + useMapGetter: key => getterValues[key], +})); + +vi.mock('dashboard/composables', () => ({ useAlert: vi.fn() })); + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ t: key => key }), +})); + +const captainDocument = { + id: 42, + name: 'FAQ source', + external_link: 'https://example.com/docs', + assistant: { id: 7 }, + content: 'Document content', + pdf_document: false, +}; + +const DialogStub = { + name: 'Dialog', + template: '
', +}; + +const TabBarStub = { + name: 'TabBar', + template: + '
- ''; + } + get formattedMessage() { return this.formatMessage(); } diff --git a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js index 3350399eb..20d64005a 100644 --- a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js +++ b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js @@ -68,6 +68,25 @@ describe('#MessageFormatter', () => { }); }); + describe('#disableImageRendering', () => { + it('omits nested and reference images with relative URLs', () => { + const message = `Before ![nested [alt]](/relative.png) + +![reference][logo] + +[logo]: /logo.png + +After`; + const formatter = new MessageFormatter(message); + + formatter.disableImageRendering(); + + expect(formatter.formattedMessage).not.toContain(' { it('should return the same string if not tags or @mentions', () => { const message = 'Chatwoot is an opensource tool'; diff --git a/enterprise/app/controllers/api/v1/accounts/captain/documents_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/documents_controller.rb index 273c082b1..d88cc6b48 100644 --- a/enterprise/app/controllers/api/v1/accounts/captain/documents_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/captain/documents_controller.rb @@ -9,16 +9,10 @@ class Api::V1::Accounts::Captain::DocumentsController < Api::V1::Accounts::BaseC RESULTS_PER_PAGE = 25 def index - base_query = @documents - base_query = base_query.where(assistant_id: permitted_params[:assistant_id]) if permitted_params[:assistant_id].present? - base_query = apply_source_filter(base_query, permitted_params[:source]) - base_query = apply_filter(base_query, permitted_params[:filter]) - base_query = apply_search(base_query, permitted_params[:search_key]) - base_query = apply_sort(base_query, permitted_params[:sort]) - - @documents_count = base_query.count + @documents = filtered_documents + @documents_count = @documents.count @sync_interval_hours = current_sync_interval&.in_hours&.to_i - @documents = base_query.page(@current_page).per(RESULTS_PER_PAGE) + @documents = with_responses_count(@documents).page(@current_page).per(RESULTS_PER_PAGE) end def show; end @@ -59,6 +53,21 @@ class Api::V1::Accounts::Captain::DocumentsController < Api::V1::Accounts::BaseC @documents = Current.account.captain_documents.with_attached_pdf_file.includes(:assistant) end + def filtered_documents + documents = @documents + documents = documents.where(assistant_id: permitted_params[:assistant_id]) if permitted_params[:assistant_id].present? + documents = apply_source_filter(documents, permitted_params[:source]) + documents = apply_filter(documents, permitted_params[:filter]) + documents = apply_search(documents, permitted_params[:search_key]) + apply_sort(documents, permitted_params[:sort]) + end + + def with_responses_count(scope) + scope.left_joins(:responses) + .select('captain_documents.*, COUNT(captain_assistant_responses.id) AS responses_count') + .group('captain_documents.id') + end + def set_document @document = @documents.find(permitted_params[:id]) end diff --git a/enterprise/app/views/api/v1/models/captain/_document.json.jbuilder b/enterprise/app/views/api/v1/models/captain/_document.json.jbuilder index 56260f675..0ab031dbf 100644 --- a/enterprise/app/views/api/v1/models/captain/_document.json.jbuilder +++ b/enterprise/app/views/api/v1/models/captain/_document.json.jbuilder @@ -9,6 +9,8 @@ json.external_link resource.external_link json.display_url resource.display_url json.file_size resource.file_size json.pdf_document resource.pdf_document? +responses_count = resource.respond_to?(:responses_count) ? resource.responses_count : resource.responses.count +json.responses_count responses_count.to_i json.id resource.id json.name resource.name json.status resource.status diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/documents_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/documents_controller_spec.rb index 77cb25f49..4d4b10fcb 100644 --- a/spec/enterprise/controllers/api/v1/accounts/captain/documents_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/captain/documents_controller_spec.rb @@ -51,6 +51,18 @@ RSpec.describe 'Api::V1::Accounts::Captain::Documents', type: :request do expect(json_response[:payload].length).to eq(5) expect(json_response[:meta]).to eq({ page: 2, total_count: 30 }) end + + it 'returns the generated FAQ count for each document' do + document = create(:captain_document, assistant: assistant, account: account) + create_list(:captain_assistant_response, 2, + assistant: assistant, account: account, documentable: document) + + get "/api/v1/accounts/#{account.id}/captain/documents", + headers: agent.create_new_auth_token, as: :json + + matching_document = json_response[:payload].find { |item| item[:id] == document.id } + expect(matching_document[:responses_count]).to eq(2) + end end context 'when filtering by assistant_id' do @@ -142,6 +154,10 @@ RSpec.describe 'Api::V1::Accounts::Captain::Documents', type: :request do expect(json_response[:external_link]).to eq(document.external_link) end + it 'returns the crawled content for the document' do + expect(json_response[:content]).to eq(document.content) + end + it 'returns sync metadata when the document has been synced' do synced_at = 1.hour.ago document.update!(sync_status: :synced, last_synced_at: synced_at)