@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.
@@ -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 { ScoreFeedback } from "../components/score-feedback"
42
- import { ScoreBreakdown } from "../components/score-breakdown"
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
- onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
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
- onScoreFeedback,
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.company, 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
- {(item.statusColor === "red" || item.statusColor === "orange") && (
256
- <div className={`inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-semibold ${
257
- item.statusColor === "red"
258
- ? "bg-red-50 text-red-700"
259
- : "bg-orange-50 text-orange-700"
260
- }`}>
261
- <span className="text-[10px] font-bold">!</span> {item.tag1.charAt(0).toUpperCase() + item.tag1.slice(1)}
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&apos;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
- <ScoreFeedback.Root
359
- onSubmitFeedback={(type, pills, detail) => (signalData.onScoreFeedback ?? onScoreFeedback)?.(type, pills, detail)}
360
- initialFeedback={signalData.initialScoreFeedback}
361
- >
362
- <div className="mb-5 rounded-md border border-border bg-muted/20 p-3">
363
- <div className="flex items-center justify-between mb-1.5">
364
- <span className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Signal Score</span>
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
- onScoreFeedback,
681
+ onOpenSignalBucket,
755
682
  }
756
683
 
757
684
  return (