@handled-ai/design-system 0.9.20 → 0.9.21

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.
@@ -3,13 +3,65 @@
3
3
  import * as React from "react"
4
4
  import { Check, CirclePlus, ExternalLink, Lock, ThumbsDown } from "lucide-react"
5
5
 
6
- const dismissReasons = [
7
- "Bad timing",
8
- "Inaccurate data",
9
- "Wrong account",
10
- "Already handled",
11
- "Not actionable",
12
- "Other",
6
+ interface DismissReasonNode {
7
+ label: string
8
+ subOptions?: string[]
9
+ }
10
+
11
+ const dismissReasonTree: DismissReasonNode[] = [
12
+ {
13
+ label: "Not relevant for this account",
14
+ subOptions: [
15
+ "Business as usual for this account",
16
+ "Account in maintenance mode",
17
+ "Wrong contact for this signal",
18
+ "Other",
19
+ ],
20
+ },
21
+ {
22
+ label: "Bad timing",
23
+ subOptions: [
24
+ "Too early in the relationship",
25
+ "Too soon after last outreach",
26
+ "Wrong time of year for this account",
27
+ "Other",
28
+ ],
29
+ },
30
+ {
31
+ label: "Inaccurate data",
32
+ subOptions: [
33
+ "Wrong amount or number",
34
+ "Stale data",
35
+ "Account info wrong",
36
+ "Other",
37
+ ],
38
+ },
39
+ {
40
+ label: "Wrong account",
41
+ subOptions: [
42
+ "Different account meant",
43
+ "Account not in scope",
44
+ "Other",
45
+ ],
46
+ },
47
+ {
48
+ label: "Already handled",
49
+ subOptions: [
50
+ "Already in conversation",
51
+ "Already an open Opportunity",
52
+ "Already escalated",
53
+ "Other",
54
+ ],
55
+ },
56
+ {
57
+ label: "Not actionable",
58
+ subOptions: [
59
+ "No clear next step",
60
+ "Outside our remit",
61
+ "Other",
62
+ ],
63
+ },
64
+ { label: "Other" },
13
65
  ]
14
66
 
15
67
  const approveReasons = [
@@ -53,7 +105,7 @@ interface SignalApprovalContextValue {
53
105
  approve: () => void
54
106
  submitApproveFeedback: (reasons: string[], detail: string) => void
55
107
  skipApproveFeedback: () => void
56
- dismiss: (reasons: string[], detail: string) => void
108
+ dismiss: (reasons: string[], detail: string, subReason?: string) => void
57
109
  requestApproval: () => void
58
110
  requestDismiss: () => void
59
111
  cancel: () => void
@@ -78,7 +130,7 @@ interface RootProps {
78
130
  hideApproveButton?: boolean
79
131
  onApprove?: () => void
80
132
  onApproveFeedback?: (reasons: string[], detail: string) => void
81
- onDismiss?: (reasons: string[], detail: string) => void
133
+ onDismiss?: (reasons: string[], detail: string, subReason?: string) => void
82
134
  }
83
135
 
84
136
  function Root({ children, companyName, opportunityUrl, scheduledTime, initialApprovalState, labels: labelOverrides, hideApproveButton, onApprove, onApproveFeedback, onDismiss }: RootProps) {
@@ -115,9 +167,9 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
115
167
  }, [])
116
168
 
117
169
  const dismiss = React.useCallback(
118
- (reasons: string[], detail: string) => {
170
+ (reasons: string[], detail: string, subReason?: string) => {
119
171
  setApprovalState("dismissed")
120
- onDismiss?.(reasons, detail)
172
+ onDismiss?.(reasons, detail, subReason)
121
173
  },
122
174
  [onDismiss]
123
175
  )
@@ -134,11 +186,13 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
134
186
  function SubmittedFeedback({
135
187
  reasons,
136
188
  detail,
189
+ subReason,
137
190
  variant,
138
191
  onEdit,
139
192
  }: {
140
193
  reasons: string[]
141
194
  detail: string
195
+ subReason?: string
142
196
  variant: "approve" | "dismiss"
143
197
  onEdit: () => void
144
198
  }) {
@@ -164,6 +218,13 @@ function SubmittedFeedback({
164
218
  {r}
165
219
  </span>
166
220
  ))}
221
+ {subReason && (
222
+ <span
223
+ className={`rounded-full border px-2 py-0.5 text-[10px] font-medium transition-colors group-hover:opacity-80 ${pillClass}`}
224
+ >
225
+ {subReason}
226
+ </span>
227
+ )}
167
228
  </div>
168
229
  )}
169
230
  {detail && (
@@ -176,15 +237,41 @@ function SubmittedFeedback({
176
237
  function Actions() {
177
238
  const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
178
239
  useSignalApproval()
240
+ const [selectedTopReason, setSelectedTopReason] = React.useState<string | null>(null)
241
+ const [selectedSubReason, setSelectedSubReason] = React.useState<string | null>(null)
179
242
  const [selectedReasons, setSelectedReasons] = React.useState<string[]>([])
180
243
  const [detailText, setDetailText] = React.useState("")
181
- const [submittedFeedback, setSubmittedFeedback] = React.useState<{ reasons: string[]; detail: string } | null>(null)
244
+ const [submittedFeedback, setSubmittedFeedback] = React.useState<{ reasons: string[]; detail: string; subReason?: string } | null>(null)
182
245
  const [isEditing, setIsEditing] = React.useState(false)
183
246
 
184
- const otherSelected = selectedReasons.includes("Other")
185
- const canSubmitDismiss = selectedReasons.length > 0 && (!otherSelected || detailText.trim().length > 0)
247
+ const topNode = dismissReasonTree.find((n) => n.label === selectedTopReason)
248
+ const hasSubOptions = !!(topNode?.subOptions && topNode.subOptions.length > 0)
249
+ const isTopOther = selectedTopReason === "Other" && !hasSubOptions
250
+ const isSubOther = selectedSubReason === "Other"
251
+ const needsText = isTopOther || isSubOther
252
+ const canSubmitDismiss =
253
+ selectedTopReason !== null &&
254
+ (!hasSubOptions || selectedSubReason !== null) &&
255
+ (!needsText || detailText.trim().length > 0)
256
+
186
257
  const canSubmitApprove = selectedReasons.length > 0 || detailText.trim().length > 0
187
258
 
259
+ const selectTopReason = (label: string) => {
260
+ if (selectedTopReason === label) {
261
+ setSelectedTopReason(null)
262
+ setSelectedSubReason(null)
263
+ setDetailText("")
264
+ } else {
265
+ setSelectedTopReason(label)
266
+ setSelectedSubReason(null)
267
+ setDetailText("")
268
+ }
269
+ }
270
+
271
+ const selectSubReason = (label: string) => {
272
+ setSelectedSubReason(selectedSubReason === label ? null : label)
273
+ }
274
+
188
275
  const toggleReason = (reason: string) => {
189
276
  setSelectedReasons((prev) =>
190
277
  prev.includes(reason) ? prev.filter((r) => r !== reason) : [...prev, reason]
@@ -193,6 +280,8 @@ function Actions() {
193
280
 
194
281
  const startEditing = () => {
195
282
  if (submittedFeedback) {
283
+ setSelectedTopReason(submittedFeedback.reasons[0] ?? null)
284
+ setSelectedSubReason(submittedFeedback.subReason ?? null)
196
285
  setSelectedReasons([...submittedFeedback.reasons])
197
286
  setDetailText(submittedFeedback.detail)
198
287
  }
@@ -200,11 +289,12 @@ function Actions() {
200
289
  }
201
290
 
202
291
  const handleDismissSubmit = () => {
203
- if (!canSubmitDismiss) return
204
- const fb = { reasons: [...selectedReasons], detail: detailText.trim() }
292
+ if (!canSubmitDismiss || !selectedTopReason) return
293
+ const fb = { reasons: [selectedTopReason], detail: detailText.trim(), subReason: selectedSubReason ?? undefined }
205
294
  setSubmittedFeedback(fb)
206
- dismiss(selectedReasons, detailText.trim())
207
- setSelectedReasons([])
295
+ dismiss([selectedTopReason], detailText.trim(), selectedSubReason ?? undefined)
296
+ setSelectedTopReason(null)
297
+ setSelectedSubReason(null)
208
298
  setDetailText("")
209
299
  setIsEditing(false)
210
300
  }
@@ -219,6 +309,8 @@ function Actions() {
219
309
  }
220
310
 
221
311
  const handleEditCancel = () => {
312
+ setSelectedTopReason(null)
313
+ setSelectedSubReason(null)
222
314
  setSelectedReasons([])
223
315
  setDetailText("")
224
316
  setIsEditing(false)
@@ -226,6 +318,8 @@ function Actions() {
226
318
 
227
319
  const handleCancel = () => {
228
320
  cancel()
321
+ setSelectedTopReason(null)
322
+ setSelectedSubReason(null)
229
323
  setSelectedReasons([])
230
324
  setDetailText("")
231
325
  }
@@ -404,20 +498,37 @@ function Actions() {
404
498
  </div>
405
499
  <p className="text-xs font-medium text-muted-foreground">Edit your feedback</p>
406
500
  <div className="flex flex-wrap gap-1.5">
407
- {dismissReasons.map((reason) => {
408
- const selected = selectedReasons.includes(reason)
501
+ {dismissReasonTree.map((node) => {
502
+ const selected = selectedTopReason === node.label
409
503
  return (
410
- <button key={reason} type="button" onClick={() => toggleReason(reason)}
504
+ <button key={node.label} type="button" onClick={() => selectTopReason(node.label)}
411
505
  className={`rounded-full border px-2.5 py-1 text-[11px] font-medium transition-colors ${
412
506
  selected ? "border-red-200 bg-red-100 text-red-700" : "border-border bg-background text-muted-foreground hover:bg-muted/50 hover:text-foreground"
413
- }`}>{reason}</button>
507
+ }`}>{node.label}</button>
414
508
  )
415
509
  })}
416
510
  </div>
417
- <input type="text" value={detailText} onChange={(e) => setDetailText(e.target.value)}
418
- onKeyDown={(e) => { if (e.key === "Enter" && canSubmitDismiss) handleDismissSubmit() }}
419
- placeholder={otherSelected ? "Please describe (required)" : "Provide additional feedback..."}
420
- className="h-7 w-full rounded-md border border-border bg-muted/20 px-2.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring" />
511
+ {topNode?.subOptions && (
512
+ <div className="ml-3 border-l-2 border-muted pl-3">
513
+ <div className="flex flex-wrap gap-1.5">
514
+ {topNode.subOptions.map((sub) => {
515
+ const selected = selectedSubReason === sub
516
+ return (
517
+ <button key={sub} type="button" onClick={() => selectSubReason(sub)}
518
+ className={`rounded-full border px-2.5 py-1 text-[11px] font-medium transition-colors ${
519
+ selected ? "border-red-200 bg-red-100 text-red-700" : "border-border bg-background text-muted-foreground hover:bg-muted/50 hover:text-foreground"
520
+ }`}>{sub}</button>
521
+ )
522
+ })}
523
+ </div>
524
+ </div>
525
+ )}
526
+ {selectedTopReason && (
527
+ <input type="text" value={detailText} onChange={(e) => setDetailText(e.target.value)}
528
+ onKeyDown={(e) => { if (e.key === "Enter" && canSubmitDismiss) handleDismissSubmit() }}
529
+ placeholder={needsText ? "Please describe (required)" : "Add context (optional)"}
530
+ className="h-7 w-full rounded-md border border-border bg-muted/20 px-2.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring" />
531
+ )}
421
532
  <div className="flex items-center gap-2">
422
533
  <button type="button" onClick={handleDismissSubmit} disabled={!canSubmitDismiss}
423
534
  className={`inline-flex h-7 items-center gap-1.5 rounded-md px-3 text-xs font-semibold transition-colors ${canSubmitDismiss ? "bg-foreground text-background hover:bg-foreground/90" : "cursor-not-allowed bg-muted text-muted-foreground"}`}>
@@ -442,6 +553,7 @@ function Actions() {
442
553
  <SubmittedFeedback
443
554
  reasons={submittedFeedback.reasons}
444
555
  detail={submittedFeedback.detail}
556
+ subReason={submittedFeedback.subReason}
445
557
  variant="dismiss"
446
558
  onEdit={startEditing}
447
559
  />
@@ -484,26 +596,50 @@ function Actions() {
484
596
  <div className="space-y-3">
485
597
  <p className="text-xs font-medium text-muted-foreground">{labels.dismissPrompt}</p>
486
598
  <div className="flex flex-wrap gap-1.5">
487
- {dismissReasons.map((reason) => {
488
- const selected = selectedReasons.includes(reason)
599
+ {dismissReasonTree.map((node) => {
600
+ const selected = selectedTopReason === node.label
489
601
  return (
490
602
  <button
491
- key={reason}
603
+ key={node.label}
492
604
  type="button"
493
- onClick={() => toggleReason(reason)}
605
+ onClick={() => selectTopReason(node.label)}
494
606
  className={`rounded-full border px-2.5 py-1 text-[11px] font-medium transition-colors ${
495
607
  selected
496
608
  ? "border-red-200 bg-red-100 text-red-700"
497
609
  : "border-border bg-background text-muted-foreground hover:bg-muted/50 hover:text-foreground"
498
610
  }`}
499
611
  >
500
- {reason}
612
+ {node.label}
501
613
  </button>
502
614
  )
503
615
  })}
504
616
  </div>
505
617
 
506
- {(selectedReasons.length > 0 || otherSelected) && (
618
+ {topNode?.subOptions && (
619
+ <div className="ml-3 border-l-2 border-muted pl-3">
620
+ <div className="flex flex-wrap gap-1.5">
621
+ {topNode.subOptions.map((sub) => {
622
+ const selected = selectedSubReason === sub
623
+ return (
624
+ <button
625
+ key={sub}
626
+ type="button"
627
+ onClick={() => selectSubReason(sub)}
628
+ className={`rounded-full border px-2.5 py-1 text-[11px] font-medium transition-colors ${
629
+ selected
630
+ ? "border-red-200 bg-red-100 text-red-700"
631
+ : "border-border bg-background text-muted-foreground hover:bg-muted/50 hover:text-foreground"
632
+ }`}
633
+ >
634
+ {sub}
635
+ </button>
636
+ )
637
+ })}
638
+ </div>
639
+ </div>
640
+ )}
641
+
642
+ {selectedTopReason && (
507
643
  <input
508
644
  type="text"
509
645
  value={detailText}
@@ -511,7 +647,7 @@ function Actions() {
511
647
  onKeyDown={(e) => {
512
648
  if (e.key === "Enter" && canSubmitDismiss) handleDismissSubmit()
513
649
  }}
514
- placeholder={otherSelected ? "Please describe (required)" : "Provide additional feedback..."}
650
+ placeholder={needsText ? "Please describe (required)" : "Add context (optional)"}
515
651
  className="h-7 w-full rounded-md border border-border bg-muted/20 px-2.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
516
652
  />
517
653
  )}
@@ -167,6 +167,7 @@ export interface SuggestedAction {
167
167
  callMeta?: SuggestedActionCallMeta
168
168
  manualMeta?: SuggestedActionManualMeta
169
169
  browserMeta?: SuggestedActionBrowserMeta
170
+ onFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
170
171
  }
171
172
 
172
173
  // ---------------------------------------------------------------------------
@@ -1253,6 +1254,7 @@ function SuggestedActionCard({
1253
1254
  onOpenRecentActivity,
1254
1255
  onMarkComplete,
1255
1256
  onDispatchAgent,
1257
+ onFeedback,
1256
1258
  iconMap,
1257
1259
  sendLabel,
1258
1260
  accountDetailsLabel,
@@ -1269,6 +1271,7 @@ function SuggestedActionCard({
1269
1271
  onOpenRecentActivity?: () => void
1270
1272
  onMarkComplete?: (id: number | string) => void
1271
1273
  onDispatchAgent?: (id: number | string, editedContent?: string, settings?: { aiDisclosureEnabled?: boolean; maxDurationMinutes?: string; callRecordingEnabled?: boolean; recordingNoticeEnabled?: boolean }) => void
1274
+ onFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
1272
1275
  iconMap?: SuggestedActionsIconMap
1273
1276
  sendLabel?: string
1274
1277
  accountDetailsLabel?: string
@@ -1405,10 +1408,14 @@ function SuggestedActionCard({
1405
1408
  {feedbackOpen && (
1406
1409
  <div className="px-5 py-3 border-b border-border/40 animate-in fade-in slide-in-from-top-2 duration-200">
1407
1410
  <DraftFeedbackInline
1408
- onRegenerateRequest={(pills, detail) => console.log("Regenerate:", pills, detail)}
1409
- onSubmitFeedback={(type, pills, detail) => console.log("Feedback:", type, pills, detail)}
1411
+ onRegenerateRequest={(pills, detail) => {
1412
+ onFeedback?.("down", pills, detail)
1413
+ }}
1414
+ onSubmitFeedback={(type, pills, detail) => {
1415
+ onFeedback?.(type, pills, detail)
1416
+ }}
1410
1417
  onDiscardRequest={(pills, detail) => {
1411
- console.log("Discard:", pills, detail)
1418
+ onFeedback?.("down", pills, detail)
1412
1419
  onDismiss?.(action.id)
1413
1420
  }}
1414
1421
  />
@@ -1972,6 +1979,7 @@ export function SuggestedActions({
1972
1979
  onOpenRecentActivity={onOpenRecentActivity}
1973
1980
  onMarkComplete={onMarkComplete}
1974
1981
  onDispatchAgent={onDispatchAgent}
1982
+ onFeedback={action.onFeedback}
1975
1983
  iconMap={iconMap}
1976
1984
  sendLabel={sendLabel}
1977
1985
  accountDetailsLabel={accountDetailsLabel}
@@ -44,7 +44,7 @@ export interface SignalScoreData {
44
44
  confidence: number
45
45
  onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
46
46
  onApproveFeedback?: (reasons: string[], detail: string) => void
47
- onDismissFeedback?: (reasons: string[], detail: string) => void
47
+ onDismissFeedback?: (reasons: string[], detail: string, subReason?: string) => void
48
48
  }
49
49
 
50
50
  // ---------------------------------------------------------------------------
@@ -72,6 +72,7 @@ export interface InboxViewConfig {
72
72
  hideToolbarActions?: boolean
73
73
  hideHoverActions?: boolean
74
74
  onSuggestedActionFeedback?: (actionId: number | string, feedback: string, actionTitle?: string) => void
75
+ onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
75
76
  buildEntityChips?: (item: QueueItem) => Array<{ id: string; label: string; avatarLetter: string; onClick?: () => void }>
76
77
  quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
77
78
  hideAccountsButton?: boolean
@@ -30,6 +30,7 @@ import {
30
30
  } from "../components/inbox-toolbar"
31
31
  import { GroupedListView, type GroupedListGroup } from "../components/item-list"
32
32
  import { SignalApproval, type ApprovalState } from "../components/signal-feedback-inline"
33
+ import { ScoreFeedback } from "../components/score-feedback"
33
34
  import { ScoreBreakdown } from "../components/score-breakdown"
34
35
  import { Citation, type SourceDef } from "../components/detail-view"
35
36
  import {
@@ -107,6 +108,7 @@ export interface DetailViewProps {
107
108
  hideApproveButton?: boolean
108
109
  signalBriefCopy?: InboxViewConfig["signalBriefCopy"]
109
110
  renderDetailExtra?: (item: QueueItem) => React.ReactNode
111
+ onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
110
112
  }
111
113
 
112
114
  export function DetailView({
@@ -128,6 +130,7 @@ export function DetailView({
128
130
  hideApproveButton,
129
131
  signalBriefCopy,
130
132
  renderDetailExtra,
133
+ onScoreFeedback,
131
134
  }: DetailViewProps) {
132
135
  const [evidenceExpanded, setEvidenceExpanded] = React.useState(false)
133
136
  const [showTimeline, setShowTimeline] = React.useState(false)
@@ -180,9 +183,8 @@ export function DetailView({
180
183
  signalData.onApproveFeedback?.(reasons, detail)
181
184
  console.log("Approval feedback:", { taskId: item.id, company: item.company, reasons, detail })
182
185
  }}
183
- onDismiss={(reasons, detail) => {
184
- signalData.onDismissFeedback?.(reasons, detail)
185
- console.log("Dismissed signal:", { taskId: item.id, reasons, detail })
186
+ onDismiss={(reasons, detail, subReason) => {
187
+ signalData.onDismissFeedback?.(reasons, detail, subReason)
186
188
  }}
187
189
  >
188
190
  <div className="mx-auto w-full max-w-3xl p-6 pb-12 md:p-8">
@@ -277,14 +279,17 @@ export function DetailView({
277
279
  {signalData.whyNow}
278
280
  </p>
279
281
 
282
+ <ScoreFeedback.Root onSubmitFeedback={(type, pills, detail) => onScoreFeedback?.(type, pills, detail)}>
280
283
  <div className="mb-5 rounded-md border border-border bg-muted/20 p-3">
281
284
  <div className="flex items-center justify-between mb-1.5">
282
285
  <span className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Signal Score</span>
283
286
  <div className="flex items-center gap-2">
284
287
  <span className="text-sm font-bold text-foreground">{signalData.score}/100</span>
285
288
  <span className={`text-[10px] font-bold uppercase ${scoreColor}`}>{scoreLabel}</span>
289
+ <ScoreFeedback.Trigger />
286
290
  </div>
287
291
  </div>
292
+ <ScoreFeedback.Panel />
288
293
  <div className="h-1.5 bg-muted rounded-full overflow-hidden mb-2">
289
294
  <div
290
295
  className={`h-full rounded-full transition-all duration-500 ${barColor}`}
@@ -320,6 +325,7 @@ export function DetailView({
320
325
  </div>
321
326
  )}
322
327
  </div>
328
+ </ScoreFeedback.Root>
323
329
 
324
330
  {!evidenceExpanded && <SignalApproval.Actions />}
325
331
  </div>
@@ -398,6 +404,7 @@ export function PrototypeInboxView({
398
404
  hideToolbarActions,
399
405
  hideHoverActions,
400
406
  onSuggestedActionFeedback,
407
+ onScoreFeedback,
401
408
  headerActions,
402
409
  onOpenEntityPanel,
403
410
  onOpenRecentActivity,
@@ -640,6 +647,7 @@ export function PrototypeInboxView({
640
647
  hideApproveButton,
641
648
  signalBriefCopy,
642
649
  renderDetailExtra,
650
+ onScoreFeedback,
643
651
  }
644
652
 
645
653
  return (