@handled-ai/design-system 0.16.2 → 0.17.0
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/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- 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/components/tabs.d.ts +1 -1
- 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 +37 -1
- package/dist/prototype/prototype-inbox-view.d.ts +9 -3
- package/dist/prototype/prototype-inbox-view.js +28 -96
- 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/prototype-config.ts +35 -0
- package/src/prototype/prototype-inbox-view.tsx +31 -104
|
@@ -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
|
+
});
|
|
@@ -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
|
|
@@ -38,9 +38,8 @@ import {
|
|
|
38
38
|
} from "../components/inbox-toolbar"
|
|
39
39
|
import { GroupedListView, type GroupedListGroup } from "../components/item-list"
|
|
40
40
|
import { SignalApproval, type ApprovalState, type OpportunityPreview } from "../components/signal-feedback-inline"
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import { Citation, type SourceDef } from "../components/detail-view"
|
|
41
|
+
import { ScoreWhyChips, SignalPriorityChip, SignalPriorityPanel } from "../components/score-why-chips"
|
|
42
|
+
import { type SourceDef } from "../components/detail-view"
|
|
44
43
|
import {
|
|
45
44
|
SuggestedActions,
|
|
46
45
|
type SuggestedAction,
|
|
@@ -124,6 +123,8 @@ export interface DetailViewProps {
|
|
|
124
123
|
onOpenEntityPanel?: () => void
|
|
125
124
|
onOpenRecentActivity?: () => void
|
|
126
125
|
onSuggestedActionFeedback?: (actionId: number | string, feedback: string, actionTitle?: string) => void
|
|
126
|
+
/** @deprecated The compact score UX no longer renders score-level thumbs by default. */
|
|
127
|
+
onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
|
|
127
128
|
onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
|
|
128
129
|
getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
|
|
129
130
|
signalLabels?: InboxViewConfig["signalLabels"]
|
|
@@ -138,7 +139,7 @@ export interface DetailViewProps {
|
|
|
138
139
|
lastActivityTime?: string
|
|
139
140
|
/** Render extra metadata chips (e.g. assignee) inside the chips row below the title. */
|
|
140
141
|
renderMetadataExtra?: (item: QueueItem) => React.ReactNode
|
|
141
|
-
|
|
142
|
+
onOpenSignalBucket?: (args: { item: QueueItem; bucketKey: string; signalId: string }) => void
|
|
142
143
|
approveButtonIconUrl?: string
|
|
143
144
|
opportunityPreview?: OpportunityPreview
|
|
144
145
|
onRequestApproval?: () => Promise<void>
|
|
@@ -151,7 +152,7 @@ export function DetailView({
|
|
|
151
152
|
sections,
|
|
152
153
|
getSignalScore,
|
|
153
154
|
buildSuggestedActions,
|
|
154
|
-
buildSourceItems,
|
|
155
|
+
buildSourceItems: _buildSourceItems,
|
|
155
156
|
getTimelineEvents,
|
|
156
157
|
accountContacts,
|
|
157
158
|
emailSignature,
|
|
@@ -159,6 +160,7 @@ export function DetailView({
|
|
|
159
160
|
onOpenEntityPanel,
|
|
160
161
|
onOpenRecentActivity,
|
|
161
162
|
onSuggestedActionFeedback: _onSuggestedActionFeedback,
|
|
163
|
+
onScoreFeedback: _onScoreFeedback,
|
|
162
164
|
onSignalApprove,
|
|
163
165
|
getSignalApprovalState,
|
|
164
166
|
signalLabels,
|
|
@@ -170,32 +172,32 @@ export function DetailView({
|
|
|
170
172
|
renderAfterScore,
|
|
171
173
|
lastActivityTime,
|
|
172
174
|
renderMetadataExtra,
|
|
173
|
-
|
|
175
|
+
onOpenSignalBucket,
|
|
174
176
|
approveButtonIconUrl,
|
|
175
177
|
opportunityPreview,
|
|
176
178
|
onRequestApproval,
|
|
177
179
|
attentionCount,
|
|
178
180
|
}: DetailViewProps) {
|
|
179
|
-
const [evidenceExpanded, setEvidenceExpanded] = React.useState(false)
|
|
180
181
|
const [showTimeline, setShowTimeline] = React.useState(false)
|
|
181
182
|
const [extraActions, setExtraActions] = React.useState<SuggestedAction[]>([])
|
|
183
|
+
const [priorityOpen, setPriorityOpen] = React.useState(false)
|
|
184
|
+
const priorityPanelId = React.useId()
|
|
182
185
|
|
|
183
186
|
React.useEffect(() => {
|
|
184
187
|
setShowTimeline(false)
|
|
185
|
-
setEvidenceExpanded(false)
|
|
186
188
|
setExtraActions([])
|
|
189
|
+
setPriorityOpen(false)
|
|
187
190
|
}, [item.id])
|
|
188
191
|
|
|
189
192
|
const signalData = React.useMemo(
|
|
190
193
|
() => getSignalScore(item.company, item),
|
|
191
|
-
[getSignalScore, item
|
|
194
|
+
[getSignalScore, item],
|
|
192
195
|
)
|
|
193
196
|
|
|
194
197
|
const suggestedActions = React.useMemo(
|
|
195
198
|
() => [...buildSuggestedActions(item), ...extraActions],
|
|
196
199
|
[buildSuggestedActions, item, extraActions],
|
|
197
200
|
)
|
|
198
|
-
const sourceItems = React.useMemo(() => buildSourceItems(item), [buildSourceItems, item])
|
|
199
201
|
const timelineEvents = React.useMemo(
|
|
200
202
|
() => getTimelineEvents?.(item) ?? [],
|
|
201
203
|
[getTimelineEvents, item],
|
|
@@ -252,18 +254,13 @@ export function DetailView({
|
|
|
252
254
|
<h1 className="mb-3 text-2xl font-bold tracking-tight text-foreground">{item.title}</h1>
|
|
253
255
|
|
|
254
256
|
<div className="mb-6 flex flex-wrap items-center gap-2">
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
</div>
|
|
263
|
-
)}
|
|
264
|
-
<div className="inline-flex items-center gap-1 rounded-md bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
|
|
265
|
-
{item.tag1}
|
|
266
|
-
</div>
|
|
257
|
+
<SignalPriorityChip
|
|
258
|
+
score={signalData.score}
|
|
259
|
+
urgencyLabel={signalData.urgencyLabel}
|
|
260
|
+
isOpen={priorityOpen}
|
|
261
|
+
controlsId={priorityPanelId}
|
|
262
|
+
onClick={() => setPriorityOpen((prev) => !prev)}
|
|
263
|
+
/>
|
|
267
264
|
{signalData.timeChipLabel && (
|
|
268
265
|
<Badge variant="outline" title={signalData.timeChipDetail ?? undefined}>
|
|
269
266
|
{signalData.timeChipLabel}
|
|
@@ -283,37 +280,10 @@ export function DetailView({
|
|
|
283
280
|
{renderMetadataExtra?.(item)}
|
|
284
281
|
</div>
|
|
285
282
|
|
|
283
|
+
{priorityOpen && <SignalPriorityPanel id={priorityPanelId} signalData={signalData} className="mb-6" />}
|
|
284
|
+
|
|
286
285
|
{/* Signal Brief */}
|
|
287
286
|
{sections.signalBrief && (() => {
|
|
288
|
-
const pct = signalData.score
|
|
289
|
-
const scoreColor = pct >= 70 ? "text-emerald-600" : pct >= 40 ? "text-amber-600" : "text-red-600"
|
|
290
|
-
const barColor = pct >= 70 ? "bg-emerald-500" : pct >= 40 ? "bg-amber-500" : "bg-red-500"
|
|
291
|
-
const scoreLabel = pct >= 70 ? "HIGH" : pct >= 40 ? "MEDIUM" : "LOW"
|
|
292
|
-
|
|
293
|
-
const evidenceWithCitations: React.ReactNode[] =
|
|
294
|
-
sourceItems.length >= 4
|
|
295
|
-
? [
|
|
296
|
-
<>
|
|
297
|
-
There are <span className="font-medium text-foreground">3 unusual signals</span> including a large balance
|
|
298
|
-
outflow and reduced login frequency
|
|
299
|
-
<Citation number={1} source={sourceItems[0]} />
|
|
300
|
-
<Citation number={2} source={sourceItems[1]} />
|
|
301
|
-
<Citation number={3} source={sourceItems[2]} />
|
|
302
|
-
</>,
|
|
303
|
-
<>
|
|
304
|
-
Scott mentioned in <span className="font-medium text-foreground">#treasury-questions</span> that they are actively
|
|
305
|
-
looking for treasury management options
|
|
306
|
-
<Citation number={4} source={sourceItems[2]} />
|
|
307
|
-
</>,
|
|
308
|
-
<>
|
|
309
|
-
You have a recent email thread regarding optimization options that hasn't been replied to
|
|
310
|
-
<Citation number={5} source={sourceItems[3]} />
|
|
311
|
-
</>,
|
|
312
|
-
]
|
|
313
|
-
: signalData.evidence.map((ev, i) => (
|
|
314
|
-
<span key={i}>{ev}</span>
|
|
315
|
-
))
|
|
316
|
-
|
|
317
287
|
const briefHeading = signalBriefCopy?.heading ?? "Signal brief"
|
|
318
288
|
const introOpt = signalBriefCopy?.intro
|
|
319
289
|
const briefIntro =
|
|
@@ -355,59 +325,14 @@ export function DetailView({
|
|
|
355
325
|
{/* Before-score content slot (e.g. "Signals on Case" chips) */}
|
|
356
326
|
{renderBeforeScore?.(item)}
|
|
357
327
|
|
|
358
|
-
<
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
<div className="flex items-center gap-2">
|
|
366
|
-
<span className="text-sm font-bold text-foreground">{signalData.score}/100</span>
|
|
367
|
-
<span className={`text-[10px] font-bold uppercase ${scoreColor}`}>{scoreLabel}</span>
|
|
368
|
-
<ScoreFeedback.Trigger />
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
371
|
-
<ScoreFeedback.Panel />
|
|
372
|
-
<div className="h-1.5 bg-muted rounded-full overflow-hidden mb-2">
|
|
373
|
-
<div
|
|
374
|
-
className={`h-full rounded-full transition-all duration-500 ${barColor}`}
|
|
375
|
-
style={{ width: `${signalData.score}%` }}
|
|
376
|
-
/>
|
|
377
|
-
</div>
|
|
378
|
-
<button
|
|
379
|
-
type="button"
|
|
380
|
-
onClick={() => setEvidenceExpanded((prev) => !prev)}
|
|
381
|
-
className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground hover:text-foreground transition-colors"
|
|
382
|
-
>
|
|
383
|
-
<ChevronDown className={`h-3 w-3 transition-transform duration-200 ${evidenceExpanded ? "rotate-180" : ""}`} />
|
|
384
|
-
View more
|
|
385
|
-
</button>
|
|
386
|
-
|
|
387
|
-
{evidenceExpanded && (
|
|
388
|
-
<div className="mt-3 space-y-3">
|
|
389
|
-
<ul className="space-y-2">
|
|
390
|
-
{evidenceWithCitations.map((ev, index) => (
|
|
391
|
-
<li key={index} className="flex items-start gap-2 text-sm">
|
|
392
|
-
<div className="w-1.5 h-1.5 bg-primary rounded-full mt-2 flex-shrink-0" />
|
|
393
|
-
<span className="text-muted-foreground leading-relaxed">{ev}</span>
|
|
394
|
-
</li>
|
|
395
|
-
))}
|
|
396
|
-
</ul>
|
|
397
|
-
<ScoreBreakdown
|
|
398
|
-
factors={signalData.factors}
|
|
399
|
-
onFactorFeedback={signalData.onFactorFeedback ?? ((key, type, detail) =>
|
|
400
|
-
console.log("Signal factor feedback:", { company: item.company, factor: key, type, detail })
|
|
401
|
-
)}
|
|
402
|
-
initialFeedback={signalData.initialFactorFeedback}
|
|
403
|
-
/>
|
|
404
|
-
<SignalApproval.Actions />
|
|
405
|
-
</div>
|
|
406
|
-
)}
|
|
328
|
+
<ScoreWhyChips
|
|
329
|
+
item={item}
|
|
330
|
+
signalData={signalData}
|
|
331
|
+
onOpenSignalBucket={onOpenSignalBucket}
|
|
332
|
+
/>
|
|
333
|
+
<div className="mt-4">
|
|
334
|
+
<SignalApproval.Actions />
|
|
407
335
|
</div>
|
|
408
|
-
</ScoreFeedback.Root>
|
|
409
|
-
|
|
410
|
-
{!evidenceExpanded && <SignalApproval.Actions />}
|
|
411
336
|
</div>
|
|
412
337
|
)
|
|
413
338
|
})()}
|
|
@@ -495,6 +420,7 @@ export function PrototypeInboxView({
|
|
|
495
420
|
hideHoverActions,
|
|
496
421
|
onSuggestedActionFeedback,
|
|
497
422
|
onScoreFeedback,
|
|
423
|
+
onOpenSignalBucket,
|
|
498
424
|
headerActions,
|
|
499
425
|
onOpenEntityPanel,
|
|
500
426
|
onOpenRecentActivity,
|
|
@@ -741,6 +667,7 @@ export function PrototypeInboxView({
|
|
|
741
667
|
onOpenEntityPanel,
|
|
742
668
|
onOpenRecentActivity,
|
|
743
669
|
onSuggestedActionFeedback,
|
|
670
|
+
onScoreFeedback,
|
|
744
671
|
onSignalApprove,
|
|
745
672
|
getSignalApprovalState,
|
|
746
673
|
signalLabels,
|
|
@@ -751,7 +678,7 @@ export function PrototypeInboxView({
|
|
|
751
678
|
renderBeforeScore,
|
|
752
679
|
renderAfterScore,
|
|
753
680
|
lastActivityTime,
|
|
754
|
-
|
|
681
|
+
onOpenSignalBucket,
|
|
755
682
|
}
|
|
756
683
|
|
|
757
684
|
return (
|