From 8864f80ab7b9a2ba6e510ec7f4b9163d16e326cf Mon Sep 17 00:00:00 2001
From: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
Date: Thu, 13 Aug 2026 17:39:49 +0530
Subject: [PATCH] feat(captain): show document conversation usage (#15140)
## Summary
Adds conversation usage to Captain documents and user created FAQs. Only
knowledge used in a Captain answer sent to the contact is counted.
Lookups that end in a handoff are excluded.
Administrators can see how many distinct conversations used a knowledge
record. Document usage appears in the Usage tab inside document details.
User created FAQ usage appears on each FAQ card and opens in a
conversation panel.
Deleted conversations are excluded from counts, sorting, and
conversation lists.
Usage shown in side panel
Sorting options
## Access
Conversation usage is available only to administrators. Agents can still
view documents and FAQs, but they cannot see usage counts, the Usage
tab, the "Most used" sort, or conversation usage details.
The API applies the same rule. An agent request for `sort=most_used` is
rejected.
## Performance and pagination
Document and FAQ lists return 25 records per page. Usage counts are
calculated only for the records in each page.
Conversation usage panels load 25 conversations at a time and show a
"Load more" action when more conversations are available. The generated
FAQs tab also keeps its existing 25 item pagination.
The count queries use the JSON indexes on `document_ids` and
`used_faq_ids`. The "Most used" sort aggregates one assistant's sessions
once before it sorts and returns the requested document page.
## How to test
1. Sign in as an administrator and open the Captain documents page for
an assistant with tracked document usage.
2. Open a document and confirm that the Usage tab shows the distinct
conversation count and the matching conversations.
3. Select "Most used" and confirm that documents are ordered by distinct
conversation usage.
4. Open the user created FAQs page and confirm that FAQ cards show their
usage count and open the matching conversations.
5. Confirm that usage panels show 25 conversations first and can load
the next page.
6. Trigger a knowledge lookup that ends in a handoff and confirm that it
does not increase document or FAQ usage.
7. Sign in as an agent and confirm that usage counts, usage details, and
the "Most used" sort are not available.
## Closes
[CW-7498](https://linear.app/chatwoot/issue/CW-7498/fe)
---
.../dashboard/api/captain/document.js | 7 +
.../dashboard/api/captain/response.js | 7 +
.../captain/assistant/DocumentCard.spec.js | 93 +++++++++
.../captain/assistant/DocumentCard.vue | 47 ++---
.../assistant/DocumentFiltersBar.spec.js | 79 ++++++++
.../captain/assistant/DocumentFiltersBar.vue | 24 ++-
.../captain/assistant/ResponseCard.spec.js | 98 +++++++++
.../captain/assistant/ResponseCard.vue | 58 +++++-
.../ConversationUsageDrawer.spec.js | 93 +++++++++
.../ConversationUsageDrawer.vue | 129 ++++++++++++
.../document/DocumentDetails.spec.js | 130 +++++++++++-
.../document/DocumentDetails.vue | 104 +++++++++-
.../i18n/locale/en/integrations.json | 7 +-
.../dashboard/captain/responses/Index.vue | 33 +++
.../reports/composables/useReportDrilldown.js | 5 +-
config/routes.rb | 5 +-
...dd_index_on_agent_sessions_document_ids.rb | 7 +
db/schema.rb | 3 +-
.../captain/conversation_usage_builder.rb | 188 ++++++++++++++++++
.../captain/assistant_responses_controller.rb | 25 ++-
.../accounts/captain/documents_controller.rb | 32 ++-
.../app/models/captain/agent_session.rb | 2 +
.../assistant_responses/index.json.jbuilder | 1 +
.../captain/documents/index.json.jbuilder | 1 +
.../assistant_responses_controller_spec.rb | 145 ++++++++++++++
.../captain/documents_controller_spec.rb | 183 +++++++++++++++++
26 files changed, 1442 insertions(+), 64 deletions(-)
create mode 100644 app/javascript/dashboard/components-next/captain/assistant/DocumentCard.spec.js
create mode 100644 app/javascript/dashboard/components-next/captain/assistant/DocumentFiltersBar.spec.js
create mode 100644 app/javascript/dashboard/components-next/captain/assistant/ResponseCard.spec.js
create mode 100644 app/javascript/dashboard/components-next/captain/pageComponents/ConversationUsageDrawer.spec.js
create mode 100644 app/javascript/dashboard/components-next/captain/pageComponents/ConversationUsageDrawer.vue
create mode 100644 db/migrate/20260806000000_add_index_on_agent_sessions_document_ids.rb
create mode 100644 enterprise/app/builders/captain/conversation_usage_builder.rb
diff --git a/app/javascript/dashboard/api/captain/document.js b/app/javascript/dashboard/api/captain/document.js
index e23a8c460..2b33d86ac 100644
--- a/app/javascript/dashboard/api/captain/document.js
+++ b/app/javascript/dashboard/api/captain/document.js
@@ -22,6 +22,13 @@ class CaptainDocument extends ApiClient {
sync(id) {
return axios.post(`${this.url}/${id}/sync`);
}
+
+ getDrilldown({ documentId, page, signal }) {
+ const requestConfig = { params: { page } };
+ if (signal) requestConfig.signal = signal;
+
+ return axios.get(`${this.url}/${documentId}/drilldown`, requestConfig);
+ }
}
export default new CaptainDocument();
diff --git a/app/javascript/dashboard/api/captain/response.js b/app/javascript/dashboard/api/captain/response.js
index 6e4ccf5fe..f4a5c6696 100644
--- a/app/javascript/dashboard/api/captain/response.js
+++ b/app/javascript/dashboard/api/captain/response.js
@@ -17,6 +17,13 @@ class CaptainResponses extends ApiClient {
signal,
});
}
+
+ getDrilldown({ responseId, page, signal }) {
+ const requestConfig = { params: { page } };
+ if (signal) requestConfig.signal = signal;
+
+ return axios.get(`${this.url}/${responseId}/drilldown`, requestConfig);
+ }
}
export default new CaptainResponses();
diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.spec.js b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.spec.js
new file mode 100644
index 000000000..6d8c20a5e
--- /dev/null
+++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.spec.js
@@ -0,0 +1,93 @@
+import { shallowMount } from '@vue/test-utils';
+import DocumentCard from './DocumentCard.vue';
+
+const { checkPermissions } = vi.hoisted(() => ({
+ checkPermissions: vi.fn(() => true),
+}));
+
+vi.mock('dashboard/composables/usePolicy', () => ({
+ usePolicy: () => ({ checkPermissions }),
+}));
+
+vi.mock('vue-i18n', () => ({
+ useI18n: () => ({
+ t: (key, { n } = {}) => {
+ if (key === 'CAPTAIN.DOCUMENTS.FAQ_COUNT') return `${n} FAQs`;
+ if (key === 'CAPTAIN.DOCUMENTS.USED_IN_CONVERSATIONS') {
+ return `Used in ${n} conversations`;
+ }
+ return key;
+ },
+ }),
+}));
+
+const ButtonStub = {
+ inheritAttrs: false,
+ props: ['label', 'disabled'],
+ emits: ['click'],
+ template:
+ '',
+};
+
+const mountCard = (props = {}) =>
+ shallowMount(DocumentCard, {
+ props: {
+ id: 42,
+ name: 'Returns and refunds',
+ assistant: { name: 'Acme assistant' },
+ externalLink:
+ 'https://example.com/help/articles/refund-and-return-policy-for-online-orders',
+ createdAt: 1_700_000_000,
+ status: 'available',
+ responsesCount: 12,
+ ...props,
+ },
+ global: {
+ directives: { onClickaway: {} },
+ stubs: {
+ Button: ButtonStub,
+ CardLayout: { template: '