@handled-ai/design-system 0.16.2 → 0.17.1
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.
- package/dist/components/contextual-quick-action-launcher.d.ts +32 -0
- package/dist/components/contextual-quick-action-launcher.js +202 -0
- package/dist/components/contextual-quick-action-launcher.js.map +1 -0
- package/dist/components/score-why-chips.d.ts +46 -0
- package/dist/components/score-why-chips.js +281 -0
- package/dist/components/score-why-chips.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +41 -1
- package/dist/prototype/prototype-inbox-view.d.ts +13 -3
- package/dist/prototype/prototype-inbox-view.js +41 -97
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +193 -0
- package/src/components/contextual-quick-action-launcher.tsx +231 -0
- package/src/components/score-why-chips.tsx +358 -0
- package/src/index.ts +2 -0
- package/src/prototype/__tests__/detail-view-score-why.test.tsx +326 -0
- package/src/prototype/__tests__/detail-view-title-subtext.test.tsx +72 -0
- package/src/prototype/prototype-config.ts +39 -0
- package/src/prototype/prototype-inbox-view.tsx +48 -105
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
5
|
+
import { DetailView, type DetailViewProps } from "../prototype-inbox-view";
|
|
6
|
+
import type { QueueItem, SignalScoreData } from "../prototype-config";
|
|
7
|
+
|
|
8
|
+
const baseItem: QueueItem = {
|
|
9
|
+
id: "case-1",
|
|
10
|
+
title: "Test Signal",
|
|
11
|
+
details: "Some details",
|
|
12
|
+
statusColor: "green",
|
|
13
|
+
time: "2h ago",
|
|
14
|
+
company: "Acme Inc",
|
|
15
|
+
tag1: "renewal",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function makeSignalScore(overrides: Partial<SignalScoreData> = {}): SignalScoreData {
|
|
19
|
+
return {
|
|
20
|
+
score: 82,
|
|
21
|
+
factors: [
|
|
22
|
+
{ key: "trigger", label: "Trigger strength", score: 70, why: "Strong signal" },
|
|
23
|
+
{ key: "fit", label: "Company fit", score: 55, why: "Good fit" },
|
|
24
|
+
],
|
|
25
|
+
whyNow: "Strong signals detected.",
|
|
26
|
+
evidence: ["Evidence line 1"],
|
|
27
|
+
confidence: 80,
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function baseProps(overrides: Partial<DetailViewProps> = {}): DetailViewProps {
|
|
33
|
+
return {
|
|
34
|
+
item: baseItem,
|
|
35
|
+
sections: { signalBrief: true, suggestedActions: false, timeline: false },
|
|
36
|
+
getSignalScore: () => makeSignalScore(),
|
|
37
|
+
buildSuggestedActions: () => [],
|
|
38
|
+
buildSourceItems: () => [],
|
|
39
|
+
accountContacts: [],
|
|
40
|
+
emailSignature: "",
|
|
41
|
+
iconMap: {},
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("DetailView corrected compact score WHY UX", () => {
|
|
47
|
+
it("renders priority in metadata row without the old signal score card", () => {
|
|
48
|
+
render(<DetailView {...baseProps()} />);
|
|
49
|
+
|
|
50
|
+
expect(screen.getByRole("button", { name: /urgent priority/i })).toBeInTheDocument();
|
|
51
|
+
expect(screen.queryByText("Signal score")).toBeNull();
|
|
52
|
+
expect(screen.queryByText("82")).toBeNull();
|
|
53
|
+
expect(screen.queryByText("/100")).toBeNull();
|
|
54
|
+
expect(screen.queryByText("Select a chip for details")).toBeNull();
|
|
55
|
+
expect(screen.queryByText("How's this score?")).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("uses provided priority label before fallback thresholds", () => {
|
|
59
|
+
render(
|
|
60
|
+
<DetailView
|
|
61
|
+
{...baseProps({
|
|
62
|
+
getSignalScore: () => makeSignalScore({ score: 92, urgencyLabel: "Medium" }),
|
|
63
|
+
})}
|
|
64
|
+
/>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(screen.getByRole("button", { name: /medium priority/i })).toBeInTheDocument();
|
|
68
|
+
expect(screen.queryByRole("button", { name: /urgent priority/i })).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("opens priority explanation from metadata independently of score feedback", () => {
|
|
72
|
+
render(
|
|
73
|
+
<DetailView
|
|
74
|
+
{...baseProps({
|
|
75
|
+
getSignalScore: () =>
|
|
76
|
+
makeSignalScore({ urgencyExplanation: "Customer activity spiked today." }),
|
|
77
|
+
})}
|
|
78
|
+
/>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
fireEvent.click(screen.getByRole("button", { name: /urgent priority/i }));
|
|
82
|
+
|
|
83
|
+
expect(screen.getByText("Customer activity spiked today.")).toBeInTheDocument();
|
|
84
|
+
expect(screen.getByText("Score 82/100")).toBeInTheDocument();
|
|
85
|
+
expect(screen.getByText("Urgent range: 80-100")).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByText("Why now")).toBeInTheDocument();
|
|
87
|
+
const priorityPanel = screen.getByRole("region", { name: /priority explanation/i });
|
|
88
|
+
expect(within(priorityPanel).getByText("Strong signals detected.")).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByText("Top factor")).toBeInTheDocument();
|
|
90
|
+
expect(screen.getAllByText("Strong signal").length).toBeGreaterThan(0);
|
|
91
|
+
expect(screen.getByText("Trigger strength")).toBeInTheDocument();
|
|
92
|
+
expect(screen.getByText("Company fit")).toBeInTheDocument();
|
|
93
|
+
expect(screen.queryByText("How's this score?")).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("keeps signal WHY chips collapsed by default and excludes factor-only buckets", () => {
|
|
97
|
+
render(
|
|
98
|
+
<DetailView
|
|
99
|
+
{...baseProps({
|
|
100
|
+
getSignalScore: () =>
|
|
101
|
+
makeSignalScore({
|
|
102
|
+
explanationBuckets: [
|
|
103
|
+
{ key: "signal-a", label: "Treasury activity", kind: "signal", primarySignalId: "sig-1", signals: [{ id: "sig-1", label: "Treasury signal" }] },
|
|
104
|
+
{ key: "factor-b", label: "Relationship depth", kind: "factor", rationale: "Relationship rationale", factorKeys: ["fit"] },
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
107
|
+
})}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(screen.getByText("Treasury activity")).toBeInTheDocument();
|
|
112
|
+
expect(screen.queryByText("Relationship depth")).toBeNull();
|
|
113
|
+
expect(screen.queryByRole("region", { name: /treasury activity details/i })).toBeNull();
|
|
114
|
+
|
|
115
|
+
fireEvent.click(screen.getByRole("button", { name: /treasury activity/i }));
|
|
116
|
+
expect(screen.getByRole("region", { name: /treasury activity details/i })).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("resets selected bucket and priority panel when item id changes", () => {
|
|
120
|
+
const { rerender } = render(<DetailView {...baseProps()} />);
|
|
121
|
+
|
|
122
|
+
fireEvent.click(screen.getByRole("button", { name: /urgent priority/i }));
|
|
123
|
+
expect(screen.getByRole("region", { name: /priority explanation/i })).toBeInTheDocument();
|
|
124
|
+
|
|
125
|
+
rerender(
|
|
126
|
+
<DetailView
|
|
127
|
+
{...baseProps({
|
|
128
|
+
item: { ...baseItem, id: "case-2", title: "Second Signal" },
|
|
129
|
+
})}
|
|
130
|
+
/>,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(screen.queryByRole("region", { name: /priority explanation/i })).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("does not render factor-derived WHY chips when no signal buckets are present", () => {
|
|
137
|
+
render(<DetailView {...baseProps()} />);
|
|
138
|
+
|
|
139
|
+
expect(screen.queryByRole("button", { name: /trigger strength/i })).toBeNull();
|
|
140
|
+
expect(screen.queryByRole("button", { name: /company fit/i })).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("honors an explicit empty explanationBuckets array", () => {
|
|
144
|
+
render(
|
|
145
|
+
<DetailView
|
|
146
|
+
{...baseProps({
|
|
147
|
+
getSignalScore: () => makeSignalScore({ explanationBuckets: [] }),
|
|
148
|
+
})}
|
|
149
|
+
/>,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(screen.queryByRole("button", { name: /trigger strength/i })).toBeNull();
|
|
153
|
+
expect(screen.queryByRole("button", { name: /company fit/i })).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("does not render a WHY row when no buckets or legacy factors exist", () => {
|
|
157
|
+
render(
|
|
158
|
+
<DetailView
|
|
159
|
+
{...baseProps({
|
|
160
|
+
getSignalScore: () => makeSignalScore({ factors: [], explanationBuckets: [] }),
|
|
161
|
+
})}
|
|
162
|
+
/>,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(screen.getByRole("button", { name: /urgent priority/i })).toBeInTheDocument();
|
|
166
|
+
expect(screen.queryByRole("button", { name: /trigger strength/i })).toBeNull();
|
|
167
|
+
expect(screen.queryByRole("button", { name: /company fit/i })).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("exposes aria-expanded and aria-controls for priority and bucket triggers", () => {
|
|
171
|
+
render(
|
|
172
|
+
<DetailView
|
|
173
|
+
{...baseProps({
|
|
174
|
+
getSignalScore: () =>
|
|
175
|
+
makeSignalScore({
|
|
176
|
+
explanationBuckets: [
|
|
177
|
+
{ key: "signal:a/test", label: "Signal A", kind: "signal", signals: [{ id: "sig-a", label: "Signal A event" }] },
|
|
178
|
+
],
|
|
179
|
+
}),
|
|
180
|
+
})}
|
|
181
|
+
/>,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const priorityButton = screen.getByRole("button", { name: /urgent priority/i });
|
|
185
|
+
expect(priorityButton.getAttribute("aria-expanded")).toBe("false");
|
|
186
|
+
expect(priorityButton.getAttribute("aria-controls")).toBeTruthy();
|
|
187
|
+
fireEvent.click(priorityButton);
|
|
188
|
+
expect(priorityButton.getAttribute("aria-expanded")).toBe("true");
|
|
189
|
+
expect(document.getElementById(priorityButton.getAttribute("aria-controls")!)).toBeInTheDocument();
|
|
190
|
+
|
|
191
|
+
const bucketButton = screen.getByRole("button", { name: /signal a/i });
|
|
192
|
+
expect(bucketButton.getAttribute("aria-expanded")).toBe("false");
|
|
193
|
+
expect(bucketButton.getAttribute("aria-controls")).toBeTruthy();
|
|
194
|
+
fireEvent.click(bucketButton);
|
|
195
|
+
expect(bucketButton.getAttribute("aria-expanded")).toBe("true");
|
|
196
|
+
expect(document.getElementById(bucketButton.getAttribute("aria-controls")!)).toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("renders repeated signal groups as one chip with a count and signal-id rows", () => {
|
|
200
|
+
const onOpenSignalBucket = vi.fn();
|
|
201
|
+
render(
|
|
202
|
+
<DetailView
|
|
203
|
+
{...baseProps({
|
|
204
|
+
getSignalScore: () =>
|
|
205
|
+
makeSignalScore({
|
|
206
|
+
explanationBuckets: [
|
|
207
|
+
{ key: "signal-repeat", label: "Treasury activity", kind: "signal", signalCount: 3, signalIds: ["sig-1", "sig-2", "sig-3"] },
|
|
208
|
+
],
|
|
209
|
+
}),
|
|
210
|
+
onOpenSignalBucket,
|
|
211
|
+
})}
|
|
212
|
+
/>,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(screen.getAllByRole("button", { name: /treasury activity/i })).toHaveLength(1);
|
|
216
|
+
expect(screen.getByText("×3")).toBeInTheDocument();
|
|
217
|
+
|
|
218
|
+
fireEvent.click(screen.getByRole("button", { name: /treasury activity/i }));
|
|
219
|
+
const matchingSignals = screen.getByRole("list", { name: /matching signals/i });
|
|
220
|
+
expect(within(matchingSignals).getAllByRole("button", { name: /treasury activity signal/i })).toHaveLength(3);
|
|
221
|
+
fireEvent.click(within(matchingSignals).getAllByRole("button", { name: /treasury activity signal/i })[1]);
|
|
222
|
+
expect(onOpenSignalBucket).toHaveBeenCalledWith({
|
|
223
|
+
item: baseItem,
|
|
224
|
+
bucketKey: "signal-repeat",
|
|
225
|
+
signalId: "sig-2",
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("renders designed matching signal rows from bucket signals", () => {
|
|
230
|
+
render(
|
|
231
|
+
<DetailView
|
|
232
|
+
{...baseProps({
|
|
233
|
+
getSignalScore: () =>
|
|
234
|
+
makeSignalScore({
|
|
235
|
+
explanationBuckets: [
|
|
236
|
+
{
|
|
237
|
+
key: "signals",
|
|
238
|
+
label: "Treasury activity",
|
|
239
|
+
kind: "signal",
|
|
240
|
+
signals: [
|
|
241
|
+
{
|
|
242
|
+
id: "sig-1",
|
|
243
|
+
label: "ACH volume increased",
|
|
244
|
+
description: "Payment activity is 40% above baseline.",
|
|
245
|
+
source: "Banking data",
|
|
246
|
+
time: "Today",
|
|
247
|
+
metric: "+40%",
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
}),
|
|
253
|
+
})}
|
|
254
|
+
/>,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
fireEvent.click(screen.getByRole("button", { name: /treasury activity/i }));
|
|
258
|
+
const matchingSignals = screen.getByRole("list", { name: /matching signals/i });
|
|
259
|
+
expect(within(matchingSignals).getByText("ACH volume increased")).toBeInTheDocument();
|
|
260
|
+
expect(within(matchingSignals).getByText("Payment activity is 40% above baseline.")).toBeInTheDocument();
|
|
261
|
+
expect(within(matchingSignals).getByText("Banking data")).toBeInTheDocument();
|
|
262
|
+
expect(within(matchingSignals).getByText("+40%")).toBeInTheDocument();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("opens a matching signal row with the selected signal id", () => {
|
|
266
|
+
const onOpenSignalBucket = vi.fn();
|
|
267
|
+
render(
|
|
268
|
+
<DetailView
|
|
269
|
+
{...baseProps({
|
|
270
|
+
getSignalScore: () =>
|
|
271
|
+
makeSignalScore({
|
|
272
|
+
explanationBuckets: [
|
|
273
|
+
{
|
|
274
|
+
key: "with-signals",
|
|
275
|
+
label: "With signals",
|
|
276
|
+
kind: "signal",
|
|
277
|
+
signalCount: 2,
|
|
278
|
+
signals: [
|
|
279
|
+
{ id: "sig-1", label: "Latest signal", description: "Newest signal" },
|
|
280
|
+
{ id: "sig-2", label: "Older signal", description: "Older signal" },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
}),
|
|
285
|
+
onOpenSignalBucket,
|
|
286
|
+
})}
|
|
287
|
+
/>,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
fireEvent.click(screen.getByRole("button", { name: /with signals/i }));
|
|
291
|
+
fireEvent.click(screen.getByRole("button", { name: /older signal/i }));
|
|
292
|
+
|
|
293
|
+
expect(onOpenSignalBucket).toHaveBeenCalledWith({
|
|
294
|
+
item: baseItem,
|
|
295
|
+
bucketKey: "with-signals",
|
|
296
|
+
signalId: "sig-2",
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("shows factor feedback in the priority panel, not signal WHY panels", () => {
|
|
301
|
+
const onFactorFeedback = vi.fn();
|
|
302
|
+
render(
|
|
303
|
+
<DetailView
|
|
304
|
+
{...baseProps({
|
|
305
|
+
getSignalScore: () =>
|
|
306
|
+
makeSignalScore({
|
|
307
|
+
onFactorFeedback,
|
|
308
|
+
initialFactorFeedback: { fit: { type: "down", detail: "Needs review" } },
|
|
309
|
+
explanationBuckets: [
|
|
310
|
+
{ key: "signal-a", label: "Signal only", kind: "signal", primarySignalId: "sig-1", signals: [{ id: "sig-1", label: "Signal event" }] },
|
|
311
|
+
],
|
|
312
|
+
}),
|
|
313
|
+
})}
|
|
314
|
+
/>,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
fireEvent.click(screen.getByRole("button", { name: /signal only/i }));
|
|
318
|
+
const signalPanel = screen.getByRole("region", { name: /signal only details/i });
|
|
319
|
+
expect(within(signalPanel).queryByTitle("This factor is accurate")).toBeNull();
|
|
320
|
+
|
|
321
|
+
fireEvent.click(screen.getByRole("button", { name: /urgent priority/i }));
|
|
322
|
+
const priorityPanel = screen.getByRole("region", { name: /priority explanation/i });
|
|
323
|
+
expect(within(priorityPanel).getAllByTitle("This factor is accurate").length).toBeGreaterThan(0);
|
|
324
|
+
expect(within(priorityPanel).getByText("Needs review")).toBeInTheDocument();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import { DetailView, type DetailViewProps } from "../prototype-inbox-view";
|
|
5
|
+
import type { QueueItem, SignalScoreData } from "../prototype-config";
|
|
6
|
+
|
|
7
|
+
const baseItem: QueueItem = {
|
|
8
|
+
id: "case-1",
|
|
9
|
+
title: "QA Placement Churn Risk Signal",
|
|
10
|
+
details: "Some details",
|
|
11
|
+
statusColor: "red",
|
|
12
|
+
time: "2h ago",
|
|
13
|
+
company: "QA Placement Account",
|
|
14
|
+
tag1: "Urgent Priority",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function signalScore(): SignalScoreData {
|
|
18
|
+
return {
|
|
19
|
+
score: 82,
|
|
20
|
+
factors: [],
|
|
21
|
+
whyNow: "Strong signal detected.",
|
|
22
|
+
evidence: [],
|
|
23
|
+
confidence: 75,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function baseProps(overrides: Partial<DetailViewProps> = {}): DetailViewProps {
|
|
28
|
+
return {
|
|
29
|
+
item: baseItem,
|
|
30
|
+
sections: { signalBrief: true, suggestedActions: false, timeline: false },
|
|
31
|
+
getSignalScore: () => signalScore(),
|
|
32
|
+
buildSuggestedActions: () => [],
|
|
33
|
+
buildSourceItems: () => [],
|
|
34
|
+
accountContacts: [],
|
|
35
|
+
emailSignature: "",
|
|
36
|
+
iconMap: {},
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("DetailView title slots", () => {
|
|
42
|
+
it("renders supporting title text below the main title", () => {
|
|
43
|
+
render(
|
|
44
|
+
<DetailView
|
|
45
|
+
{...baseProps({
|
|
46
|
+
renderTitleSubtext: (item) => (
|
|
47
|
+
<p data-testid="title-subtext">Full case: {item.title}</p>
|
|
48
|
+
),
|
|
49
|
+
})}
|
|
50
|
+
/>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(screen.getByRole("heading", { name: "QA Placement Churn Risk Signal" })).toBeTruthy();
|
|
54
|
+
expect(screen.getByTestId("title-subtext").textContent).toBe(
|
|
55
|
+
"Full case: QA Placement Churn Risk Signal",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders title extra content beside the main title", () => {
|
|
60
|
+
render(
|
|
61
|
+
<DetailView
|
|
62
|
+
{...baseProps({
|
|
63
|
+
renderTitleExtra: () => (
|
|
64
|
+
<button type="button" data-testid="title-extra">Quick action</button>
|
|
65
|
+
),
|
|
66
|
+
})}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(screen.getByTestId("title-extra").textContent).toBe("Quick action");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -36,16 +36,49 @@ export interface QueueItem {
|
|
|
36
36
|
tag1: string
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export type SignalScoreUrgencyLabel = "Low" | "Medium" | "High" | "Urgent"
|
|
40
|
+
|
|
41
|
+
export interface SignalScoreExplanationSignal {
|
|
42
|
+
id?: string
|
|
43
|
+
label: string
|
|
44
|
+
description?: string
|
|
45
|
+
source?: string
|
|
46
|
+
time?: string
|
|
47
|
+
metric?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SignalScoreExplanationBucket {
|
|
51
|
+
key: string
|
|
52
|
+
label: string
|
|
53
|
+
kind: "signal" | "factor" | "merged"
|
|
54
|
+
score?: number
|
|
55
|
+
classification?: string
|
|
56
|
+
rationale?: string
|
|
57
|
+
evidence?: string[]
|
|
58
|
+
signals?: SignalScoreExplanationSignal[]
|
|
59
|
+
primaryMetricLabel?: string
|
|
60
|
+
primaryMetricValue?: string
|
|
61
|
+
signalCount?: number
|
|
62
|
+
signalIds?: string[]
|
|
63
|
+
primarySignalId?: string
|
|
64
|
+
factorKeys?: string[]
|
|
65
|
+
}
|
|
66
|
+
|
|
39
67
|
export interface SignalScoreData {
|
|
40
68
|
score: number
|
|
41
69
|
factors: ScoreFactor[]
|
|
42
70
|
whyNow: string
|
|
43
71
|
evidence: string[]
|
|
44
72
|
confidence: number
|
|
73
|
+
urgencyLabel?: SignalScoreUrgencyLabel
|
|
74
|
+
urgencyExplanation?: string
|
|
75
|
+
explanationBuckets?: SignalScoreExplanationBucket[]
|
|
45
76
|
onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
|
|
77
|
+
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
46
78
|
onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
|
|
47
79
|
onApproveFeedback?: (reasons: string[], detail: string) => void
|
|
48
80
|
onDismissFeedback?: (reasons: string[], detail: string, subReason?: string) => void
|
|
81
|
+
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
49
82
|
initialScoreFeedback?: { type: "up" | "down"; pills: string[]; detail: string } | null
|
|
50
83
|
initialFactorFeedback?: Record<string, { type: "up" | "down"; detail: string }>
|
|
51
84
|
/** AI-generated signal brief text. When present, rendered in a dedicated section. */
|
|
@@ -89,7 +122,9 @@ export interface InboxViewConfig {
|
|
|
89
122
|
hideToolbarActions?: boolean
|
|
90
123
|
hideHoverActions?: boolean
|
|
91
124
|
onSuggestedActionFeedback?: (actionId: number | string, feedback: string, actionTitle?: string) => void
|
|
125
|
+
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
92
126
|
onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
|
|
127
|
+
onOpenSignalBucket?: (args: { item: QueueItem; bucketKey: string; signalId: string }) => void
|
|
93
128
|
buildEntityChips?: (item: QueueItem) => Array<{ id: string; label: string; avatarLetter: string; onClick?: () => void }>
|
|
94
129
|
quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
|
|
95
130
|
hideAccountsButton?: boolean
|
|
@@ -131,6 +166,10 @@ export interface InboxViewConfig {
|
|
|
131
166
|
briefStyleVariant?: BriefStyleVariant
|
|
132
167
|
/** Render extra content at the end of the detail view, below the suggested actions section. */
|
|
133
168
|
renderDetailExtra?: (item: QueueItem) => React.ReactNode
|
|
169
|
+
/** Render extra content inline with the detail title. */
|
|
170
|
+
renderTitleExtra?: (item: QueueItem) => React.ReactNode
|
|
171
|
+
/** Render supporting content below the detail title. */
|
|
172
|
+
renderTitleSubtext?: (item: QueueItem) => React.ReactNode
|
|
134
173
|
/** Render content between the signal brief text and the signal score bar (e.g. "Signals on Case" chips). */
|
|
135
174
|
renderBeforeScore?: (item: QueueItem) => React.ReactNode
|
|
136
175
|
/** Render content between the signal score section and the activity timeline (e.g. OpportunityPanel). */
|