chordia-ui 3.9.0 → 3.9.2

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.
Files changed (48) hide show
  1. package/dist/DataTable2.cjs.js +1 -1
  2. package/dist/DataTable2.cjs.js.map +1 -1
  3. package/dist/DataTable2.es.js +611 -604
  4. package/dist/DataTable2.es.js.map +1 -1
  5. package/dist/PerformancePanel.cjs.js +1 -1
  6. package/dist/PerformancePanel.cjs.js.map +1 -1
  7. package/dist/PerformancePanel.es.js +1068 -816
  8. package/dist/PerformancePanel.es.js.map +1 -1
  9. package/dist/SupervisorSelect.cjs.js +2 -0
  10. package/dist/SupervisorSelect.cjs.js.map +1 -0
  11. package/dist/SupervisorSelect.es.js +352 -0
  12. package/dist/SupervisorSelect.es.js.map +1 -0
  13. package/dist/components/Signals.cjs.js +1 -1
  14. package/dist/components/Signals.cjs.js.map +1 -1
  15. package/dist/components/Signals.es.js +273 -318
  16. package/dist/components/Signals.es.js.map +1 -1
  17. package/dist/components/UpdatedInteractionDetails.cjs.js +3 -3
  18. package/dist/components/UpdatedInteractionDetails.cjs.js.map +1 -1
  19. package/dist/components/UpdatedInteractionDetails.es.js +98 -92
  20. package/dist/components/UpdatedInteractionDetails.es.js.map +1 -1
  21. package/dist/components/performance.cjs.js +1 -1
  22. package/dist/components/performance.cjs.js.map +1 -1
  23. package/dist/components/performance.es.js +116 -349
  24. package/dist/components/performance.es.js.map +1 -1
  25. package/dist/components/reports.cjs.js +2 -2
  26. package/dist/components/reports.cjs.js.map +1 -1
  27. package/dist/components/reports.es.js +122 -141
  28. package/dist/components/reports.es.js.map +1 -1
  29. package/dist/index.cjs.js +1 -1
  30. package/dist/index.es.js +1 -1
  31. package/dist/pages/interactionDetails.cjs.js +2 -2
  32. package/dist/pages/interactionDetails.cjs.js.map +1 -1
  33. package/dist/pages/interactionDetails.es.js +130 -124
  34. package/dist/pages/interactionDetails.es.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/components/Signals/SignalDetailsPage.jsx +2 -29
  37. package/src/components/Signals/SignalsListPage.jsx +2 -24
  38. package/src/components/UpdatedInteractionDetails/UpdatedCoachingSynthesisCard.jsx +8 -1
  39. package/src/components/data/DataTable2.jsx +19 -5
  40. package/src/components/pages/interactionDetails/CoachingSynthesisCard.jsx +8 -1
  41. package/src/components/performance/PerformanceDetailsPage.jsx +230 -75
  42. package/src/components/performance/PerformancePanel.jsx +42 -45
  43. package/src/components/performance/index.js +27 -0
  44. package/src/components/performance/performanceApiMap.js +133 -0
  45. package/src/components/performance/performanceMetrics.js +63 -0
  46. package/src/components/performance/performanceRangeFormat.js +92 -0
  47. package/src/components/performance/performanceSparkline.js +52 -0
  48. package/src/components/reports/ReportsList.jsx +2 -25
@@ -0,0 +1,133 @@
1
+ import { trendFromPoints } from "./performanceSparkline";
2
+
3
+ const METRIC_PLACEHOLDER = "—";
4
+
5
+ function isNullish(value) {
6
+ return value == null || (typeof value === "number" && Number.isNaN(value));
7
+ }
8
+
9
+ function trendFromDelta(delta) {
10
+ if (isNullish(delta) || delta === 0) return "up";
11
+ return delta > 0 ? "up" : "down";
12
+ }
13
+
14
+ function formatSignedDelta(delta, { suffix = "", decimals = 1 } = {}) {
15
+ if (isNullish(delta)) return null;
16
+ const n = Number(delta);
17
+ if (!Number.isFinite(n)) return null;
18
+ const sign = n > 0 ? "+" : n < 0 ? "" : "";
19
+ return `${sign}${n.toFixed(decimals)}${suffix}`;
20
+ }
21
+
22
+ /** Pull a numeric time series from API `trend[]` buckets (skips nulls). */
23
+ export function extractTrendSeries(trend, key, { includeZero = true } = {}) {
24
+ if (!Array.isArray(trend)) return [];
25
+ return trend
26
+ .map((bucket) => bucket?.[key])
27
+ .filter((v) => {
28
+ if (v == null) return false;
29
+ const n = Number(v);
30
+ if (!Number.isFinite(n)) return false;
31
+ return includeZero || n !== 0;
32
+ })
33
+ .map(Number);
34
+ }
35
+
36
+ function formatPercent(value, decimals = 1) {
37
+ if (isNullish(value)) return METRIC_PLACEHOLDER;
38
+ return `${(Number(value) * 100).toFixed(decimals)}%`;
39
+ }
40
+
41
+ function formatPp(value, decimals = 1) {
42
+ if (isNullish(value)) return METRIC_PLACEHOLDER;
43
+ const n = Number(value) * 100;
44
+ const sign = n > 0 ? "+" : "";
45
+ return `${sign}${n.toFixed(decimals)}pp`;
46
+ }
47
+
48
+ function formatCount(value) {
49
+ if (isNullish(value)) return METRIC_PLACEHOLDER;
50
+ return Number(value).toLocaleString("en-US");
51
+ }
52
+
53
+ function formatCsat(value, decimals = 2) {
54
+ if (isNullish(value)) return METRIC_PLACEHOLDER;
55
+ return Number(value).toFixed(decimals);
56
+ }
57
+
58
+ function statLabel(base, delta, formatter) {
59
+ const deltaStr = formatter(delta);
60
+ return deltaStr ? `${base} (${deltaStr})` : base;
61
+ }
62
+
63
+ /**
64
+ * Map `/performance/agents/{projectId}/v2/__all__` (or agent-specific) payload
65
+ * to PerformancePanel KPI stat props. Pass each stat's `points` into the panel
66
+ * for real sparklines instead of the decorative fallback curve.
67
+ */
68
+ export function mapPerformanceKpiStats(api) {
69
+ const current = api?.current ?? {};
70
+ const deltas = api?.deltas ?? {};
71
+ const trend = api?.trend ?? [];
72
+
73
+ const volumePoints = extractTrendSeries(trend, "count", { includeZero: true });
74
+ const csatPoints = extractTrendSeries(trend, "csat");
75
+ const baselinePoints = extractTrendSeries(trend, "baseline").map((v) => v * 100);
76
+ const liftPoints = extractTrendSeries(trend, "agent_lift").map((v) => v * 100);
77
+ const scorePoints = extractTrendSeries(trend, "score").map((v) => v * 100);
78
+
79
+ const volumeStat = {
80
+ value: formatCount(current.call_count),
81
+ label: "Volume",
82
+ trend: trendFromDelta(deltas.call_count),
83
+ points: volumePoints,
84
+ };
85
+
86
+ const compassStat = {
87
+ value: formatCsat(current.avg_csat),
88
+ label: statLabel("Compass Score", deltas.csat, (d) => formatSignedDelta(d, { decimals: 2 })),
89
+ trend: trendFromDelta(deltas.csat),
90
+ points: csatPoints,
91
+ };
92
+
93
+ const baselineSubStat = {
94
+ value: formatPercent(current.avg_baseline),
95
+ label: statLabel("Baseline", deltas.baseline, (d) =>
96
+ formatSignedDelta(Number(d) * 100, { suffix: "", decimals: 1 }),
97
+ ),
98
+ trend: trendFromDelta(deltas.baseline),
99
+ points: baselinePoints,
100
+ };
101
+
102
+ const agentLiftSubStat = {
103
+ value: formatPp(current.avg_agent_lift),
104
+ label: statLabel("Agent Lift", deltas.agent_lift, (d) =>
105
+ formatSignedDelta(Number(d) * 100, { suffix: "pp", decimals: 1 }),
106
+ ),
107
+ trend: trendFromDelta(deltas.agent_lift),
108
+ points: liftPoints,
109
+ };
110
+
111
+ const scoreSubStat = {
112
+ value: formatPercent(current.avg_score),
113
+ label: statLabel("Score", deltas.score, (d) =>
114
+ formatSignedDelta(Number(d) * 100, { suffix: "", decimals: 1 }),
115
+ ),
116
+ trend: trendFromDelta(deltas.score),
117
+ points: scorePoints,
118
+ };
119
+
120
+ // When points exist, prefer shape-derived direction for sparkline consistency.
121
+ for (const stat of [volumeStat, compassStat, baselineSubStat, agentLiftSubStat, scoreSubStat]) {
122
+ const derived = trendFromPoints(stat.points);
123
+ if (derived) stat.trend = derived;
124
+ }
125
+
126
+ return {
127
+ volumeStat,
128
+ compassStat,
129
+ baselineSubStat,
130
+ agentLiftSubStat,
131
+ scoreSubStat,
132
+ };
133
+ }
@@ -0,0 +1,63 @@
1
+ import { trendFromPoints } from "./performanceSparkline.js";
2
+
3
+ export const METRIC_PLACEHOLDER = "—";
4
+
5
+ /** Hosts often send "-", "—", or empty strings when API delta/value is null. */
6
+ export function isNullishMetric(value) {
7
+ if (value == null) return true;
8
+ const str = String(value).trim();
9
+ if (!str) return true;
10
+ if (/^[-−–—]$/.test(str)) return true;
11
+ if (/^(n\/a|null|undefined)$/i.test(str)) return true;
12
+ return false;
13
+ }
14
+
15
+ export function hasMeaningfulMetric(value) {
16
+ return !isNullishMetric(value);
17
+ }
18
+
19
+ export function formatMetricDisplay(value, fallback = METRIC_PLACEHOLDER) {
20
+ return isNullishMetric(value) ? fallback : String(value);
21
+ }
22
+
23
+ /** Non-empty host field — includes placeholder dashes the API uses for missing data. */
24
+ export function hasHostMetricField(value) {
25
+ return value != null && String(value).trim() !== "";
26
+ }
27
+
28
+ /**
29
+ * Whether a chart card should render. Matches legacy `value || delta || points.length`
30
+ * while treating numeric zero as present data.
31
+ */
32
+ export function hasChartData(chart) {
33
+ if (!chart) return false;
34
+ if (hasHostMetricField(chart.value) || hasHostMetricField(chart.delta)) return true;
35
+ return Array.isArray(chart.points) && chart.points.length > 0;
36
+ }
37
+
38
+ /** Derive up/down from signed deltas (+2.0%, -3pp, 95.5% (0.6pp), etc.). */
39
+ export function trendFromSignedValue(text) {
40
+ if (isNullishMetric(text)) return null;
41
+ const str = String(text).trim();
42
+ const parenMatch = str.match(/\(\s*([+-])?\s*([\d.]+)/);
43
+ if (parenMatch) {
44
+ if (parenMatch[1] === "-") return "down";
45
+ if (parenMatch[1] === "+") return "up";
46
+ return parseFloat(parenMatch[2]) < 0 ? "down" : "up";
47
+ }
48
+ if (/^[-−]/.test(str)) return "down";
49
+ if (/^\+/.test(str)) return "up";
50
+ if (/-\d/.test(str)) return "down";
51
+ return null;
52
+ }
53
+
54
+ export function resolveTrend(trend, signedText) {
55
+ return trendFromSignedValue(signedText) ?? (trend === "down" ? "down" : "up");
56
+ }
57
+
58
+ export function resolveKpiTrend(trend, value, points) {
59
+ return (
60
+ trendFromPoints(points) ??
61
+ (hasMeaningfulMetric(value) ? resolveTrend(trend, value) : trend === "down" ? "down" : "up")
62
+ );
63
+ }
@@ -0,0 +1,92 @@
1
+ // Shared en-US date/time formatting for time-range chips (8h / 1d / 7d / 30d).
2
+
3
+ export const RANGE_TO_MS = {
4
+ "8h": 8 * 60 * 60 * 1000,
5
+ "1d": 24 * 60 * 60 * 1000,
6
+ "7d": 7 * 24 * 60 * 60 * 1000,
7
+ "30d": 30 * 24 * 60 * 60 * 1000,
8
+ };
9
+
10
+ const LOCALE = "en-US";
11
+ const DATE_OPTS = { month: "2-digit", day: "2-digit", year: "numeric" };
12
+ const TIME_OPTS = { hour: "numeric", minute: "2-digit", hour12: true };
13
+
14
+ export function coalesceToDate(input) {
15
+ if (!input) return null;
16
+ if (input instanceof Date) {
17
+ return Number.isNaN(input.getTime()) ? null : input;
18
+ }
19
+ const str = String(input).trim();
20
+ const ymd = str.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
21
+ if (ymd) {
22
+ const d = new Date(Number(ymd[1]), Number(ymd[2]) - 1, Number(ymd[3]));
23
+ return Number.isNaN(d.getTime()) ? null : d;
24
+ }
25
+ const parsed = new Date(str);
26
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
27
+ }
28
+
29
+ function isSameLocalCalendarDay(a, b) {
30
+ return (
31
+ a.getFullYear() === b.getFullYear() &&
32
+ a.getMonth() === b.getMonth() &&
33
+ a.getDate() === b.getDate()
34
+ );
35
+ }
36
+
37
+ export function formatLocalDate(date) {
38
+ const d = date instanceof Date ? date : coalesceToDate(date);
39
+ if (!d) return "";
40
+ return d.toLocaleDateString(LOCALE, DATE_OPTS);
41
+ }
42
+
43
+ function formatLocalTime(date) {
44
+ return date.toLocaleTimeString(LOCALE, TIME_OPTS);
45
+ }
46
+
47
+ function formatLocalDateTime(date) {
48
+ return `${formatLocalDate(date)} ${formatLocalTime(date)}`;
49
+ }
50
+
51
+ /** Single-line label for the 8h chip (panel header). */
52
+ export function formatEightHourWindowLabel(from, to) {
53
+ const f = coalesceToDate(from);
54
+ const t = coalesceToDate(to);
55
+ if (!f || !t) return "";
56
+ if (isSameLocalCalendarDay(f, t)) {
57
+ return `${formatLocalDateTime(f)} – ${formatLocalTime(t)}`;
58
+ }
59
+ return `${formatLocalDateTime(f)} – ${formatLocalDateTime(t)}`;
60
+ }
61
+
62
+ /** Start/end strings for the details page (arrow between). */
63
+ export function formatEightHourRangeEndpoints(from, to) {
64
+ const f = coalesceToDate(from);
65
+ const t = coalesceToDate(to);
66
+ if (!f || !t) return null;
67
+ if (isSameLocalCalendarDay(f, t)) {
68
+ return { from: formatLocalDateTime(f), to: formatLocalTime(t) };
69
+ }
70
+ return { from: formatLocalDateTime(f), to: formatLocalDateTime(t) };
71
+ }
72
+
73
+ /** Rolling from/to for a preset chip (matches panel + details display). */
74
+ export function getPresetWindowDates(windowId) {
75
+ const ms = RANGE_TO_MS[windowId];
76
+ if (!ms) return null;
77
+ const to = new Date();
78
+ const from = new Date(to.getTime() - ms);
79
+ return { from, to };
80
+ }
81
+
82
+ /** Label for preset window chips (8h shows time range; others show dates). */
83
+ export function computeWindowLabel(windowId) {
84
+ const ms = RANGE_TO_MS[windowId];
85
+ if (!ms) return "";
86
+ const range = getPresetWindowDates(windowId);
87
+ if (!range) return "";
88
+ if (windowId === "8h") {
89
+ return formatEightHourWindowLabel(range.from, range.to);
90
+ }
91
+ return `${formatLocalDate(range.from)} – ${formatLocalDate(range.to)}`;
92
+ }
@@ -0,0 +1,52 @@
1
+ // Sparkline path builders shared by PerformancePanel + PerformanceDetailsPage.
2
+
3
+ export const SPARK_DEFAULT_LINE =
4
+ "M1.00049 13.5213C7.85975 11.7032 8.88149 4.73912 13.7838 4.73912C19.8344 4.73912 23.6237 17.166 29.1281 17.166C34.5998 17.166 37.4149 4.88121 43.4004 8.61051C47.9764 11.4616 50.6186 1.00024 50.6186 1.00024";
5
+
6
+ export const SPARK_DEFAULT_AREA =
7
+ "M1.00049 16.692C8.88149 15.6975 8.88149 7.79403 13.7838 7.79403C19.8344 7.79403 23.6237 21.4034 29.1281 21.4034C34.5998 21.4034 37.4149 8.93821 43.4004 12.7223C47.9764 15.6152 50.6186 5.00024 50.6186 5.00024V36.0002C50.6186 36.0002 10.8223 36.0002 1.00049 36.0002C1.00049 28.8653 1.00049 19.4871 1.00049 16.692Z";
8
+
9
+ /** Normalize host points — flat array of numbers or [x, y] pairs (y used). */
10
+ export function normalizeSparkPoints(points) {
11
+ if (!Array.isArray(points)) return null;
12
+ const numeric = points
13
+ .map((p) => {
14
+ if (Array.isArray(p)) return Number(p[1]);
15
+ if (p == null || p === "") return NaN;
16
+ return Number(p);
17
+ })
18
+ .filter((n) => Number.isFinite(n));
19
+ return numeric.length >= 2 ? numeric : null;
20
+ }
21
+
22
+ /** Project a numeric series onto an SVG viewBox. Returns null when unusable. */
23
+ export function buildSparkPaths(points, vbW = 52, vbH = 36) {
24
+ const numeric = normalizeSparkPoints(points);
25
+ if (!numeric) return null;
26
+ const min = Math.min(...numeric);
27
+ const max = Math.max(...numeric);
28
+ const padY = 4;
29
+ const span = max - min || 1;
30
+ const usableH = vbH - padY * 2 - 4;
31
+ const sx = (i) => (numeric.length === 1 ? 0 : (i / (numeric.length - 1)) * (vbW - 2)) + 1;
32
+ const sy = (v) => padY + (1 - (v - min) / span) * usableH;
33
+ let line = "";
34
+ for (let i = 0; i < numeric.length; i++) {
35
+ line += `${i === 0 ? "M" : "L"}${sx(i).toFixed(2)},${sy(numeric[i]).toFixed(2)}`;
36
+ }
37
+ const lastX = sx(numeric.length - 1);
38
+ const firstX = sx(0);
39
+ const area = `${line} L${lastX.toFixed(2)},${vbH} L${firstX.toFixed(2)},${vbH} Z`;
40
+ return { line, area };
41
+ }
42
+
43
+ /** Infer up/down from first vs last point when host omits `trend`. */
44
+ export function trendFromPoints(points) {
45
+ const numeric = normalizeSparkPoints(points);
46
+ if (!numeric) return null;
47
+ const first = numeric[0];
48
+ const last = numeric[numeric.length - 1];
49
+ if (last > first) return "up";
50
+ if (last < first) return "down";
51
+ return "up";
52
+ }
@@ -14,6 +14,7 @@ import {
14
14
  } from "lucide-react";
15
15
  import DataTable2 from "../data/DataTable2";
16
16
  import { DateRangeButton } from "../performance/PerformancePanel";
17
+ import { computeWindowLabel, formatLocalDate } from "../performance/performanceRangeFormat";
17
18
  import ConfirmationPopup from "../common/ConfirmationPopup";
18
19
 
19
20
  // Color values resolve to CSS variables declared in `src/tokens/colors.css`.
@@ -41,30 +42,6 @@ const DEFAULT_RANGE_OPTIONS = [
41
42
  { id: "30d", label: "30d" },
42
43
  ];
43
44
 
44
- const RANGE_TO_MS = {
45
- "8h": 8 * 60 * 60 * 1000,
46
- "1d": 24 * 60 * 60 * 1000,
47
- "7d": 7 * 24 * 60 * 60 * 1000,
48
- "30d": 30 * 24 * 60 * 60 * 1000,
49
- };
50
-
51
- function formatLocalDate(date) {
52
- if (!(date instanceof Date)) return String(date ?? "");
53
- return date.toLocaleDateString("en-US", {
54
- month: "2-digit",
55
- day: "2-digit",
56
- year: "numeric",
57
- });
58
- }
59
-
60
- function computeWindowLabel(windowId) {
61
- const ms = RANGE_TO_MS[windowId];
62
- if (!ms) return "";
63
- const to = new Date();
64
- const from = new Date(to.getTime() - ms);
65
- return `${formatLocalDate(from)} – ${formatLocalDate(to)}`;
66
- }
67
-
68
45
  function RangeChip({ label, active = false, onClick }) {
69
46
  return (
70
47
  <button
@@ -331,7 +308,7 @@ export default function ReportsList({
331
308
 
332
309
  const dateLabel = customRangeActive
333
310
  ? `${formatLocalDate(effectiveDateRange.from)} – ${formatLocalDate(effectiveDateRange.to)}`
334
- : dateRangeLabel || computeWindowLabel(selectedWindow);
311
+ : computeWindowLabel(selectedWindow) || dateRangeLabel;
335
312
 
336
313
  // Inject the actions column at the right edge of whatever column set the
337
314
  // host passed (or DEFAULT_REPORTS_COLUMNS). Built dynamically so the