{{ label }}
{
);
});
+const toggleChatSupport = () => {
+ if (window.$chatwoot) {
+ window.$chatwoot.toggle();
+ }
+};
+
const menuItems = computed(() => {
return [
{
@@ -51,9 +57,7 @@ const menuItems = computed(() => {
showOnCustomBrandedInstance: false,
label: t('SIDEBAR_ITEMS.CONTACT_SUPPORT'),
icon: 'i-lucide-life-buoy',
- click: () => {
- window.$chatwoot.toggle();
- },
+ click: toggleChatSupport,
},
{
show: true,
diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json
index a6f3ef2f2..d3dc538a9 100644
--- a/app/javascript/dashboard/i18n/locale/en/integrations.json
+++ b/app/javascript/dashboard/i18n/locale/en/integrations.json
@@ -439,6 +439,13 @@
"HINT": "Average replies the assistant sends per conversation."
}
},
+ "DRILLDOWN": {
+ "CLOSE": "Close details",
+ "EMPTY": "No records found for this metric.",
+ "ERROR": "Could not load records. Please try again.",
+ "LOAD_MORE": "Load more",
+ "RESULT_COUNT_CONVERSATION": "{count} conversation | {count} conversations"
+ },
"KNOWLEDGE": {
"TITLE": "Knowledge coverage",
"COVERAGE": "{pct}% approved",
@@ -446,17 +453,6 @@
"PENDING": "Pending FAQs",
"DOCUMENTS": "Documents"
},
- "FLAGGED": {
- "TITLE": "Response quality",
- "TOTAL": "{count} flagged ยท {rate}"
- },
- "CREDITS": {
- "TITLE": "Credit usage",
- "UNIT": "credits",
- "LEGEND": "Daily credits used",
- "AXIS_START": "{count}d ago",
- "AXIS_END": "Today"
- },
"LINKS": {
"DOCS": {
"TITLE": "Captain docs",
@@ -470,13 +466,6 @@
"TITLE": "Billing",
"DESCRIPTION": "Manage credits and plan"
}
- },
- "FLAG_REASONS": {
- "INCORRECT": "Incorrect info",
- "INCOMPLETE": "Incomplete",
- "OUTDATED": "Outdated",
- "INAPPROPRIATE": "Inappropriate",
- "OTHER": "Other"
}
},
"ASSISTANT_SWITCHER": {
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index f8e973e9a..b621e63b1 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -263,7 +263,7 @@
"EMAIL_VERIFICATION_SENT": "Verification email has been sent. Please check your inbox.",
"ACCOUNT_SUSPENDED": {
"TITLE": "Account Suspended",
- "MESSAGE": "Your account has been suspended after we detected activity that may violate our policies or put other users at risk. If you believe this is a mistake, please contact our support team."
+ "MESSAGE": "Your account has been suspended due to activity that may violate our policies. If you believe this is a mistake, please contact our support team."
},
"NO_ACCOUNTS": {
"TITLE": "No account found",
diff --git a/app/javascript/dashboard/routes/dashboard/captain/assistants/overview/Index.vue b/app/javascript/dashboard/routes/dashboard/captain/assistants/overview/Index.vue
index b9cc6ae35..29411e56d 100644
--- a/app/javascript/dashboard/routes/dashboard/captain/assistants/overview/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/captain/assistants/overview/Index.vue
@@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
+import { usePolicy } from 'dashboard/composables/usePolicy';
import CaptainAssistant from 'dashboard/api/captain/assistant';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
@@ -10,6 +11,7 @@ import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Pay
import RangeSelector from 'dashboard/components-next/captain/pageComponents/overview/RangeSelector.vue';
import WelcomeCard from 'dashboard/components-next/captain/pageComponents/overview/WelcomeCard.vue';
import MetricCard from 'dashboard/components-next/captain/pageComponents/overview/MetricCard.vue';
+import AssistantDrilldownDrawer from 'dashboard/components-next/captain/pageComponents/overview/AssistantDrilldownDrawer.vue';
import KnowledgeCard from 'dashboard/components-next/captain/pageComponents/overview/KnowledgeCard.vue';
import QuickLinks from 'dashboard/components-next/captain/pageComponents/overview/QuickLinks.vue';
import InboxBanner from 'dashboard/components-next/captain/pageComponents/overview/InboxBanner.vue';
@@ -17,6 +19,9 @@ import CoverageBanner from 'dashboard/components-next/captain/pageComponents/ove
const { t } = useI18n();
const route = useRoute();
+// Drilldown is admin-only; the backend policy enforces the same restriction.
+const { checkPermissions } = usePolicy();
+const canDrilldown = computed(() => checkPermissions(['administrator']));
const selectedRange = ref('this_month');
@@ -69,34 +74,38 @@ const metricFor = (statKey, formatValue, direction, trendKind = 'percent') => {
const metrics = computed(() => [
{
key: 'handled',
+ metric: 'conversations_handled',
label: t('CAPTAIN.OVERVIEW.METRICS.HANDLED.LABEL'),
hint: t('CAPTAIN.OVERVIEW.METRICS.HANDLED.HINT'),
...metricFor('conversations_handled', v => v.toLocaleString(), 'up'),
},
{
key: 'autoResolution',
+ metric: 'auto_resolution_rate',
label: t('CAPTAIN.OVERVIEW.METRICS.AUTO_RESOLUTION.LABEL'),
hint: t('CAPTAIN.OVERVIEW.METRICS.AUTO_RESOLUTION.HINT'),
...metricFor('auto_resolution_rate', v => `${v}%`, 'up', 'point'),
},
{
key: 'handoff',
+ metric: 'handoff_rate',
label: t('CAPTAIN.OVERVIEW.METRICS.HANDOFF.LABEL'),
hint: t('CAPTAIN.OVERVIEW.METRICS.HANDOFF.HINT'),
...metricFor('handoff_rate', v => `${v}%`, 'down', 'point'),
},
+ {
+ key: 'reopen',
+ metric: 'reopen_rate',
+ label: t('CAPTAIN.OVERVIEW.METRICS.REOPEN.LABEL'),
+ hint: t('CAPTAIN.OVERVIEW.METRICS.REOPEN.HINT'),
+ ...metricFor('reopen_rate', v => `${v}%`, 'down', 'point'),
+ },
{
key: 'hoursSaved',
label: t('CAPTAIN.OVERVIEW.METRICS.HOURS_SAVED.LABEL'),
hint: t('CAPTAIN.OVERVIEW.METRICS.HOURS_SAVED.HINT'),
...metricFor('hours_saved', formatDuration, 'up'),
},
- {
- key: 'reopen',
- label: t('CAPTAIN.OVERVIEW.METRICS.REOPEN.LABEL'),
- hint: t('CAPTAIN.OVERVIEW.METRICS.REOPEN.HINT'),
- ...metricFor('reopen_rate', v => `${v}%`, 'down', 'point'),
- },
{
key: 'depth',
label: t('CAPTAIN.OVERVIEW.METRICS.DEPTH.LABEL'),
@@ -109,6 +118,22 @@ const metrics = computed(() => [
),
},
]);
+
+const drilldown = ref({ metric: '', label: '', value: '' });
+const isDrilldownOpen = ref(false);
+
+const openDrilldown = metric => {
+ drilldown.value = {
+ metric: metric.metric,
+ label: metric.label,
+ value: metric.value,
+ };
+ isDrilldownOpen.value = true;
+};
+
+const closeDrilldown = () => {
+ isDrilldownOpen.value = false;
+};
@@ -144,6 +169,8 @@ const metrics = computed(() => [
:trend="metric.trend"
:hint="metric.hint"
:trend-good="metric.trendGood"
+ :clickable="canDrilldown && Boolean(metric.metric)"
+ @click="openDrilldown(metric)"
/>
@@ -151,6 +178,17 @@ const metrics = computed(() => [
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/composables/useReportDrilldown.js b/app/javascript/dashboard/routes/dashboard/settings/reports/composables/useReportDrilldown.js
index 7c37cd9cc..8309ed360 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/composables/useReportDrilldown.js
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/composables/useReportDrilldown.js
@@ -1,7 +1,13 @@
import { computed, ref } from 'vue';
import ReportsAPI from 'dashboard/api/reports';
-export function useReportDrilldown() {
+// `fetcher` is any `({ ...request, page, signal }) => Promise` returning the
+// shared drilldown envelope (`{ data: { meta, payload } }`), so the same paging
+// and abort machinery backs both the reports and Captain assistant drilldowns.
+// The default is wrapped so `ReportsAPI` stays the receiver when invoked.
+export function useReportDrilldown(
+ fetcher = params => ReportsAPI.getDrilldown(params)
+) {
const activeRequest = ref(null);
const records = ref([]);
const meta = ref({});
@@ -20,17 +26,7 @@ export function useReportDrilldown() {
const isCurrentRequest = token =>
token === requestToken && !!activeRequest.value;
- const requestFingerprint = request =>
- JSON.stringify({
- metric: request.metric,
- bucketTimestamp: request.bucketTimestamp,
- from: request.from,
- to: request.to,
- type: request.type,
- id: request.id,
- groupBy: request.groupBy,
- businessHours: request.businessHours,
- });
+ const requestFingerprint = request => JSON.stringify(request);
const abortActiveRequest = () => {
if (!activeRequestController) return;
@@ -55,7 +51,7 @@ export function useReportDrilldown() {
hasError.value = false;
try {
- const response = await ReportsAPI.getDrilldown({
+ const response = await fetcher({
...request,
page,
signal: controller.signal,
diff --git a/app/javascript/dashboard/routes/dashboard/suspended/Index.vue b/app/javascript/dashboard/routes/dashboard/suspended/Index.vue
index 56a5b5cae..027f75ff5 100644
--- a/app/javascript/dashboard/routes/dashboard/suspended/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/suspended/Index.vue
@@ -1,5 +1,6 @@