@handled-ai/design-system 0.18.22 → 0.18.23
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/case-panel-activity-timeline.d.ts +100 -0
- package/dist/components/case-panel-activity-timeline.js +270 -0
- package/dist/components/case-panel-activity-timeline.js.map +1 -0
- package/dist/components/case-panel-detail.d.ts +60 -0
- package/dist/components/case-panel-detail.js +129 -0
- package/dist/components/case-panel-detail.js.map +1 -0
- package/dist/components/case-panel-email-composer.d.ts +61 -0
- package/dist/components/case-panel-email-composer.js +304 -0
- package/dist/components/case-panel-email-composer.js.map +1 -0
- package/dist/components/case-panel-why.d.ts +35 -0
- package/dist/components/case-panel-why.js +149 -0
- package/dist/components/case-panel-why.js.map +1 -0
- package/dist/components/contextual-quick-action-launcher.d.ts +7 -3
- package/dist/components/contextual-quick-action-launcher.js +99 -27
- package/dist/components/contextual-quick-action-launcher.js.map +1 -1
- package/dist/components/data-table.js +0 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/score-analysis-modal.d.ts +2 -8
- package/dist/components/score-analysis-modal.js +6 -19
- package/dist/components/score-analysis-modal.js.map +1 -1
- package/dist/components/score-breakdown.d.ts +1 -3
- package/dist/components/score-breakdown.js +6 -5
- package/dist/components/score-breakdown.js.map +1 -1
- package/dist/components/score-ring.d.ts +3 -6
- package/dist/components/score-ring.js +14 -11
- package/dist/components/score-ring.js.map +1 -1
- package/dist/components/score-why-chips.d.ts +2 -3
- package/dist/components/score-why-chips.js +21 -10
- package/dist/components/score-why-chips.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +0 -1
- package/dist/components/signal-priority-popover.js +20 -20
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/prototype/index.d.ts +0 -1
- package/dist/prototype/prototype-accounts-view.d.ts +0 -1
- package/dist/prototype/prototype-admin-view.d.ts +0 -1
- package/dist/prototype/prototype-config.d.ts +0 -1
- package/dist/prototype/prototype-inbox-view.d.ts +0 -1
- package/dist/prototype/prototype-insights-view.d.ts +0 -1
- package/dist/prototype/prototype-shell.d.ts +0 -1
- package/package.json +1 -1
- package/src/components/__tests__/case-panel-activity-timeline.test.tsx +152 -0
- package/src/components/__tests__/case-panel-detail.test.tsx +138 -0
- package/src/components/__tests__/case-panel-email-composer.test.tsx +171 -0
- package/src/components/__tests__/case-panel-why.test.tsx +152 -0
- package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +87 -0
- package/src/components/__tests__/signal-priority-popover.test.tsx +5 -7
- package/src/components/case-panel-activity-timeline.tsx +414 -0
- package/src/components/case-panel-detail.tsx +228 -0
- package/src/components/case-panel-email-composer.tsx +341 -0
- package/src/components/case-panel-why.tsx +214 -0
- package/src/components/contextual-quick-action-launcher.tsx +92 -15
- package/src/components/data-table.tsx +0 -1
- package/src/components/score-analysis-modal.tsx +5 -22
- package/src/components/score-breakdown.tsx +6 -7
- package/src/components/score-ring.tsx +13 -11
- package/src/components/score-why-chips.tsx +23 -12
- package/src/components/signal-priority-popover.tsx +21 -21
- package/src/index.ts +4 -1
- package/dist/components/score-semantics.d.ts +0 -27
- package/dist/components/score-semantics.js +0 -173
- package/dist/components/score-semantics.js.map +0 -1
- package/src/components/__tests__/score-analysis-modal.test.tsx +0 -55
- package/src/components/__tests__/score-breakdown-intent.test.tsx +0 -47
- package/src/components/__tests__/score-ring.test.tsx +0 -43
- package/src/components/__tests__/score-semantics.test.ts +0 -107
- package/src/components/score-semantics.ts +0 -187
|
@@ -8,19 +8,8 @@ import { ScoreRing } from "./score-ring.js";
|
|
|
8
8
|
import { ScoreBreakdown } from "./score-breakdown.js";
|
|
9
9
|
import { SignalApproval } from "./signal-feedback-inline.js";
|
|
10
10
|
import { X } from "lucide-react";
|
|
11
|
-
function getScoreLabel(score, denominator
|
|
11
|
+
function getScoreLabel(score, denominator) {
|
|
12
12
|
const pct = score / denominator * 100;
|
|
13
|
-
if (intent === "urgency") {
|
|
14
|
-
if (pct >= 80) return { text: "URGENT", className: "text-red-600" };
|
|
15
|
-
if (pct >= 60) return { text: "HIGH", className: "text-orange-600" };
|
|
16
|
-
if (pct >= 35) return { text: "MEDIUM", className: "text-amber-600" };
|
|
17
|
-
return { text: "LOW", className: "text-neutral-600" };
|
|
18
|
-
}
|
|
19
|
-
if (intent === "risk") {
|
|
20
|
-
if (pct >= 70) return { text: "High Risk", className: "text-red-600" };
|
|
21
|
-
if (pct >= 40) return { text: "Medium Risk", className: "text-amber-600" };
|
|
22
|
-
return { text: "Low Risk", className: "text-neutral-600" };
|
|
23
|
-
}
|
|
24
13
|
if (pct >= 70) return { text: "HIGH", className: "text-emerald-600" };
|
|
25
14
|
if (pct >= 40) return { text: "MEDIUM", className: "text-amber-600" };
|
|
26
15
|
return { text: "LOW", className: "text-red-600" };
|
|
@@ -41,10 +30,9 @@ function ScoreAnalysisModal({
|
|
|
41
30
|
onApprove,
|
|
42
31
|
onApproveFeedback,
|
|
43
32
|
onDismiss,
|
|
44
|
-
useInlinePanel = false
|
|
45
|
-
scoreIntent = "positive"
|
|
33
|
+
useInlinePanel = false
|
|
46
34
|
}) {
|
|
47
|
-
const label = getScoreLabel(score, denominator
|
|
35
|
+
const label = getScoreLabel(score, denominator);
|
|
48
36
|
const panelContent = /* @__PURE__ */ jsxs(
|
|
49
37
|
SignalApproval.Root,
|
|
50
38
|
{
|
|
@@ -71,7 +59,7 @@ function ScoreAnalysisModal({
|
|
|
71
59
|
/* @__PURE__ */ jsx("div", { className: "mb-8", children: /* @__PURE__ */ jsx(SignalApproval.Actions, {}) }),
|
|
72
60
|
/* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
73
61
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3", children: [
|
|
74
|
-
/* @__PURE__ */ jsx(ScoreRing, { score, denominator, size: 120, strokeWidth: 10
|
|
62
|
+
/* @__PURE__ */ jsx(ScoreRing, { score, denominator, size: 120, strokeWidth: 10 }),
|
|
75
63
|
/* @__PURE__ */ jsxs(Badge, { variant: "outline", children: [
|
|
76
64
|
Math.round(score / denominator * 100),
|
|
77
65
|
"% Score",
|
|
@@ -92,7 +80,7 @@ function ScoreAnalysisModal({
|
|
|
92
80
|
] }),
|
|
93
81
|
factors && factors.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
94
82
|
/* @__PURE__ */ jsx("h3", { className: "font-semibold mb-2 text-sm", children: "Score Breakdown" }),
|
|
95
|
-
/* @__PURE__ */ jsx(ScoreBreakdown, { factors, onFactorFeedback
|
|
83
|
+
/* @__PURE__ */ jsx(ScoreBreakdown, { factors, onFactorFeedback })
|
|
96
84
|
] })
|
|
97
85
|
] })
|
|
98
86
|
] })
|
|
@@ -148,7 +136,6 @@ function ScoreAnalysisModal({
|
|
|
148
136
|
const ScoreAnalysisPanel = ScoreAnalysisModal;
|
|
149
137
|
export {
|
|
150
138
|
ScoreAnalysisModal,
|
|
151
|
-
ScoreAnalysisPanel
|
|
152
|
-
getScoreLabel
|
|
139
|
+
ScoreAnalysisPanel
|
|
153
140
|
};
|
|
154
141
|
//# sourceMappingURL=score-analysis-modal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/score-analysis-modal.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Sheet, SheetContent, SheetHeader, SheetTitle } from \"./sheet\"\nimport { Badge } from \"./badge\"\nimport { ScoreRing } from \"./score-ring\"\nimport { ScoreBreakdown, type ScoreFactor } from \"./score-breakdown\"\nimport { SignalApproval } from \"./signal-feedback-inline\"\nimport { X } from \"lucide-react\"\nimport type { ScoreIntent } from \"./score-semantics\"\n\ninterface ScoreAnalysisModalProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n title: string\n description: string\n score: number\n denominator?: number\n whyNow: string\n evidence: React.ReactNode[]\n factors?: ScoreFactor[]\n onFactorFeedback?: (factorKey: string, type: \"up\" | \"down\" | null, detail?: string) => void\n companyName?: string\n opportunityUrl?: string\n onApprove?: () => void\n onApproveFeedback?: (reasons: string[], detail: string) => void\n onDismiss?: (reasons: string[], detail: string) => void\n /** When true, renders as an absolute-positioned inline panel instead of a Radix Sheet portal. Useful when the component is inside a container that should not be escaped. */\n useInlinePanel?: boolean\n scoreIntent?: ScoreIntent\n}\n\nfunction getScoreLabel(score: number, denominator: number, intent: ScoreIntent = \"positive\") {\n const pct = (score / denominator) * 100\n\n if (intent === \"urgency\") {\n if (pct >= 80) return { text: \"URGENT\", className: \"text-red-600\" }\n if (pct >= 60) return { text: \"HIGH\", className: \"text-orange-600\" }\n if (pct >= 35) return { text: \"MEDIUM\", className: \"text-amber-600\" }\n return { text: \"LOW\", className: \"text-neutral-600\" }\n }\n\n if (intent === \"risk\") {\n if (pct >= 70) return { text: \"High Risk\", className: \"text-red-600\" }\n if (pct >= 40) return { text: \"Medium Risk\", className: \"text-amber-600\" }\n return { text: \"Low Risk\", className: \"text-neutral-600\" }\n }\n\n if (pct >= 70) return { text: \"HIGH\", className: \"text-emerald-600\" }\n if (pct >= 40) return { text: \"MEDIUM\", className: \"text-amber-600\" }\n return { text: \"LOW\", className: \"text-red-600\" }\n}\n\nfunction ScoreAnalysisModal({\n open,\n onOpenChange,\n title,\n description,\n score,\n denominator = 100,\n whyNow,\n evidence,\n factors,\n onFactorFeedback,\n companyName = \"Account\",\n opportunityUrl,\n onApprove,\n onApproveFeedback,\n onDismiss,\n useInlinePanel = false,\n scoreIntent = \"positive\",\n}: ScoreAnalysisModalProps) {\n const label = getScoreLabel(score, denominator, scoreIntent)\n\n const panelContent = (\n <SignalApproval.Root\n companyName={companyName}\n opportunityUrl={opportunityUrl}\n onApprove={onApprove}\n onApproveFeedback={onApproveFeedback}\n onDismiss={onDismiss}\n >\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-border shrink-0\">\n <h2 className=\"text-lg font-semibold text-foreground\">{title}</h2>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"p-1.5 rounded-md text-muted-foreground hover:bg-secondary transition-colors\"\n >\n <X className=\"w-4 h-4\" />\n </button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto px-6 py-6\">\n <p className=\"text-sm text-muted-foreground mb-6\">{description}</p>\n\n <div className=\"mb-8\">\n <SignalApproval.Actions />\n </div>\n\n <div className=\"space-y-6\">\n <div className=\"flex flex-col items-center gap-3\">\n <ScoreRing score={score} denominator={denominator} size={120} strokeWidth={10} intent={scoreIntent} />\n <Badge variant=\"outline\">\n {Math.round((score / denominator) * 100)}% Score\n {\" \\u2014 \"}\n <span className={label.className}>{label.text}</span>\n </Badge>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-2 text-sm\">Why Now</h3>\n <p className=\"text-sm text-muted-foreground leading-relaxed\">{whyNow}</p>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-2 text-sm\">Supporting Evidence</h3>\n <ul className=\"space-y-2\">\n {evidence.map((item, index) => (\n <li key={index} className=\"flex items-start gap-2 text-sm\">\n <div className=\"w-1.5 h-1.5 bg-primary rounded-full mt-2 flex-shrink-0\" />\n <span className=\"text-muted-foreground leading-relaxed\">{item}</span>\n </li>\n ))}\n </ul>\n </div>\n\n {factors && factors.length > 0 && (\n <div>\n <h3 className=\"font-semibold mb-2 text-sm\">Score Breakdown</h3>\n <ScoreBreakdown factors={factors} onFactorFeedback={onFactorFeedback} scoreIntent={scoreIntent} />\n </div>\n )}\n </div>\n </div>\n </SignalApproval.Root>\n )\n\n if (useInlinePanel) {\n if (!open) return null\n return (\n <div style={{ position: \"absolute\", inset: 0, zIndex: 50, pointerEvents: \"auto\", overflow: \"hidden\" }}>\n <div\n onClick={() => onOpenChange(false)}\n style={{ position: \"absolute\", inset: 0, backgroundColor: \"rgba(0,0,0,0.5)\", transition: \"opacity 200ms\" }}\n />\n <div\n style={{\n position: \"absolute\",\n top: 0,\n right: 0,\n bottom: 0,\n width: 500,\n maxWidth: \"100%\",\n zIndex: 50,\n display: \"flex\",\n flexDirection: \"column\",\n overflow: \"hidden\",\n borderLeft: \"1px solid var(--border)\",\n backgroundColor: \"var(--background, #fff)\",\n boxShadow: \"-4px 0 24px rgba(0,0,0,0.12)\",\n }}\n >\n {panelContent}\n </div>\n </div>\n )\n }\n\n return (\n <Sheet open={open} onOpenChange={onOpenChange}>\n <SheetContent\n side=\"right\"\n className=\"w-full sm:w-[500px] sm:max-w-[600px] overflow-hidden p-0 bg-background border-l border-border flex flex-col\"\n showCloseButton={false}\n >\n <SheetHeader className=\"sr-only p-0\">\n <SheetTitle>{title}</SheetTitle>\n </SheetHeader>\n {panelContent}\n </SheetContent>\n </Sheet>\n )\n}\n\nconst ScoreAnalysisPanel = ScoreAnalysisModal\n\nexport { ScoreAnalysisModal, ScoreAnalysisPanel, getScoreLabel }\nexport type { ScoreAnalysisModalProps }\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/score-analysis-modal.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Sheet, SheetContent, SheetHeader, SheetTitle } from \"./sheet\"\nimport { Badge } from \"./badge\"\nimport { ScoreRing } from \"./score-ring\"\nimport { ScoreBreakdown, type ScoreFactor } from \"./score-breakdown\"\nimport { SignalApproval } from \"./signal-feedback-inline\"\nimport { X } from \"lucide-react\"\n\ninterface ScoreAnalysisModalProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n title: string\n description: string\n score: number\n denominator?: number\n whyNow: string\n evidence: React.ReactNode[]\n factors?: ScoreFactor[]\n onFactorFeedback?: (factorKey: string, type: \"up\" | \"down\" | null, detail?: string) => void\n companyName?: string\n opportunityUrl?: string\n onApprove?: () => void\n onApproveFeedback?: (reasons: string[], detail: string) => void\n onDismiss?: (reasons: string[], detail: string) => void\n /** When true, renders as an absolute-positioned inline panel instead of a Radix Sheet portal. Useful when the component is inside a container that should not be escaped. */\n useInlinePanel?: boolean\n}\n\nfunction getScoreLabel(score: number, denominator: number) {\n const pct = (score / denominator) * 100\n if (pct >= 70) return { text: \"HIGH\", className: \"text-emerald-600\" }\n if (pct >= 40) return { text: \"MEDIUM\", className: \"text-amber-600\" }\n return { text: \"LOW\", className: \"text-red-600\" }\n}\n\nfunction ScoreAnalysisModal({\n open,\n onOpenChange,\n title,\n description,\n score,\n denominator = 100,\n whyNow,\n evidence,\n factors,\n onFactorFeedback,\n companyName = \"Account\",\n opportunityUrl,\n onApprove,\n onApproveFeedback,\n onDismiss,\n useInlinePanel = false,\n}: ScoreAnalysisModalProps) {\n const label = getScoreLabel(score, denominator)\n\n const panelContent = (\n <SignalApproval.Root\n companyName={companyName}\n opportunityUrl={opportunityUrl}\n onApprove={onApprove}\n onApproveFeedback={onApproveFeedback}\n onDismiss={onDismiss}\n >\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-border shrink-0\">\n <h2 className=\"text-lg font-semibold text-foreground\">{title}</h2>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"p-1.5 rounded-md text-muted-foreground hover:bg-secondary transition-colors\"\n >\n <X className=\"w-4 h-4\" />\n </button>\n </div>\n\n <div className=\"flex-1 overflow-y-auto px-6 py-6\">\n <p className=\"text-sm text-muted-foreground mb-6\">{description}</p>\n\n <div className=\"mb-8\">\n <SignalApproval.Actions />\n </div>\n\n <div className=\"space-y-6\">\n <div className=\"flex flex-col items-center gap-3\">\n <ScoreRing score={score} denominator={denominator} size={120} strokeWidth={10} />\n <Badge variant=\"outline\">\n {Math.round((score / denominator) * 100)}% Score\n {\" \\u2014 \"}\n <span className={label.className}>{label.text}</span>\n </Badge>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-2 text-sm\">Why Now</h3>\n <p className=\"text-sm text-muted-foreground leading-relaxed\">{whyNow}</p>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-2 text-sm\">Supporting Evidence</h3>\n <ul className=\"space-y-2\">\n {evidence.map((item, index) => (\n <li key={index} className=\"flex items-start gap-2 text-sm\">\n <div className=\"w-1.5 h-1.5 bg-primary rounded-full mt-2 flex-shrink-0\" />\n <span className=\"text-muted-foreground leading-relaxed\">{item}</span>\n </li>\n ))}\n </ul>\n </div>\n\n {factors && factors.length > 0 && (\n <div>\n <h3 className=\"font-semibold mb-2 text-sm\">Score Breakdown</h3>\n <ScoreBreakdown factors={factors} onFactorFeedback={onFactorFeedback} />\n </div>\n )}\n </div>\n </div>\n </SignalApproval.Root>\n )\n\n if (useInlinePanel) {\n if (!open) return null\n return (\n <div style={{ position: \"absolute\", inset: 0, zIndex: 50, pointerEvents: \"auto\", overflow: \"hidden\" }}>\n <div\n onClick={() => onOpenChange(false)}\n style={{ position: \"absolute\", inset: 0, backgroundColor: \"rgba(0,0,0,0.5)\", transition: \"opacity 200ms\" }}\n />\n <div\n style={{\n position: \"absolute\",\n top: 0,\n right: 0,\n bottom: 0,\n width: 500,\n maxWidth: \"100%\",\n zIndex: 50,\n display: \"flex\",\n flexDirection: \"column\",\n overflow: \"hidden\",\n borderLeft: \"1px solid var(--border)\",\n backgroundColor: \"var(--background, #fff)\",\n boxShadow: \"-4px 0 24px rgba(0,0,0,0.12)\",\n }}\n >\n {panelContent}\n </div>\n </div>\n )\n }\n\n return (\n <Sheet open={open} onOpenChange={onOpenChange}>\n <SheetContent\n side=\"right\"\n className=\"w-full sm:w-[500px] sm:max-w-[600px] overflow-hidden p-0 bg-background border-l border-border flex flex-col\"\n showCloseButton={false}\n >\n <SheetHeader className=\"sr-only p-0\">\n <SheetTitle>{title}</SheetTitle>\n </SheetHeader>\n {panelContent}\n </SheetContent>\n </Sheet>\n )\n}\n\nconst ScoreAnalysisPanel = ScoreAnalysisModal\n\nexport { ScoreAnalysisModal, ScoreAnalysisPanel }\nexport type { ScoreAnalysisModalProps }\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { ScoreIntent } from './score-semantics.js';
|
|
3
2
|
|
|
4
3
|
interface ScoreFactor {
|
|
5
4
|
key: string;
|
|
@@ -16,8 +15,7 @@ interface ScoreBreakdownProps {
|
|
|
16
15
|
type: "up" | "down";
|
|
17
16
|
detail: string;
|
|
18
17
|
}>;
|
|
19
|
-
scoreIntent?: ScoreIntent;
|
|
20
18
|
}
|
|
21
|
-
declare function ScoreBreakdown({ factors, onFactorFeedback, className, initialFeedback
|
|
19
|
+
declare function ScoreBreakdown({ factors, onFactorFeedback, className, initialFeedback }: ScoreBreakdownProps): React.JSX.Element;
|
|
22
20
|
|
|
23
21
|
export { ScoreBreakdown, type ScoreBreakdownProps, type ScoreFactor };
|
|
@@ -24,9 +24,10 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
24
24
|
import * as React from "react";
|
|
25
25
|
import { ThumbsUp, ThumbsDown } from "lucide-react";
|
|
26
26
|
import { cn } from "../lib/utils.js";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
function getFactorBarColor(score) {
|
|
28
|
+
if (score >= 70) return "bg-emerald-500";
|
|
29
|
+
if (score >= 40) return "bg-amber-500";
|
|
30
|
+
return "bg-red-500";
|
|
30
31
|
}
|
|
31
32
|
function getRiskBadgeStyle(risk) {
|
|
32
33
|
switch (risk) {
|
|
@@ -44,7 +45,7 @@ function deriveInitialState(init, mapFn, filterFn) {
|
|
|
44
45
|
const filtered = filterFn ? entries.filter(([, v]) => filterFn(v)) : entries;
|
|
45
46
|
return Object.fromEntries(filtered.map(([k, v]) => [k, mapFn(v)]));
|
|
46
47
|
}
|
|
47
|
-
function ScoreBreakdown({ factors, onFactorFeedback, className, initialFeedback
|
|
48
|
+
function ScoreBreakdown({ factors, onFactorFeedback, className, initialFeedback }) {
|
|
48
49
|
const [feedback, setFeedback] = React.useState(
|
|
49
50
|
() => deriveInitialState(initialFeedback, (v) => v.type)
|
|
50
51
|
);
|
|
@@ -154,7 +155,7 @@ function ScoreBreakdown({ factors, onFactorFeedback, className, initialFeedback,
|
|
|
154
155
|
factor.score !== null && /* @__PURE__ */ jsx("div", { className: "w-full h-1 bg-muted rounded-full overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
155
156
|
"div",
|
|
156
157
|
{
|
|
157
|
-
className: cn("h-full rounded-full", getFactorBarColor(factor.score
|
|
158
|
+
className: cn("h-full rounded-full", getFactorBarColor(factor.score)),
|
|
158
159
|
style: { width: `${factor.score}%` }
|
|
159
160
|
}
|
|
160
161
|
) }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/score-breakdown.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ThumbsUp, ThumbsDown } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\nimport type { ScoreIntent } from \"./score-semantics\"\nimport { getScoreToneClasses } from \"./score-semantics\"\n\nexport interface ScoreFactor {\n key: string\n label: string\n score: number | null\n risk?: \"Low\" | \"Medium\" | \"High\"\n why: string\n}\n\nfunction getFactorBarColor(score: number, intent: ScoreIntent = \"positive\") {\n return getScoreToneClasses(score, intent).bar\n}\n\nfunction getRiskBadgeStyle(risk: \"Low\" | \"Medium\" | \"High\") {\n switch (risk) {\n case \"Low\":\n return \"text-emerald-600 bg-emerald-50 dark:text-emerald-400 dark:bg-emerald-950/30\"\n case \"Medium\":\n return \"text-amber-600 bg-amber-50 dark:text-amber-400 dark:bg-amber-950/30\"\n case \"High\":\n return \"text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-950/30\"\n }\n}\n\ninterface ScoreBreakdownProps {\n factors: ScoreFactor[]\n onFactorFeedback?: (factorKey: string, type: \"up\" | \"down\" | null, detail?: string) => void\n className?: string\n initialFeedback?: Record<string, { type: \"up\" | \"down\"; detail: string }>\n scoreIntent?: ScoreIntent\n}\n\nfunction deriveInitialState<T>(\n init: Record<string, { type: \"up\" | \"down\"; detail: string }> | undefined,\n mapFn: (v: { type: \"up\" | \"down\"; detail: string }) => T,\n filterFn?: (v: { type: \"up\" | \"down\"; detail: string }) => boolean,\n): Record<string, T> {\n if (!init) return {}\n const entries = Object.entries(init)\n const filtered = filterFn ? entries.filter(([, v]) => filterFn(v)) : entries\n return Object.fromEntries(filtered.map(([k, v]) => [k, mapFn(v)]))\n}\n\nfunction ScoreBreakdown({ factors, onFactorFeedback, className, initialFeedback, scoreIntent = \"positive\" }: ScoreBreakdownProps) {\n const [feedback, setFeedback] = React.useState<Record<string, \"up\" | \"down\" | null>>(\n () => deriveInitialState(initialFeedback, (v) => v.type)\n )\n const [feedbackText, setFeedbackText] = React.useState<Record<string, string>>(\n () => deriveInitialState(initialFeedback, (v) => v.detail)\n )\n const [savedText, setSavedText] = React.useState<Record<string, string>>(\n () => deriveInitialState(initialFeedback, (v) => v.detail, (v) => !!v.detail)\n )\n const [editingKey, setEditingKey] = React.useState<string | null>(null)\n\n // Sync state when initialFeedback prop changes (e.g. async hydration).\n // Skip keys the user is actively editing to avoid clobbering in-progress input.\n React.useEffect(() => {\n setFeedback((prev) => {\n const next: Record<string, \"up\" | \"down\" | null> = deriveInitialState(initialFeedback, (v) => v.type)\n // Preserve any key the user is currently editing\n if (editingKey && prev[editingKey] !== undefined) {\n next[editingKey] = prev[editingKey]\n }\n return next\n })\n setFeedbackText((prev) => {\n const next = deriveInitialState(initialFeedback, (v) => v.detail)\n if (editingKey && prev[editingKey] !== undefined) {\n next[editingKey] = prev[editingKey]\n }\n return next\n })\n setSavedText((prev) => {\n const next = deriveInitialState(initialFeedback, (v) => v.detail, (v) => !!v.detail)\n if (editingKey && prev[editingKey] !== undefined) {\n next[editingKey] = prev[editingKey]\n }\n return next\n })\n }, [initialFeedback]) // eslint-disable-line react-hooks/exhaustive-deps -- reads editingKey as guard, not trigger\n\n const handleFeedback = (factorKey: string, type: \"up\" | \"down\") => {\n const newState = feedback[factorKey] === type ? null : type\n setFeedback((prev) => ({ ...prev, [factorKey]: newState }))\n if (newState === null) {\n setEditingKey(null)\n onFactorFeedback?.(factorKey, null)\n } else {\n setEditingKey(factorKey)\n onFactorFeedback?.(factorKey, newState)\n }\n }\n\n const submitFeedbackText = (factorKey: string) => {\n const text = (feedbackText[factorKey] ?? \"\").trim()\n if (feedback[factorKey]) {\n onFactorFeedback?.(factorKey, feedback[factorKey]!, text ?? \"\")\n if (text) {\n setSavedText((prev) => ({ ...prev, [factorKey]: text }))\n }\n }\n setEditingKey(null)\n }\n\n return (\n <div className={cn(\"border border-border rounded-lg overflow-hidden divide-y divide-border/40\", className)}>\n {factors.map((factor) => {\n const feedbackState = feedback[factor.key]\n const hasThumb = feedbackState === \"up\" || feedbackState === \"down\"\n const isEditing = editingKey === factor.key\n const saved = savedText[factor.key]\n\n return (\n <div key={factor.key} className=\"px-3 py-3 space-y-1.5\">\n {/* Row 1: Factor name + score + rating buttons */}\n <div className=\"flex items-center gap-2\">\n <span className=\"text-[13px] font-semibold text-foreground flex-1\">{factor.label}</span>\n\n {factor.score !== null ? (\n <span className=\"text-xs font-bold text-foreground tabular-nums\">{factor.score}<span className=\"text-muted-foreground font-normal\">/100</span></span>\n ) : factor.risk ? (\n <span\n className={cn(\n \"text-[11px] font-semibold px-1.5 py-0.5 rounded\",\n getRiskBadgeStyle(factor.risk)\n )}\n >\n {factor.risk}\n </span>\n ) : null}\n\n <div className=\"flex items-center gap-0.5 ml-1\">\n <button\n type=\"button\"\n onClick={() => handleFeedback(factor.key, \"up\")}\n className={cn(\n \"p-1 rounded transition-colors\",\n feedbackState === \"up\"\n ? \"text-emerald-600 bg-emerald-50 dark:text-emerald-400 dark:bg-emerald-950/30\"\n : \"text-muted-foreground/40 hover:text-emerald-600 hover:bg-emerald-50/50\"\n )}\n title=\"This factor is accurate\"\n >\n <ThumbsUp className=\"w-3 h-3\" />\n </button>\n <button\n type=\"button\"\n onClick={() => handleFeedback(factor.key, \"down\")}\n className={cn(\n \"p-1 rounded transition-colors\",\n feedbackState === \"down\"\n ? \"text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-950/30\"\n : \"text-muted-foreground/40 hover:text-red-600 hover:bg-red-50/50\"\n )}\n title=\"Report issue with this factor\"\n >\n <ThumbsDown className=\"w-3 h-3\" />\n </button>\n </div>\n </div>\n\n {/* Row 2: Score bar */}\n {factor.score !== null && (\n <div className=\"w-full h-1 bg-muted rounded-full overflow-hidden\">\n <div\n className={cn(\"h-full rounded-full\", getFactorBarColor(factor.score, scoreIntent))}\n style={{ width: `${factor.score}%` }}\n />\n </div>\n )}\n\n {/* Row 3: Why text */}\n <p className=\"text-xs text-muted-foreground leading-relaxed\">{factor.why}</p>\n\n {/* Feedback input */}\n {hasThumb && isEditing && (\n <input\n type=\"text\"\n autoFocus\n value={feedbackText[factor.key] ?? \"\"}\n onChange={(e) =>\n setFeedbackText((prev) => ({ ...prev, [factor.key]: e.target.value }))\n }\n onKeyDown={(e) => {\n if (e.key === \"Enter\") submitFeedbackText(factor.key)\n if (e.key === \"Escape\") setEditingKey(null)\n }}\n onBlur={() => submitFeedbackText(factor.key)}\n placeholder={\n feedbackState === \"up\"\n ? \"What\\u2019s accurate? (optional)\"\n : \"What\\u2019s wrong? (optional)\"\n }\n className=\"w-full h-6 rounded border border-border bg-background px-2 text-[11px] text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring\"\n />\n )}\n {hasThumb && !isEditing && saved && (\n <button\n type=\"button\"\n onClick={() => {\n setFeedbackText((prev) => ({ ...prev, [factor.key]: saved }))\n setEditingKey(factor.key)\n }}\n className=\"text-[11px] text-muted-foreground/70 hover:text-muted-foreground transition-colors text-left leading-snug\"\n >\n {saved}\n </button>\n )}\n </div>\n )\n })}\n </div>\n )\n}\n\nexport { ScoreBreakdown }\nexport type { ScoreBreakdownProps }\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/score-breakdown.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ThumbsUp, ThumbsDown } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\n\nexport interface ScoreFactor {\n key: string\n label: string\n score: number | null\n risk?: \"Low\" | \"Medium\" | \"High\"\n why: string\n}\n\nfunction getFactorBarColor(score: number) {\n if (score >= 70) return \"bg-emerald-500\"\n if (score >= 40) return \"bg-amber-500\"\n return \"bg-red-500\"\n}\n\nfunction getRiskBadgeStyle(risk: \"Low\" | \"Medium\" | \"High\") {\n switch (risk) {\n case \"Low\":\n return \"text-emerald-600 bg-emerald-50 dark:text-emerald-400 dark:bg-emerald-950/30\"\n case \"Medium\":\n return \"text-amber-600 bg-amber-50 dark:text-amber-400 dark:bg-amber-950/30\"\n case \"High\":\n return \"text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-950/30\"\n }\n}\n\ninterface ScoreBreakdownProps {\n factors: ScoreFactor[]\n onFactorFeedback?: (factorKey: string, type: \"up\" | \"down\" | null, detail?: string) => void\n className?: string\n initialFeedback?: Record<string, { type: \"up\" | \"down\"; detail: string }>\n}\n\nfunction deriveInitialState<T>(\n init: Record<string, { type: \"up\" | \"down\"; detail: string }> | undefined,\n mapFn: (v: { type: \"up\" | \"down\"; detail: string }) => T,\n filterFn?: (v: { type: \"up\" | \"down\"; detail: string }) => boolean,\n): Record<string, T> {\n if (!init) return {}\n const entries = Object.entries(init)\n const filtered = filterFn ? entries.filter(([, v]) => filterFn(v)) : entries\n return Object.fromEntries(filtered.map(([k, v]) => [k, mapFn(v)]))\n}\n\nfunction ScoreBreakdown({ factors, onFactorFeedback, className, initialFeedback }: ScoreBreakdownProps) {\n const [feedback, setFeedback] = React.useState<Record<string, \"up\" | \"down\" | null>>(\n () => deriveInitialState(initialFeedback, (v) => v.type)\n )\n const [feedbackText, setFeedbackText] = React.useState<Record<string, string>>(\n () => deriveInitialState(initialFeedback, (v) => v.detail)\n )\n const [savedText, setSavedText] = React.useState<Record<string, string>>(\n () => deriveInitialState(initialFeedback, (v) => v.detail, (v) => !!v.detail)\n )\n const [editingKey, setEditingKey] = React.useState<string | null>(null)\n\n // Sync state when initialFeedback prop changes (e.g. async hydration).\n // Skip keys the user is actively editing to avoid clobbering in-progress input.\n React.useEffect(() => {\n setFeedback((prev) => {\n const next: Record<string, \"up\" | \"down\" | null> = deriveInitialState(initialFeedback, (v) => v.type)\n // Preserve any key the user is currently editing\n if (editingKey && prev[editingKey] !== undefined) {\n next[editingKey] = prev[editingKey]\n }\n return next\n })\n setFeedbackText((prev) => {\n const next = deriveInitialState(initialFeedback, (v) => v.detail)\n if (editingKey && prev[editingKey] !== undefined) {\n next[editingKey] = prev[editingKey]\n }\n return next\n })\n setSavedText((prev) => {\n const next = deriveInitialState(initialFeedback, (v) => v.detail, (v) => !!v.detail)\n if (editingKey && prev[editingKey] !== undefined) {\n next[editingKey] = prev[editingKey]\n }\n return next\n })\n }, [initialFeedback]) // eslint-disable-line react-hooks/exhaustive-deps -- reads editingKey as guard, not trigger\n\n const handleFeedback = (factorKey: string, type: \"up\" | \"down\") => {\n const newState = feedback[factorKey] === type ? null : type\n setFeedback((prev) => ({ ...prev, [factorKey]: newState }))\n if (newState === null) {\n setEditingKey(null)\n onFactorFeedback?.(factorKey, null)\n } else {\n setEditingKey(factorKey)\n onFactorFeedback?.(factorKey, newState)\n }\n }\n\n const submitFeedbackText = (factorKey: string) => {\n const text = (feedbackText[factorKey] ?? \"\").trim()\n if (feedback[factorKey]) {\n onFactorFeedback?.(factorKey, feedback[factorKey]!, text ?? \"\")\n if (text) {\n setSavedText((prev) => ({ ...prev, [factorKey]: text }))\n }\n }\n setEditingKey(null)\n }\n\n return (\n <div className={cn(\"border border-border rounded-lg overflow-hidden divide-y divide-border/40\", className)}>\n {factors.map((factor) => {\n const feedbackState = feedback[factor.key]\n const hasThumb = feedbackState === \"up\" || feedbackState === \"down\"\n const isEditing = editingKey === factor.key\n const saved = savedText[factor.key]\n\n return (\n <div key={factor.key} className=\"px-3 py-3 space-y-1.5\">\n {/* Row 1: Factor name + score + rating buttons */}\n <div className=\"flex items-center gap-2\">\n <span className=\"text-[13px] font-semibold text-foreground flex-1\">{factor.label}</span>\n\n {factor.score !== null ? (\n <span className=\"text-xs font-bold text-foreground tabular-nums\">{factor.score}<span className=\"text-muted-foreground font-normal\">/100</span></span>\n ) : factor.risk ? (\n <span\n className={cn(\n \"text-[11px] font-semibold px-1.5 py-0.5 rounded\",\n getRiskBadgeStyle(factor.risk)\n )}\n >\n {factor.risk}\n </span>\n ) : null}\n\n <div className=\"flex items-center gap-0.5 ml-1\">\n <button\n type=\"button\"\n onClick={() => handleFeedback(factor.key, \"up\")}\n className={cn(\n \"p-1 rounded transition-colors\",\n feedbackState === \"up\"\n ? \"text-emerald-600 bg-emerald-50 dark:text-emerald-400 dark:bg-emerald-950/30\"\n : \"text-muted-foreground/40 hover:text-emerald-600 hover:bg-emerald-50/50\"\n )}\n title=\"This factor is accurate\"\n >\n <ThumbsUp className=\"w-3 h-3\" />\n </button>\n <button\n type=\"button\"\n onClick={() => handleFeedback(factor.key, \"down\")}\n className={cn(\n \"p-1 rounded transition-colors\",\n feedbackState === \"down\"\n ? \"text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-950/30\"\n : \"text-muted-foreground/40 hover:text-red-600 hover:bg-red-50/50\"\n )}\n title=\"Report issue with this factor\"\n >\n <ThumbsDown className=\"w-3 h-3\" />\n </button>\n </div>\n </div>\n\n {/* Row 2: Score bar */}\n {factor.score !== null && (\n <div className=\"w-full h-1 bg-muted rounded-full overflow-hidden\">\n <div\n className={cn(\"h-full rounded-full\", getFactorBarColor(factor.score))}\n style={{ width: `${factor.score}%` }}\n />\n </div>\n )}\n\n {/* Row 3: Why text */}\n <p className=\"text-xs text-muted-foreground leading-relaxed\">{factor.why}</p>\n\n {/* Feedback input */}\n {hasThumb && isEditing && (\n <input\n type=\"text\"\n autoFocus\n value={feedbackText[factor.key] ?? \"\"}\n onChange={(e) =>\n setFeedbackText((prev) => ({ ...prev, [factor.key]: e.target.value }))\n }\n onKeyDown={(e) => {\n if (e.key === \"Enter\") submitFeedbackText(factor.key)\n if (e.key === \"Escape\") setEditingKey(null)\n }}\n onBlur={() => submitFeedbackText(factor.key)}\n placeholder={\n feedbackState === \"up\"\n ? \"What\\u2019s accurate? (optional)\"\n : \"What\\u2019s wrong? (optional)\"\n }\n className=\"w-full h-6 rounded border border-border bg-background px-2 text-[11px] text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring\"\n />\n )}\n {hasThumb && !isEditing && saved && (\n <button\n type=\"button\"\n onClick={() => {\n setFeedbackText((prev) => ({ ...prev, [factor.key]: saved }))\n setEditingKey(factor.key)\n }}\n className=\"text-[11px] text-muted-foreground/70 hover:text-muted-foreground transition-colors text-left leading-snug\"\n >\n {saved}\n </button>\n )}\n </div>\n )\n })}\n </div>\n )\n}\n\nexport { ScoreBreakdown }\nexport type { ScoreBreakdownProps }\n"],"mappings":"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","names":[]}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { ScoreIntent } from './score-semantics.js';
|
|
3
2
|
|
|
4
|
-
declare function getScoreColor(score: number, denominator: number
|
|
5
|
-
declare function getScoreTrackColor(score: number, denominator: number, intent?: ScoreIntent): string;
|
|
3
|
+
declare function getScoreColor(score: number, denominator: number): "text-emerald-500" | "text-amber-500" | "text-red-500";
|
|
6
4
|
interface ScoreRingProps {
|
|
7
5
|
score: number;
|
|
8
6
|
denominator?: number;
|
|
@@ -10,8 +8,7 @@ interface ScoreRingProps {
|
|
|
10
8
|
strokeWidth?: number;
|
|
11
9
|
className?: string;
|
|
12
10
|
showLabel?: boolean;
|
|
13
|
-
intent?: ScoreIntent;
|
|
14
11
|
}
|
|
15
|
-
declare function ScoreRing({ score, denominator, size, strokeWidth, className, showLabel,
|
|
12
|
+
declare function ScoreRing({ score, denominator, size, strokeWidth, className, showLabel, }: ScoreRingProps): React.JSX.Element;
|
|
16
13
|
|
|
17
|
-
export { ScoreRing, type ScoreRingProps, getScoreColor
|
|
14
|
+
export { ScoreRing, type ScoreRingProps, getScoreColor };
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../lib/utils.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
function getScoreColor(score, denominator) {
|
|
4
|
+
const pct = score / denominator * 100;
|
|
5
|
+
if (pct >= 70) return "text-emerald-500";
|
|
6
|
+
if (pct >= 40) return "text-amber-500";
|
|
7
|
+
return "text-red-500";
|
|
6
8
|
}
|
|
7
|
-
function getScoreTrackColor(score, denominator
|
|
8
|
-
|
|
9
|
+
function getScoreTrackColor(score, denominator) {
|
|
10
|
+
const pct = score / denominator * 100;
|
|
11
|
+
if (pct >= 70) return "text-emerald-500/15";
|
|
12
|
+
if (pct >= 40) return "text-amber-500/15";
|
|
13
|
+
return "text-red-500/15";
|
|
9
14
|
}
|
|
10
15
|
function ScoreRing({
|
|
11
16
|
score,
|
|
@@ -13,8 +18,7 @@ function ScoreRing({
|
|
|
13
18
|
size = 120,
|
|
14
19
|
strokeWidth = 10,
|
|
15
20
|
className,
|
|
16
|
-
showLabel = true
|
|
17
|
-
intent = "positive"
|
|
21
|
+
showLabel = true
|
|
18
22
|
}) {
|
|
19
23
|
const radius = (size - strokeWidth) / 2;
|
|
20
24
|
const circumference = 2 * Math.PI * radius;
|
|
@@ -38,7 +42,7 @@ function ScoreRing({
|
|
|
38
42
|
fill: "none",
|
|
39
43
|
stroke: "currentColor",
|
|
40
44
|
strokeWidth,
|
|
41
|
-
className: getScoreTrackColor(score, denominator
|
|
45
|
+
className: getScoreTrackColor(score, denominator)
|
|
42
46
|
}
|
|
43
47
|
),
|
|
44
48
|
/* @__PURE__ */ jsx(
|
|
@@ -53,7 +57,7 @@ function ScoreRing({
|
|
|
53
57
|
strokeLinecap: "round",
|
|
54
58
|
strokeDasharray: circumference,
|
|
55
59
|
strokeDashoffset: offset,
|
|
56
|
-
className: cn("transition-all duration-500", getScoreColor(score, denominator
|
|
60
|
+
className: cn("transition-all duration-500", getScoreColor(score, denominator))
|
|
57
61
|
}
|
|
58
62
|
)
|
|
59
63
|
]
|
|
@@ -70,7 +74,6 @@ function ScoreRing({
|
|
|
70
74
|
}
|
|
71
75
|
export {
|
|
72
76
|
ScoreRing,
|
|
73
|
-
getScoreColor
|
|
74
|
-
getScoreTrackColor
|
|
77
|
+
getScoreColor
|
|
75
78
|
};
|
|
76
79
|
//# sourceMappingURL=score-ring.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/score-ring.tsx"],"sourcesContent":["import * as React from \"react\"\nimport { cn } from \"../lib/utils\"\
|
|
1
|
+
{"version":3,"sources":["../../src/components/score-ring.tsx"],"sourcesContent":["import * as React from \"react\"\nimport { cn } from \"../lib/utils\"\n\nfunction getScoreColor(score: number, denominator: number) {\n const pct = (score / denominator) * 100\n if (pct >= 70) return \"text-emerald-500\"\n if (pct >= 40) return \"text-amber-500\"\n return \"text-red-500\"\n}\n\nfunction getScoreTrackColor(score: number, denominator: number) {\n const pct = (score / denominator) * 100\n if (pct >= 70) return \"text-emerald-500/15\"\n if (pct >= 40) return \"text-amber-500/15\"\n return \"text-red-500/15\"\n}\n\ninterface ScoreRingProps {\n score: number\n denominator?: number\n size?: number\n strokeWidth?: number\n className?: string\n showLabel?: boolean\n}\n\nfunction ScoreRing({\n score,\n denominator = 100,\n size = 120,\n strokeWidth = 10,\n className,\n showLabel = true,\n}: ScoreRingProps) {\n const radius = (size - strokeWidth) / 2\n const circumference = 2 * Math.PI * radius\n const pct = Math.min(score / denominator, 1)\n const offset = circumference * (1 - pct)\n\n return (\n <div className={cn(\"relative inline-flex items-center justify-center\", className)}>\n <svg\n width={size}\n height={size}\n viewBox={`0 0 ${size} ${size}`}\n className=\"-rotate-90\"\n >\n {/* Track */}\n <circle\n cx={size / 2}\n cy={size / 2}\n r={radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={strokeWidth}\n className={getScoreTrackColor(score, denominator)}\n />\n {/* Fill */}\n <circle\n cx={size / 2}\n cy={size / 2}\n r={radius}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeDasharray={circumference}\n strokeDashoffset={offset}\n className={cn(\"transition-all duration-500\", getScoreColor(score, denominator))}\n />\n </svg>\n {showLabel && (\n <div className=\"absolute inset-0 flex flex-col items-center justify-center\">\n <span className=\"text-2xl font-bold text-foreground leading-none\">\n {score}\n </span>\n <span className=\"text-xs text-muted-foreground mt-0.5\">\n /{denominator}\n </span>\n </div>\n )}\n </div>\n )\n}\n\nexport { ScoreRing, getScoreColor }\nexport type { ScoreRingProps }\n"],"mappings":"AAyCM,SAOE,KAPF;AAxCN,SAAS,UAAU;AAEnB,SAAS,cAAc,OAAe,aAAqB;AACzD,QAAM,MAAO,QAAQ,cAAe;AACpC,MAAI,OAAO,GAAI,QAAO;AACtB,MAAI,OAAO,GAAI,QAAO;AACtB,SAAO;AACT;AAEA,SAAS,mBAAmB,OAAe,aAAqB;AAC9D,QAAM,MAAO,QAAQ,cAAe;AACpC,MAAI,OAAO,GAAI,QAAO;AACtB,MAAI,OAAO,GAAI,QAAO;AACtB,SAAO;AACT;AAWA,SAAS,UAAU;AAAA,EACjB;AAAA,EACA,cAAc;AAAA,EACd,OAAO;AAAA,EACP,cAAc;AAAA,EACd;AAAA,EACA,YAAY;AACd,GAAmB;AACjB,QAAM,UAAU,OAAO,eAAe;AACtC,QAAM,gBAAgB,IAAI,KAAK,KAAK;AACpC,QAAM,MAAM,KAAK,IAAI,QAAQ,aAAa,CAAC;AAC3C,QAAM,SAAS,iBAAiB,IAAI;AAEpC,SACE,qBAAC,SAAI,WAAW,GAAG,oDAAoD,SAAS,GAC9E;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,SAAS,OAAO,IAAI,IAAI,IAAI;AAAA,QAC5B,WAAU;AAAA,QAGV;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,IAAI,OAAO;AAAA,cACX,IAAI,OAAO;AAAA,cACX,GAAG;AAAA,cACH,MAAK;AAAA,cACL,QAAO;AAAA,cACP;AAAA,cACA,WAAW,mBAAmB,OAAO,WAAW;AAAA;AAAA,UAClD;AAAA,UAEA;AAAA,YAAC;AAAA;AAAA,cACC,IAAI,OAAO;AAAA,cACX,IAAI,OAAO;AAAA,cACX,GAAG;AAAA,cACH,MAAK;AAAA,cACL,QAAO;AAAA,cACP;AAAA,cACA,eAAc;AAAA,cACd,iBAAiB;AAAA,cACjB,kBAAkB;AAAA,cAClB,WAAW,GAAG,+BAA+B,cAAc,OAAO,WAAW,CAAC;AAAA;AAAA,UAChF;AAAA;AAAA;AAAA,IACF;AAAA,IACC,aACC,qBAAC,SAAI,WAAU,8DACb;AAAA,0BAAC,UAAK,WAAU,mDACb,iBACH;AAAA,MACA,qBAAC,UAAK,WAAU,wCAAuC;AAAA;AAAA,QACnD;AAAA,SACJ;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -4,7 +4,6 @@ import './feedback-primitives.js';
|
|
|
4
4
|
import './quick-action-sidebar-nav.js';
|
|
5
5
|
import './quick-action-modal.js';
|
|
6
6
|
import './score-breakdown.js';
|
|
7
|
-
import './score-semantics.js';
|
|
8
7
|
import './suggested-actions.js';
|
|
9
8
|
import './detail-view.js';
|
|
10
9
|
import './inbox-toolbar.js';
|
|
@@ -19,9 +18,9 @@ import 'lucide-react';
|
|
|
19
18
|
|
|
20
19
|
declare function getSignalScoreUrgencyLabel(score: number, providedLabel?: SignalScoreUrgencyLabel): SignalScoreUrgencyLabel;
|
|
21
20
|
declare function scoreRangeForUrgency(label: SignalScoreUrgencyLabel): string;
|
|
22
|
-
/** Shared tone-to-class map. Re-exported for
|
|
21
|
+
/** Shared tone-to-class map. Re-exported for signal-priority-popover. */
|
|
23
22
|
declare const SIGNAL_TONE_CLASSES: Record<string, string>;
|
|
24
|
-
/** Default tone for missing/unknown tone values
|
|
23
|
+
/** Default tone for missing/unknown tone values */
|
|
25
24
|
declare const DEFAULT_TONE_CLASS = "bg-muted text-muted-foreground";
|
|
26
25
|
interface ScoreWhyChipsProps {
|
|
27
26
|
item: QueueItem;
|
|
@@ -17,17 +17,24 @@ import {
|
|
|
17
17
|
} from "lucide-react";
|
|
18
18
|
import { FeedbackFooter } from "./feedback-primitives.js";
|
|
19
19
|
import { cn } from "../lib/utils.js";
|
|
20
|
-
import {
|
|
21
|
-
DEFAULT_SCORE_TONE_CLASS,
|
|
22
|
-
SCORE_TONE_CLASSES,
|
|
23
|
-
getUrgencyLevel,
|
|
24
|
-
getUrgencyRange
|
|
25
|
-
} from "./score-semantics.js";
|
|
26
20
|
function getSignalScoreUrgencyLabel(score, providedLabel) {
|
|
27
|
-
|
|
21
|
+
if (providedLabel) return providedLabel;
|
|
22
|
+
if (score >= 80) return "Urgent";
|
|
23
|
+
if (score >= 60) return "High";
|
|
24
|
+
if (score >= 35) return "Medium";
|
|
25
|
+
return "Low";
|
|
28
26
|
}
|
|
29
27
|
function scoreRangeForUrgency(label) {
|
|
30
|
-
|
|
28
|
+
switch (label) {
|
|
29
|
+
case "Urgent":
|
|
30
|
+
return "80-100";
|
|
31
|
+
case "High":
|
|
32
|
+
return "60-79";
|
|
33
|
+
case "Medium":
|
|
34
|
+
return "35-59";
|
|
35
|
+
case "Low":
|
|
36
|
+
return "0-34";
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
function makeDomId(...parts) {
|
|
33
40
|
return parts.filter((part) => Boolean(part)).join("-").replace(/[^A-Za-z0-9_-]+/g, "-");
|
|
@@ -63,8 +70,12 @@ function resolveIcon(iconName) {
|
|
|
63
70
|
if (!iconName) return Activity;
|
|
64
71
|
return (_a = SIGNAL_TYPE_ICONS[iconName]) != null ? _a : Activity;
|
|
65
72
|
}
|
|
66
|
-
const SIGNAL_TONE_CLASSES =
|
|
67
|
-
|
|
73
|
+
const SIGNAL_TONE_CLASSES = {
|
|
74
|
+
alert: "bg-red-50 text-red-600",
|
|
75
|
+
warn: "bg-amber-50 text-amber-600",
|
|
76
|
+
info: "bg-blue-50 text-blue-600"
|
|
77
|
+
};
|
|
78
|
+
const DEFAULT_TONE_CLASS = "bg-muted text-muted-foreground";
|
|
68
79
|
function slotValue(value) {
|
|
69
80
|
return value && value.trim().length > 0 ? value : "";
|
|
70
81
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/score-why-chips.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport {\n ChevronDown,\n ChevronUp,\n ChevronRight,\n X,\n TrendingDown,\n ArrowUpRight,\n Radar,\n ArrowDownLeft,\n GitMerge,\n Activity,\n} from \"lucide-react\"\nimport type { LucideIcon } from \"lucide-react\"\nimport { FeedbackFooter } from \"./feedback-primitives\"\nimport type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from \"./feedback-primitives\"\nimport { cn } from \"../lib/utils\"\nimport {\n DEFAULT_SCORE_TONE_CLASS,\n SCORE_TONE_CLASSES,\n getUrgencyLevel,\n getUrgencyRange,\n} from \"./score-semantics\"\nimport type {\n QueueItem,\n SignalScoreData,\n SignalScoreExplanationBucket,\n SignalScoreExplanationSignal,\n SignalScoreUrgencyLabel,\n} from \"../prototype/prototype-config\"\n\n// ---------------------------------------------------------------------------\n// Constants & helpers\n// ---------------------------------------------------------------------------\n\nexport function getSignalScoreUrgencyLabel(\n score: number,\n providedLabel?: SignalScoreUrgencyLabel,\n): SignalScoreUrgencyLabel {\n return providedLabel ?? getUrgencyLevel(score)\n}\n\nexport function scoreRangeForUrgency(label: SignalScoreUrgencyLabel): string {\n return getUrgencyRange(label)\n}\n\nfunction makeDomId(...parts: Array<string | undefined>): string {\n return parts\n .filter((part): part is string => Boolean(part))\n .join(\"-\")\n .replace(/[^A-Za-z0-9_-]+/g, \"-\")\n}\n\nfunction bucketHasSignalRows(bucket: SignalScoreExplanationBucket): boolean {\n return (\n (bucket.signals?.length ?? 0) > 0 ||\n (bucket.signalIds?.length ?? 0) > 0 ||\n Boolean(bucket.primarySignalId)\n )\n}\n\nfunction getSignalScoreBuckets(signalData: SignalScoreData): SignalScoreExplanationBucket[] {\n return (signalData.explanationBuckets ?? []).filter(\n (bucket) => bucket.kind !== \"factor\" && bucketHasSignalRows(bucket),\n )\n}\n\nfunction getBucketSignals(bucket: SignalScoreExplanationBucket): SignalScoreExplanationSignal[] {\n if (bucket.signals && bucket.signals.length > 0) return bucket.signals\n\n const signalIds = bucket.signalIds && bucket.signalIds.length > 0 ? bucket.signalIds : bucket.primarySignalId ? [bucket.primarySignalId] : []\n const uniqueSignalIds = Array.from(new Set(signalIds))\n\n return uniqueSignalIds.map((signalId) => ({\n id: signalId,\n label: `${bucket.label} signal`,\n }))\n}\n\n// ---------------------------------------------------------------------------\n// Signal type icon map - keyed by signal type name (Tailwind v4 source scanned)\n// ---------------------------------------------------------------------------\n\nconst SIGNAL_TYPE_ICONS: Record<string, LucideIcon> = {\n treasury_liquidation: TrendingDown,\n cumulative_treasury_outflow: ArrowUpRight,\n test_transaction: Radar,\n micro_deposit: ArrowDownLeft,\n combined_signal: GitMerge,\n}\n\nfunction resolveIcon(iconName?: string): LucideIcon {\n if (!iconName) return Activity\n return SIGNAL_TYPE_ICONS[iconName] ?? Activity\n}\n\n// ---------------------------------------------------------------------------\n// Static tone class maps (REQUIRED for Tailwind v4 source scanning)\n// ---------------------------------------------------------------------------\n\n/** Shared tone-to-class map. Re-exported for backward compatibility. */\nexport const SIGNAL_TONE_CLASSES: Record<string, string> = SCORE_TONE_CLASSES\n\n/** Default tone for missing/unknown tone values. Re-exported for backward compatibility. */\nexport const DEFAULT_TONE_CLASS = DEFAULT_SCORE_TONE_CLASS\n\n// ---------------------------------------------------------------------------\n// Em-dash fallback for missing slot data\n// ---------------------------------------------------------------------------\n\nfunction slotValue(value: string | null | undefined): string {\n return value && value.trim().length > 0 ? value : \"\"\n}\n\n// ---------------------------------------------------------------------------\n// Bucket feedback chip config\n// ---------------------------------------------------------------------------\n\nconst BUCKET_NEGATIVE_CHIPS: FeedbackChipTree[] = [\n {\n label: \"Not relevant for this account\",\n subPrompt: \"Why isn't it relevant?\",\n subChips: [\n \"Business as usual for this account\",\n \"Account in maintenance mode\",\n \"Wrong contact for this signal\",\n \"Other\",\n ],\n },\n { label: \"Bad timing\" },\n {\n label: \"Inaccurate data\",\n subPrompt: \"Which field?\",\n subChips: [\"Balance figures\", \"Counterparty\", \"Timestamp\", \"Other\"],\n },\n { label: \"Wrong account\" },\n { label: \"Already handled\" },\n { label: \"Other\" },\n]\n\n// ---------------------------------------------------------------------------\n// Default visible row count for long lists\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_VISIBLE_ROWS = 8\n\n// ---------------------------------------------------------------------------\n// WhyPill - Bucket toggle button with icon, count badge, chevron, close\n// ---------------------------------------------------------------------------\n\ninterface WhyPillProps {\n bucket: SignalScoreExplanationBucket\n isSelected: boolean\n signalCount: number\n panelId: string\n onToggle: () => void\n onClose: () => void\n}\n\nfunction WhyPill({ bucket, isSelected, signalCount, panelId, onToggle, onClose }: WhyPillProps) {\n const IconComponent = resolveIcon(bucket.icon)\n\n const sharedClasses = cn(\n \"inline-flex items-center text-[11px] font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n isSelected\n ? \"border-border bg-muted text-foreground\"\n : \"border-border bg-background text-muted-foreground hover:bg-muted/60 hover:text-foreground\",\n )\n\n return (\n <div className=\"inline-flex h-[26px] items-stretch\">\n <button\n type=\"button\"\n onClick={onToggle}\n aria-expanded={isSelected}\n aria-controls={panelId}\n className={cn(\n sharedClasses,\n \"gap-1.5 rounded-lg border px-2.5 py-1\",\n isSelected && \"rounded-b-none rounded-r-none border-r-0\",\n )}\n >\n <IconComponent className=\"h-3 w-3 shrink-0\" />\n {bucket.label}\n {signalCount > 1 && (\n <span className={cn(\"rounded-full px-1.5 py-0 text-[10px]\", isSelected ? \"bg-background/60\" : \"bg-muted\")}>\n x{signalCount}\n </span>\n )}\n {isSelected ? (\n <ChevronUp className=\"h-3 w-3 shrink-0\" />\n ) : (\n <ChevronDown className=\"h-3 w-3 shrink-0\" />\n )}\n </button>\n {isSelected && (\n <button\n type=\"button\"\n aria-label={`Close ${bucket.label}`}\n onClick={onClose}\n className={cn(\n sharedClasses,\n \"rounded-lg rounded-b-none rounded-l-none border border-l-0 border-border px-1.5 py-1 hover:bg-background/60\",\n )}\n >\n <X className=\"h-3 w-3\" />\n </button>\n )}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// CombinedSignalMiniChips - renders component type chips for combined signals\n// ---------------------------------------------------------------------------\n\ninterface CombinedSignalMiniChipsProps {\n components: Array<{ type: string; count: number }>\n}\n\nfunction CombinedSignalMiniChips({ components }: CombinedSignalMiniChipsProps) {\n return (\n <div className=\"flex flex-wrap items-center gap-1\">\n {components.map((comp, idx) => {\n const CompIcon = resolveIcon(comp.type)\n return (\n <React.Fragment key={comp.type}>\n {idx > 0 && <span className=\"text-[10px] text-muted-foreground/60\">+</span>}\n <span className=\"inline-flex items-center gap-0.5 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground\">\n <CompIcon className=\"h-2.5 w-2.5 shrink-0\" />\n {comp.type.replace(/_/g, \" \")} x{comp.count}\n </span>\n </React.Fragment>\n )\n })}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// StructuredSignalRow - CSS grid slot grammar signal row\n// ---------------------------------------------------------------------------\n\ninterface StructuredSignalRowProps {\n item: QueueItem\n bucketKey: string\n signal: SignalScoreExplanationSignal\n tone?: \"alert\" | \"warn\" | \"info\"\n onOpenSignalBucket?: ScoreWhyChipsProps[\"onOpenSignalBucket\"]\n}\n\nfunction StructuredSignalRow({ item, bucketKey, signal, tone, onOpenSignalBucket }: StructuredSignalRowProps) {\n const IconComponent = resolveIcon(signal.signalTypeName)\n const toneClass = tone ? (SIGNAL_TONE_CLASSES[tone] ?? DEFAULT_TONE_CLASS) : DEFAULT_TONE_CLASS\n const isCombined = signal.signalTypeName === \"combined_signal\" && signal.components && signal.components.length > 0\n const hasBalance = Boolean(signal.currentBalance || signal.balanceContext)\n\n const rowContent = (\n <>\n {/* Slot 1: Icon */}\n <div className={cn(\"flex h-5 w-5 shrink-0 items-center justify-center rounded\", toneClass)}>\n <IconComponent className=\"h-3 w-3\" />\n </div>\n\n {/* Slot 2: Primary value + qualifier */}\n <div className=\"min-w-0\">\n {isCombined ? (\n <CombinedSignalMiniChips components={signal.components!} />\n ) : (\n <div className=\"flex items-baseline gap-1.5\">\n <span className=\"text-sm font-semibold tabular-nums text-foreground\">\n {slotValue(signal.primaryValue)}\n </span>\n <span className=\"text-xs text-muted-foreground\">\n {slotValue(signal.qualifier)}\n </span>\n </div>\n )}\n </div>\n\n {/* Slot 3: Counterparty */}\n <div className=\"min-w-0\">\n <span className=\"block truncate text-xs text-muted-foreground\">\n {slotValue(signal.counterparty)}\n </span>\n </div>\n\n {/* Slot 4: Time */}\n <span className=\"shrink-0 text-[11px] text-muted-foreground/70\">\n {slotValue(signal.time)}\n </span>\n\n {/* Slot 5: Chevron */}\n <ChevronRight className=\"h-3 w-3 shrink-0 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5 group-hover:text-foreground/50\" />\n\n {/* Balance context strip — spans full row below grid columns */}\n {hasBalance && (\n <div\n className=\"col-span-full mt-0.5 text-[10px] text-muted-foreground/70\"\n data-testid=\"balance-context-strip\"\n >\n {signal.currentBalance && (\n <span>\n Current balance <span className=\"font-medium text-muted-foreground\">{signal.currentBalance}</span>\n </span>\n )}\n {signal.balanceContext && (\n <span>\n {signal.currentBalance ? \" · \" : \"\"}\n {signal.balanceContext}\n </span>\n )}\n </div>\n )}\n </>\n )\n\n if (signal.id && onOpenSignalBucket) {\n return (\n <button\n type=\"button\"\n className=\"group grid w-full cursor-pointer items-center gap-x-3 gap-y-1 rounded-md px-3 py-2 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n style={{ gridTemplateColumns: \"20px minmax(0,1fr) minmax(0,1fr) auto 16px\" }}\n onClick={() => onOpenSignalBucket({ item, bucketKey, signalId: signal.id! })}\n >\n {rowContent}\n </button>\n )\n }\n\n return (\n <div\n className=\"grid w-full items-center gap-x-3 gap-y-1 rounded-md px-3 py-2\"\n style={{ gridTemplateColumns: \"20px minmax(0,1fr) minmax(0,1fr) auto 16px\" }}\n >\n {rowContent}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// Legacy SignalRow (for signals without structured data)\n// ---------------------------------------------------------------------------\n\ninterface SignalRowProps {\n item: QueueItem\n bucketKey: string\n signal: SignalScoreExplanationSignal\n onOpenSignalBucket?: ScoreWhyChipsProps[\"onOpenSignalBucket\"]\n}\n\nfunction LegacySignalRow({ item, bucketKey, signal, onOpenSignalBucket }: SignalRowProps) {\n const isClickable = !!(signal.id && onOpenSignalBucket)\n const rowContent = (\n <>\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-medium text-foreground\">{signal.label}</p>\n {signal.description ? <p className=\"mt-1 leading-relaxed text-muted-foreground\">{signal.description}</p> : null}\n {(signal.source || signal.metric) && (\n <div className=\"mt-1.5 flex flex-wrap gap-1.5 text-[11px] text-muted-foreground/80\">\n {signal.source ? <span className=\"rounded-full bg-muted px-2 py-0.5\">{signal.source}</span> : null}\n {signal.metric ? <span className=\"rounded-full bg-muted px-2 py-0.5\">{signal.metric}</span> : null}\n </div>\n )}\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {signal.time ? <span className=\"text-[11px] text-muted-foreground/70\">{signal.time}</span> : null}\n {isClickable && (\n <ChevronRight className=\"h-3 w-3 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5 group-hover:text-foreground/50\" />\n )}\n </div>\n </div>\n </>\n )\n\n if (isClickable) {\n return (\n <button\n type=\"button\"\n className=\"group w-full cursor-pointer rounded-md bg-background/80 p-3 text-left text-xs transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n onClick={() => onOpenSignalBucket!({ item, bucketKey, signalId: signal.id! })}\n >\n {rowContent}\n </button>\n )\n }\n\n return <div className=\"rounded-md bg-background/80 p-3 text-xs\">{rowContent}</div>\n}\n\n/**\n * Determine whether a signal has structured slot data.\n * If it has primaryValue, counterparty, qualifier, or components, use the structured row.\n */\nfunction hasStructuredData(signal: SignalScoreExplanationSignal): boolean {\n return Boolean(\n signal.primaryValue ||\n signal.qualifier ||\n signal.counterparty ||\n (signal.components && signal.components.length > 0),\n )\n}\n\n// ---------------------------------------------------------------------------\n// WhyCard - Expanded panel under a pill\n// ---------------------------------------------------------------------------\n\ninterface WhyCardProps {\n bucket: SignalScoreExplanationBucket\n signals: SignalScoreExplanationSignal[]\n item: QueueItem\n panelId: string\n onOpenSignalBucket?: ScoreWhyChipsProps[\"onOpenSignalBucket\"]\n onBucketFeedback?: (bucketKey: string, data: FeedbackSubmitData) => void\n /** Persisted bucket-level feedback to hydrate from. */\n initialBucketFeedback?: PersistedFeedbackData | null\n}\n\nfunction WhyCard({ bucket, signals, item, panelId, onOpenSignalBucket, onBucketFeedback, initialBucketFeedback }: WhyCardProps) {\n const [showAll, setShowAll] = React.useState(false)\n const [bucketFeedback, setBucketFeedback] = React.useState<\"positive\" | \"negative\" | null>(null)\n const totalCount = bucket.signalCount ?? signals.length\n const visibleSignals = showAll ? signals : signals.slice(0, DEFAULT_VISIBLE_ROWS)\n const hiddenCount = signals.length - DEFAULT_VISIBLE_ROWS\n\n // Determine whether to use structured rows (any signal has structured data)\n const useStructured = signals.some(hasStructuredData)\n\n return (\n <div\n id={panelId}\n className=\"rounded-lg rounded-t-none border border-t-0 border-border bg-muted/20 p-3\"\n role=\"region\"\n aria-label={`${bucket.label} details`}\n >\n {/* Card header */}\n <div className=\"mb-2 flex items-center justify-between\">\n <span className=\"text-[10px] font-bold uppercase tracking-wider text-muted-foreground\">\n {totalCount} signal{totalCount !== 1 ? \"s\" : \"\"} – {bucket.label}\n </span>\n </div>\n\n {/* Signal rows */}\n {visibleSignals.length > 0 ? (\n <ul className=\"divide-y divide-border/30\" aria-label=\"Matching signals\">\n {visibleSignals.map((signal, index) => (\n <li key={signal.id ?? `${bucket.key}-signal-${index}`}>\n {useStructured ? (\n <StructuredSignalRow\n item={item}\n bucketKey={bucket.key}\n signal={signal}\n tone={bucket.tone}\n onOpenSignalBucket={onOpenSignalBucket}\n />\n ) : (\n <LegacySignalRow\n item={item}\n bucketKey={bucket.key}\n signal={signal}\n onOpenSignalBucket={onOpenSignalBucket}\n />\n )}\n </li>\n ))}\n </ul>\n ) : bucket.evidence && bucket.evidence.length > 0 ? (\n <ul className=\"mt-3 space-y-1.5\" aria-label=\"Matching signals\">\n {bucket.evidence.map((evidence, index) => (\n <li key={`${bucket.key}-evidence-${index}`} className=\"flex gap-2 text-xs text-muted-foreground\">\n <span className=\"mt-1.5 h-1 w-1 shrink-0 rounded-full bg-primary\" />\n <span className=\"leading-relaxed\">{evidence}</span>\n </li>\n ))}\n </ul>\n ) : null}\n\n {/* \"Show N more\" button */}\n {!showAll && hiddenCount > 0 && (\n <button\n type=\"button\"\n onClick={() => setShowAll(true)}\n className=\"mt-2 flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground\"\n >\n <ChevronDown className=\"h-3 w-3\" />\n Show {hiddenCount} more\n </button>\n )}\n\n {/* Bucket feedback footer */}\n {onBucketFeedback && (\n <div className=\"mt-3 border-t border-border/40 pt-3\">\n <FeedbackFooter\n feedback={bucketFeedback}\n onFeedbackChange={setBucketFeedback}\n onSubmit={(data) => onBucketFeedback(bucket.key, data)}\n negativeChips={BUCKET_NEGATIVE_CHIPS}\n negativePrompt=\"Was this bucket useful?\"\n positivePrompt=\"Thanks! What was useful about this bucket?\"\n initialFeedback={initialBucketFeedback}\n feedbackKey={bucket.key}\n />\n </div>\n )}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// ScoreWhyChips - Main export\n// ---------------------------------------------------------------------------\n\nexport interface ScoreWhyChipsProps {\n item: QueueItem\n signalData: SignalScoreData\n onOpenSignalBucket?: (args: { item: QueueItem; bucketKey: string; signalId: string }) => void\n className?: string\n}\n\nexport function ScoreWhyChips({\n item,\n signalData,\n onOpenSignalBucket,\n className,\n}: ScoreWhyChipsProps) {\n const [selectedBucketKey, setSelectedBucketKey] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n setSelectedBucketKey(null)\n }, [item.id])\n\n const reactId = React.useId()\n const idPrefix = makeDomId(\"score-why\", reactId, item.id)\n const buckets = React.useMemo(() => getSignalScoreBuckets(signalData), [signalData])\n const selectedBucket = buckets.find((bucket) => bucket.key === selectedBucketKey) ?? null\n const selectedBucketSignals = selectedBucket ? getBucketSignals(selectedBucket) : []\n const selectedPanelId = selectedBucket ? `${idPrefix}-panel-${makeDomId(selectedBucket.key)}` : undefined\n\n if (buckets.length === 0) return null\n\n return (\n <div className={cn(\"mt-4\", className)}>\n <div className=\"mb-2 flex items-center gap-2\">\n <span className=\"text-[10px] font-bold uppercase tracking-wider text-muted-foreground\">Why</span>\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {buckets.map((bucket) => {\n const isSelected = selectedBucketKey === bucket.key\n const panelId = `${idPrefix}-panel-${makeDomId(bucket.key)}`\n const signals = getBucketSignals(bucket)\n const signalCount = bucket.signalCount ?? signals.length\n return (\n <div key={bucket.key} className=\"flex flex-col\">\n <WhyPill\n bucket={bucket}\n isSelected={isSelected}\n signalCount={signalCount}\n panelId={panelId}\n onToggle={() => setSelectedBucketKey((prev) => (prev === bucket.key ? null : bucket.key))}\n onClose={() => setSelectedBucketKey(null)}\n />\n </div>\n )\n })}\n </div>\n\n {selectedBucket && selectedPanelId && (\n <WhyCard\n bucket={selectedBucket}\n signals={selectedBucketSignals}\n item={item}\n panelId={selectedPanelId}\n onOpenSignalBucket={onOpenSignalBucket}\n onBucketFeedback={signalData.onBucketFeedback}\n initialBucketFeedback={signalData.initialBucketFeedback?.[selectedBucket.key]}\n />\n )}\n </div>\n )\n}\n"],"mappings":"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","names":["_a"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/score-why-chips.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport {\n ChevronDown,\n ChevronUp,\n ChevronRight,\n X,\n TrendingDown,\n ArrowUpRight,\n Radar,\n ArrowDownLeft,\n GitMerge,\n Activity,\n} from \"lucide-react\"\nimport type { LucideIcon } from \"lucide-react\"\nimport { FeedbackFooter } from \"./feedback-primitives\"\nimport type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from \"./feedback-primitives\"\nimport { cn } from \"../lib/utils\"\nimport type {\n QueueItem,\n SignalScoreData,\n SignalScoreExplanationBucket,\n SignalScoreExplanationSignal,\n SignalScoreUrgencyLabel,\n} from \"../prototype/prototype-config\"\n\n// ---------------------------------------------------------------------------\n// Constants & helpers\n// ---------------------------------------------------------------------------\n\nexport function getSignalScoreUrgencyLabel(\n score: number,\n providedLabel?: SignalScoreUrgencyLabel,\n): SignalScoreUrgencyLabel {\n if (providedLabel) return providedLabel\n if (score >= 80) return \"Urgent\"\n if (score >= 60) return \"High\"\n if (score >= 35) return \"Medium\"\n return \"Low\"\n}\n\nexport function scoreRangeForUrgency(label: SignalScoreUrgencyLabel): string {\n switch (label) {\n case \"Urgent\":\n return \"80-100\"\n case \"High\":\n return \"60-79\"\n case \"Medium\":\n return \"35-59\"\n case \"Low\":\n return \"0-34\"\n }\n}\n\nfunction makeDomId(...parts: Array<string | undefined>): string {\n return parts\n .filter((part): part is string => Boolean(part))\n .join(\"-\")\n .replace(/[^A-Za-z0-9_-]+/g, \"-\")\n}\n\nfunction bucketHasSignalRows(bucket: SignalScoreExplanationBucket): boolean {\n return (\n (bucket.signals?.length ?? 0) > 0 ||\n (bucket.signalIds?.length ?? 0) > 0 ||\n Boolean(bucket.primarySignalId)\n )\n}\n\nfunction getSignalScoreBuckets(signalData: SignalScoreData): SignalScoreExplanationBucket[] {\n return (signalData.explanationBuckets ?? []).filter(\n (bucket) => bucket.kind !== \"factor\" && bucketHasSignalRows(bucket),\n )\n}\n\nfunction getBucketSignals(bucket: SignalScoreExplanationBucket): SignalScoreExplanationSignal[] {\n if (bucket.signals && bucket.signals.length > 0) return bucket.signals\n\n const signalIds = bucket.signalIds && bucket.signalIds.length > 0 ? bucket.signalIds : bucket.primarySignalId ? [bucket.primarySignalId] : []\n const uniqueSignalIds = Array.from(new Set(signalIds))\n\n return uniqueSignalIds.map((signalId) => ({\n id: signalId,\n label: `${bucket.label} signal`,\n }))\n}\n\n// ---------------------------------------------------------------------------\n// Signal type icon map - keyed by signal type name (Tailwind v4 source scanned)\n// ---------------------------------------------------------------------------\n\nconst SIGNAL_TYPE_ICONS: Record<string, LucideIcon> = {\n treasury_liquidation: TrendingDown,\n cumulative_treasury_outflow: ArrowUpRight,\n test_transaction: Radar,\n micro_deposit: ArrowDownLeft,\n combined_signal: GitMerge,\n}\n\nfunction resolveIcon(iconName?: string): LucideIcon {\n if (!iconName) return Activity\n return SIGNAL_TYPE_ICONS[iconName] ?? Activity\n}\n\n// ---------------------------------------------------------------------------\n// Static tone class maps (REQUIRED for Tailwind v4 source scanning)\n// ---------------------------------------------------------------------------\n\n/** Shared tone-to-class map. Re-exported for signal-priority-popover. */\nexport const SIGNAL_TONE_CLASSES: Record<string, string> = {\n alert: \"bg-red-50 text-red-600\",\n warn: \"bg-amber-50 text-amber-600\",\n info: \"bg-blue-50 text-blue-600\",\n}\n\n/** Default tone for missing/unknown tone values */\nexport const DEFAULT_TONE_CLASS = \"bg-muted text-muted-foreground\"\n\n// ---------------------------------------------------------------------------\n// Em-dash fallback for missing slot data\n// ---------------------------------------------------------------------------\n\nfunction slotValue(value: string | null | undefined): string {\n return value && value.trim().length > 0 ? value : \"\"\n}\n\n// ---------------------------------------------------------------------------\n// Bucket feedback chip config\n// ---------------------------------------------------------------------------\n\nconst BUCKET_NEGATIVE_CHIPS: FeedbackChipTree[] = [\n {\n label: \"Not relevant for this account\",\n subPrompt: \"Why isn't it relevant?\",\n subChips: [\n \"Business as usual for this account\",\n \"Account in maintenance mode\",\n \"Wrong contact for this signal\",\n \"Other\",\n ],\n },\n { label: \"Bad timing\" },\n {\n label: \"Inaccurate data\",\n subPrompt: \"Which field?\",\n subChips: [\"Balance figures\", \"Counterparty\", \"Timestamp\", \"Other\"],\n },\n { label: \"Wrong account\" },\n { label: \"Already handled\" },\n { label: \"Other\" },\n]\n\n// ---------------------------------------------------------------------------\n// Default visible row count for long lists\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_VISIBLE_ROWS = 8\n\n// ---------------------------------------------------------------------------\n// WhyPill - Bucket toggle button with icon, count badge, chevron, close\n// ---------------------------------------------------------------------------\n\ninterface WhyPillProps {\n bucket: SignalScoreExplanationBucket\n isSelected: boolean\n signalCount: number\n panelId: string\n onToggle: () => void\n onClose: () => void\n}\n\nfunction WhyPill({ bucket, isSelected, signalCount, panelId, onToggle, onClose }: WhyPillProps) {\n const IconComponent = resolveIcon(bucket.icon)\n\n const sharedClasses = cn(\n \"inline-flex items-center text-[11px] font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n isSelected\n ? \"border-border bg-muted text-foreground\"\n : \"border-border bg-background text-muted-foreground hover:bg-muted/60 hover:text-foreground\",\n )\n\n return (\n <div className=\"inline-flex h-[26px] items-stretch\">\n <button\n type=\"button\"\n onClick={onToggle}\n aria-expanded={isSelected}\n aria-controls={panelId}\n className={cn(\n sharedClasses,\n \"gap-1.5 rounded-lg border px-2.5 py-1\",\n isSelected && \"rounded-b-none rounded-r-none border-r-0\",\n )}\n >\n <IconComponent className=\"h-3 w-3 shrink-0\" />\n {bucket.label}\n {signalCount > 1 && (\n <span className={cn(\"rounded-full px-1.5 py-0 text-[10px]\", isSelected ? \"bg-background/60\" : \"bg-muted\")}>\n x{signalCount}\n </span>\n )}\n {isSelected ? (\n <ChevronUp className=\"h-3 w-3 shrink-0\" />\n ) : (\n <ChevronDown className=\"h-3 w-3 shrink-0\" />\n )}\n </button>\n {isSelected && (\n <button\n type=\"button\"\n aria-label={`Close ${bucket.label}`}\n onClick={onClose}\n className={cn(\n sharedClasses,\n \"rounded-lg rounded-b-none rounded-l-none border border-l-0 border-border px-1.5 py-1 hover:bg-background/60\",\n )}\n >\n <X className=\"h-3 w-3\" />\n </button>\n )}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// CombinedSignalMiniChips - renders component type chips for combined signals\n// ---------------------------------------------------------------------------\n\ninterface CombinedSignalMiniChipsProps {\n components: Array<{ type: string; count: number }>\n}\n\nfunction CombinedSignalMiniChips({ components }: CombinedSignalMiniChipsProps) {\n return (\n <div className=\"flex flex-wrap items-center gap-1\">\n {components.map((comp, idx) => {\n const CompIcon = resolveIcon(comp.type)\n return (\n <React.Fragment key={comp.type}>\n {idx > 0 && <span className=\"text-[10px] text-muted-foreground/60\">+</span>}\n <span className=\"inline-flex items-center gap-0.5 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground\">\n <CompIcon className=\"h-2.5 w-2.5 shrink-0\" />\n {comp.type.replace(/_/g, \" \")} x{comp.count}\n </span>\n </React.Fragment>\n )\n })}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// StructuredSignalRow - CSS grid slot grammar signal row\n// ---------------------------------------------------------------------------\n\ninterface StructuredSignalRowProps {\n item: QueueItem\n bucketKey: string\n signal: SignalScoreExplanationSignal\n tone?: \"alert\" | \"warn\" | \"info\"\n onOpenSignalBucket?: ScoreWhyChipsProps[\"onOpenSignalBucket\"]\n}\n\nfunction StructuredSignalRow({ item, bucketKey, signal, tone, onOpenSignalBucket }: StructuredSignalRowProps) {\n const IconComponent = resolveIcon(signal.signalTypeName)\n const toneClass = tone ? (SIGNAL_TONE_CLASSES[tone] ?? DEFAULT_TONE_CLASS) : DEFAULT_TONE_CLASS\n const isCombined = signal.signalTypeName === \"combined_signal\" && signal.components && signal.components.length > 0\n const hasBalance = Boolean(signal.currentBalance || signal.balanceContext)\n\n const rowContent = (\n <>\n {/* Slot 1: Icon */}\n <div className={cn(\"flex h-5 w-5 shrink-0 items-center justify-center rounded\", toneClass)}>\n <IconComponent className=\"h-3 w-3\" />\n </div>\n\n {/* Slot 2: Primary value + qualifier */}\n <div className=\"min-w-0\">\n {isCombined ? (\n <CombinedSignalMiniChips components={signal.components!} />\n ) : (\n <div className=\"flex items-baseline gap-1.5\">\n <span className=\"text-sm font-semibold tabular-nums text-foreground\">\n {slotValue(signal.primaryValue)}\n </span>\n <span className=\"text-xs text-muted-foreground\">\n {slotValue(signal.qualifier)}\n </span>\n </div>\n )}\n </div>\n\n {/* Slot 3: Counterparty */}\n <div className=\"min-w-0\">\n <span className=\"block truncate text-xs text-muted-foreground\">\n {slotValue(signal.counterparty)}\n </span>\n </div>\n\n {/* Slot 4: Time */}\n <span className=\"shrink-0 text-[11px] text-muted-foreground/70\">\n {slotValue(signal.time)}\n </span>\n\n {/* Slot 5: Chevron */}\n <ChevronRight className=\"h-3 w-3 shrink-0 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5 group-hover:text-foreground/50\" />\n\n {/* Balance context strip — spans full row below grid columns */}\n {hasBalance && (\n <div\n className=\"col-span-full mt-0.5 text-[10px] text-muted-foreground/70\"\n data-testid=\"balance-context-strip\"\n >\n {signal.currentBalance && (\n <span>\n Current balance <span className=\"font-medium text-muted-foreground\">{signal.currentBalance}</span>\n </span>\n )}\n {signal.balanceContext && (\n <span>\n {signal.currentBalance ? \" · \" : \"\"}\n {signal.balanceContext}\n </span>\n )}\n </div>\n )}\n </>\n )\n\n if (signal.id && onOpenSignalBucket) {\n return (\n <button\n type=\"button\"\n className=\"group grid w-full cursor-pointer items-center gap-x-3 gap-y-1 rounded-md px-3 py-2 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n style={{ gridTemplateColumns: \"20px minmax(0,1fr) minmax(0,1fr) auto 16px\" }}\n onClick={() => onOpenSignalBucket({ item, bucketKey, signalId: signal.id! })}\n >\n {rowContent}\n </button>\n )\n }\n\n return (\n <div\n className=\"grid w-full items-center gap-x-3 gap-y-1 rounded-md px-3 py-2\"\n style={{ gridTemplateColumns: \"20px minmax(0,1fr) minmax(0,1fr) auto 16px\" }}\n >\n {rowContent}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// Legacy SignalRow (for signals without structured data)\n// ---------------------------------------------------------------------------\n\ninterface SignalRowProps {\n item: QueueItem\n bucketKey: string\n signal: SignalScoreExplanationSignal\n onOpenSignalBucket?: ScoreWhyChipsProps[\"onOpenSignalBucket\"]\n}\n\nfunction LegacySignalRow({ item, bucketKey, signal, onOpenSignalBucket }: SignalRowProps) {\n const isClickable = !!(signal.id && onOpenSignalBucket)\n const rowContent = (\n <>\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-medium text-foreground\">{signal.label}</p>\n {signal.description ? <p className=\"mt-1 leading-relaxed text-muted-foreground\">{signal.description}</p> : null}\n {(signal.source || signal.metric) && (\n <div className=\"mt-1.5 flex flex-wrap gap-1.5 text-[11px] text-muted-foreground/80\">\n {signal.source ? <span className=\"rounded-full bg-muted px-2 py-0.5\">{signal.source}</span> : null}\n {signal.metric ? <span className=\"rounded-full bg-muted px-2 py-0.5\">{signal.metric}</span> : null}\n </div>\n )}\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {signal.time ? <span className=\"text-[11px] text-muted-foreground/70\">{signal.time}</span> : null}\n {isClickable && (\n <ChevronRight className=\"h-3 w-3 text-muted-foreground/60 transition-transform group-hover:translate-x-0.5 group-hover:text-foreground/50\" />\n )}\n </div>\n </div>\n </>\n )\n\n if (isClickable) {\n return (\n <button\n type=\"button\"\n className=\"group w-full cursor-pointer rounded-md bg-background/80 p-3 text-left text-xs transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n onClick={() => onOpenSignalBucket!({ item, bucketKey, signalId: signal.id! })}\n >\n {rowContent}\n </button>\n )\n }\n\n return <div className=\"rounded-md bg-background/80 p-3 text-xs\">{rowContent}</div>\n}\n\n/**\n * Determine whether a signal has structured slot data.\n * If it has primaryValue, counterparty, qualifier, or components, use the structured row.\n */\nfunction hasStructuredData(signal: SignalScoreExplanationSignal): boolean {\n return Boolean(\n signal.primaryValue ||\n signal.qualifier ||\n signal.counterparty ||\n (signal.components && signal.components.length > 0),\n )\n}\n\n// ---------------------------------------------------------------------------\n// WhyCard - Expanded panel under a pill\n// ---------------------------------------------------------------------------\n\ninterface WhyCardProps {\n bucket: SignalScoreExplanationBucket\n signals: SignalScoreExplanationSignal[]\n item: QueueItem\n panelId: string\n onOpenSignalBucket?: ScoreWhyChipsProps[\"onOpenSignalBucket\"]\n onBucketFeedback?: (bucketKey: string, data: FeedbackSubmitData) => void\n /** Persisted bucket-level feedback to hydrate from. */\n initialBucketFeedback?: PersistedFeedbackData | null\n}\n\nfunction WhyCard({ bucket, signals, item, panelId, onOpenSignalBucket, onBucketFeedback, initialBucketFeedback }: WhyCardProps) {\n const [showAll, setShowAll] = React.useState(false)\n const [bucketFeedback, setBucketFeedback] = React.useState<\"positive\" | \"negative\" | null>(null)\n const totalCount = bucket.signalCount ?? signals.length\n const visibleSignals = showAll ? signals : signals.slice(0, DEFAULT_VISIBLE_ROWS)\n const hiddenCount = signals.length - DEFAULT_VISIBLE_ROWS\n\n // Determine whether to use structured rows (any signal has structured data)\n const useStructured = signals.some(hasStructuredData)\n\n return (\n <div\n id={panelId}\n className=\"rounded-lg rounded-t-none border border-t-0 border-border bg-muted/20 p-3\"\n role=\"region\"\n aria-label={`${bucket.label} details`}\n >\n {/* Card header */}\n <div className=\"mb-2 flex items-center justify-between\">\n <span className=\"text-[10px] font-bold uppercase tracking-wider text-muted-foreground\">\n {totalCount} signal{totalCount !== 1 ? \"s\" : \"\"} – {bucket.label}\n </span>\n </div>\n\n {/* Signal rows */}\n {visibleSignals.length > 0 ? (\n <ul className=\"divide-y divide-border/30\" aria-label=\"Matching signals\">\n {visibleSignals.map((signal, index) => (\n <li key={signal.id ?? `${bucket.key}-signal-${index}`}>\n {useStructured ? (\n <StructuredSignalRow\n item={item}\n bucketKey={bucket.key}\n signal={signal}\n tone={bucket.tone}\n onOpenSignalBucket={onOpenSignalBucket}\n />\n ) : (\n <LegacySignalRow\n item={item}\n bucketKey={bucket.key}\n signal={signal}\n onOpenSignalBucket={onOpenSignalBucket}\n />\n )}\n </li>\n ))}\n </ul>\n ) : bucket.evidence && bucket.evidence.length > 0 ? (\n <ul className=\"mt-3 space-y-1.5\" aria-label=\"Matching signals\">\n {bucket.evidence.map((evidence, index) => (\n <li key={`${bucket.key}-evidence-${index}`} className=\"flex gap-2 text-xs text-muted-foreground\">\n <span className=\"mt-1.5 h-1 w-1 shrink-0 rounded-full bg-primary\" />\n <span className=\"leading-relaxed\">{evidence}</span>\n </li>\n ))}\n </ul>\n ) : null}\n\n {/* \"Show N more\" button */}\n {!showAll && hiddenCount > 0 && (\n <button\n type=\"button\"\n onClick={() => setShowAll(true)}\n className=\"mt-2 flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground\"\n >\n <ChevronDown className=\"h-3 w-3\" />\n Show {hiddenCount} more\n </button>\n )}\n\n {/* Bucket feedback footer */}\n {onBucketFeedback && (\n <div className=\"mt-3 border-t border-border/40 pt-3\">\n <FeedbackFooter\n feedback={bucketFeedback}\n onFeedbackChange={setBucketFeedback}\n onSubmit={(data) => onBucketFeedback(bucket.key, data)}\n negativeChips={BUCKET_NEGATIVE_CHIPS}\n negativePrompt=\"Was this bucket useful?\"\n positivePrompt=\"Thanks! What was useful about this bucket?\"\n initialFeedback={initialBucketFeedback}\n feedbackKey={bucket.key}\n />\n </div>\n )}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// ScoreWhyChips - Main export\n// ---------------------------------------------------------------------------\n\nexport interface ScoreWhyChipsProps {\n item: QueueItem\n signalData: SignalScoreData\n onOpenSignalBucket?: (args: { item: QueueItem; bucketKey: string; signalId: string }) => void\n className?: string\n}\n\nexport function ScoreWhyChips({\n item,\n signalData,\n onOpenSignalBucket,\n className,\n}: ScoreWhyChipsProps) {\n const [selectedBucketKey, setSelectedBucketKey] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n setSelectedBucketKey(null)\n }, [item.id])\n\n const reactId = React.useId()\n const idPrefix = makeDomId(\"score-why\", reactId, item.id)\n const buckets = React.useMemo(() => getSignalScoreBuckets(signalData), [signalData])\n const selectedBucket = buckets.find((bucket) => bucket.key === selectedBucketKey) ?? null\n const selectedBucketSignals = selectedBucket ? getBucketSignals(selectedBucket) : []\n const selectedPanelId = selectedBucket ? `${idPrefix}-panel-${makeDomId(selectedBucket.key)}` : undefined\n\n if (buckets.length === 0) return null\n\n return (\n <div className={cn(\"mt-4\", className)}>\n <div className=\"mb-2 flex items-center gap-2\">\n <span className=\"text-[10px] font-bold uppercase tracking-wider text-muted-foreground\">Why</span>\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {buckets.map((bucket) => {\n const isSelected = selectedBucketKey === bucket.key\n const panelId = `${idPrefix}-panel-${makeDomId(bucket.key)}`\n const signals = getBucketSignals(bucket)\n const signalCount = bucket.signalCount ?? signals.length\n return (\n <div key={bucket.key} className=\"flex flex-col\">\n <WhyPill\n bucket={bucket}\n isSelected={isSelected}\n signalCount={signalCount}\n panelId={panelId}\n onToggle={() => setSelectedBucketKey((prev) => (prev === bucket.key ? null : bucket.key))}\n onClose={() => setSelectedBucketKey(null)}\n />\n </div>\n )\n })}\n </div>\n\n {selectedBucket && selectedPanelId && (\n <WhyCard\n bucket={selectedBucket}\n signals={selectedBucketSignals}\n item={item}\n panelId={selectedPanelId}\n onOpenSignalBucket={onOpenSignalBucket}\n onBucketFeedback={signalData.onBucketFeedback}\n initialBucketFeedback={signalData.initialBucketFeedback?.[selectedBucket.key]}\n />\n )}\n </div>\n )\n}\n"],"mappings":";AAmMQ,SA4EJ,UA5EI,KAGE,YAHF;AAjMR,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,sBAAsB;AAE/B,SAAS,UAAU;AAaZ,SAAS,2BACd,OACA,eACyB;AACzB,MAAI,cAAe,QAAO;AAC1B,MAAI,SAAS,GAAI,QAAO;AACxB,MAAI,SAAS,GAAI,QAAO;AACxB,MAAI,SAAS,GAAI,QAAO;AACxB,SAAO;AACT;AAEO,SAAS,qBAAqB,OAAwC;AAC3E,UAAQ,OAAO;AAAA,IACb,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,EACX;AACF;AAEA,SAAS,aAAa,OAA0C;AAC9D,SAAO,MACJ,OAAO,CAAC,SAAyB,QAAQ,IAAI,CAAC,EAC9C,KAAK,GAAG,EACR,QAAQ,oBAAoB,GAAG;AACpC;AAEA,SAAS,oBAAoB,QAA+C;AA9D5E;AA+DE,WACG,kBAAO,YAAP,mBAAgB,WAAhB,YAA0B,KAAK,OAC/B,kBAAO,cAAP,mBAAkB,WAAlB,YAA4B,KAAK,KAClC,QAAQ,OAAO,eAAe;AAElC;AAEA,SAAS,sBAAsB,YAA6D;AAtE5F;AAuEE,WAAQ,gBAAW,uBAAX,YAAiC,CAAC,GAAG;AAAA,IAC3C,CAAC,WAAW,OAAO,SAAS,YAAY,oBAAoB,MAAM;AAAA,EACpE;AACF;AAEA,SAAS,iBAAiB,QAAsE;AAC9F,MAAI,OAAO,WAAW,OAAO,QAAQ,SAAS,EAAG,QAAO,OAAO;AAE/D,QAAM,YAAY,OAAO,aAAa,OAAO,UAAU,SAAS,IAAI,OAAO,YAAY,OAAO,kBAAkB,CAAC,OAAO,eAAe,IAAI,CAAC;AAC5I,QAAM,kBAAkB,MAAM,KAAK,IAAI,IAAI,SAAS,CAAC;AAErD,SAAO,gBAAgB,IAAI,CAAC,cAAc;AAAA,IACxC,IAAI;AAAA,IACJ,OAAO,GAAG,OAAO,KAAK;AAAA,EACxB,EAAE;AACJ;AAMA,MAAM,oBAAgD;AAAA,EACpD,sBAAsB;AAAA,EACtB,6BAA6B;AAAA,EAC7B,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,iBAAiB;AACnB;AAEA,SAAS,YAAY,UAA+B;AApGpD;AAqGE,MAAI,CAAC,SAAU,QAAO;AACtB,UAAO,uBAAkB,QAAQ,MAA1B,YAA+B;AACxC;AAOO,MAAM,sBAA8C;AAAA,EACzD,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AACR;AAGO,MAAM,qBAAqB;AAMlC,SAAS,UAAU,OAA0C;AAC3D,SAAO,SAAS,MAAM,KAAK,EAAE,SAAS,IAAI,QAAQ;AACpD;AAMA,MAAM,wBAA4C;AAAA,EAChD;AAAA,IACE,OAAO;AAAA,IACP,WAAW;AAAA,IACX,UAAU;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA,EAAE,OAAO,aAAa;AAAA,EACtB;AAAA,IACE,OAAO;AAAA,IACP,WAAW;AAAA,IACX,UAAU,CAAC,mBAAmB,gBAAgB,aAAa,OAAO;AAAA,EACpE;AAAA,EACA,EAAE,OAAO,gBAAgB;AAAA,EACzB,EAAE,OAAO,kBAAkB;AAAA,EAC3B,EAAE,OAAO,QAAQ;AACnB;AAMA,MAAM,uBAAuB;AAe7B,SAAS,QAAQ,EAAE,QAAQ,YAAY,aAAa,SAAS,UAAU,QAAQ,GAAiB;AAC9F,QAAM,gBAAgB,YAAY,OAAO,IAAI;AAE7C,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA,aACI,2CACA;AAAA,EACN;AAEA,SACE,qBAAC,SAAI,WAAU,sCACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,iBAAe;AAAA,QACf,iBAAe;AAAA,QACf,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,cAAc;AAAA,QAChB;AAAA,QAEA;AAAA,8BAAC,iBAAc,WAAU,oBAAmB;AAAA,UAC3C,OAAO;AAAA,UACP,cAAc,KACb,qBAAC,UAAK,WAAW,GAAG,wCAAwC,aAAa,qBAAqB,UAAU,GAAG;AAAA;AAAA,YACvG;AAAA,aACJ;AAAA,UAED,aACC,oBAAC,aAAU,WAAU,oBAAmB,IAExC,oBAAC,eAAY,WAAU,oBAAmB;AAAA;AAAA;AAAA,IAE9C;AAAA,IACC,cACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,cAAY,SAAS,OAAO,KAAK;AAAA,QACjC,SAAS;AAAA,QACT,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEA,8BAAC,KAAE,WAAU,WAAU;AAAA;AAAA,IACzB;AAAA,KAEJ;AAEJ;AAUA,SAAS,wBAAwB,EAAE,WAAW,GAAiC;AAC7E,SACE,oBAAC,SAAI,WAAU,qCACZ,qBAAW,IAAI,CAAC,MAAM,QAAQ;AAC7B,UAAM,WAAW,YAAY,KAAK,IAAI;AACtC,WACE,qBAAC,MAAM,UAAN,EACE;AAAA,YAAM,KAAK,oBAAC,UAAK,WAAU,wCAAuC,eAAC;AAAA,MACpE,qBAAC,UAAK,WAAU,iHACd;AAAA,4BAAC,YAAS,WAAU,wBAAuB;AAAA,QAC1C,KAAK,KAAK,QAAQ,MAAM,GAAG;AAAA,QAAE;AAAA,QAAG,KAAK;AAAA,SACxC;AAAA,SALmB,KAAK,IAM1B;AAAA,EAEJ,CAAC,GACH;AAEJ;AAcA,SAAS,oBAAoB,EAAE,MAAM,WAAW,QAAQ,MAAM,mBAAmB,GAA6B;AAxQ9G;AAyQE,QAAM,gBAAgB,YAAY,OAAO,cAAc;AACvD,QAAM,YAAY,QAAQ,yBAAoB,IAAI,MAAxB,YAA6B,qBAAsB;AAC7E,QAAM,aAAa,OAAO,mBAAmB,qBAAqB,OAAO,cAAc,OAAO,WAAW,SAAS;AAClH,QAAM,aAAa,QAAQ,OAAO,kBAAkB,OAAO,cAAc;AAEzE,QAAM,aACJ,iCAEE;AAAA,wBAAC,SAAI,WAAW,GAAG,6DAA6D,SAAS,GACvF,8BAAC,iBAAc,WAAU,WAAU,GACrC;AAAA,IAGA,oBAAC,SAAI,WAAU,WACZ,uBACC,oBAAC,2BAAwB,YAAY,OAAO,YAAa,IAEzD,qBAAC,SAAI,WAAU,+BACb;AAAA,0BAAC,UAAK,WAAU,sDACb,oBAAU,OAAO,YAAY,GAChC;AAAA,MACA,oBAAC,UAAK,WAAU,iCACb,oBAAU,OAAO,SAAS,GAC7B;AAAA,OACF,GAEJ;AAAA,IAGA,oBAAC,SAAI,WAAU,WACb,8BAAC,UAAK,WAAU,gDACb,oBAAU,OAAO,YAAY,GAChC,GACF;AAAA,IAGA,oBAAC,UAAK,WAAU,iDACb,oBAAU,OAAO,IAAI,GACxB;AAAA,IAGA,oBAAC,gBAAa,WAAU,6HAA4H;AAAA,IAGnJ,cACC;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,eAAY;AAAA,QAEX;AAAA,iBAAO,kBACN,qBAAC,UAAK;AAAA;AAAA,YACY,oBAAC,UAAK,WAAU,qCAAqC,iBAAO,gBAAe;AAAA,aAC7F;AAAA,UAED,OAAO,kBACN,qBAAC,UACE;AAAA,mBAAO,iBAAiB,WAAQ;AAAA,YAChC,OAAO;AAAA,aACV;AAAA;AAAA;AAAA,IAEJ;AAAA,KAEJ;AAGF,MAAI,OAAO,MAAM,oBAAoB;AACnC,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO,EAAE,qBAAqB,6CAA6C;AAAA,QAC3E,SAAS,MAAM,mBAAmB,EAAE,MAAM,WAAW,UAAU,OAAO,GAAI,CAAC;AAAA,QAE1E;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,OAAO,EAAE,qBAAqB,6CAA6C;AAAA,MAE1E;AAAA;AAAA,EACH;AAEJ;AAaA,SAAS,gBAAgB,EAAE,MAAM,WAAW,QAAQ,mBAAmB,GAAmB;AACxF,QAAM,cAAc,CAAC,EAAE,OAAO,MAAM;AACpC,QAAM,aACJ,gCACE,+BAAC,SAAI,WAAU,0CACb;AAAA,yBAAC,SAAI,WAAU,kBACb;AAAA,0BAAC,OAAE,WAAU,+BAA+B,iBAAO,OAAM;AAAA,MACxD,OAAO,cAAc,oBAAC,OAAE,WAAU,8CAA8C,iBAAO,aAAY,IAAO;AAAA,OACzG,OAAO,UAAU,OAAO,WACxB,qBAAC,SAAI,WAAU,sEACZ;AAAA,eAAO,SAAS,oBAAC,UAAK,WAAU,qCAAqC,iBAAO,QAAO,IAAU;AAAA,QAC7F,OAAO,SAAS,oBAAC,UAAK,WAAU,qCAAqC,iBAAO,QAAO,IAAU;AAAA,SAChG;AAAA,OAEJ;AAAA,IACA,qBAAC,SAAI,WAAU,oCACZ;AAAA,aAAO,OAAO,oBAAC,UAAK,WAAU,wCAAwC,iBAAO,MAAK,IAAU;AAAA,MAC5F,eACC,oBAAC,gBAAa,WAAU,oHAAmH;AAAA,OAE/I;AAAA,KACF,GACF;AAGF,MAAI,aAAa;AACf,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,SAAS,MAAM,mBAAoB,EAAE,MAAM,WAAW,UAAU,OAAO,GAAI,CAAC;AAAA,QAE3E;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,SAAO,oBAAC,SAAI,WAAU,2CAA2C,sBAAW;AAC9E;AAMA,SAAS,kBAAkB,QAA+C;AACxE,SAAO;AAAA,IACL,OAAO,gBACP,OAAO,aACP,OAAO,gBACN,OAAO,cAAc,OAAO,WAAW,SAAS;AAAA,EACnD;AACF;AAiBA,SAAS,QAAQ,EAAE,QAAQ,SAAS,MAAM,SAAS,oBAAoB,kBAAkB,sBAAsB,GAAiB;AAhbhI;AAibE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAyC,IAAI;AAC/F,QAAM,cAAa,YAAO,gBAAP,YAAsB,QAAQ;AACjD,QAAM,iBAAiB,UAAU,UAAU,QAAQ,MAAM,GAAG,oBAAoB;AAChF,QAAM,cAAc,QAAQ,SAAS;AAGrC,QAAM,gBAAgB,QAAQ,KAAK,iBAAiB;AAEpD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,IAAI;AAAA,MACJ,WAAU;AAAA,MACV,MAAK;AAAA,MACL,cAAY,GAAG,OAAO,KAAK;AAAA,MAG3B;AAAA,4BAAC,SAAI,WAAU,0CACb,+BAAC,UAAK,WAAU,wEACb;AAAA;AAAA,UAAW;AAAA,UAAQ,eAAe,IAAI,MAAM;AAAA,UAAG;AAAA,UAAU,OAAO;AAAA,WACnE,GACF;AAAA,QAGC,eAAe,SAAS,IACvB,oBAAC,QAAG,WAAU,6BAA4B,cAAW,oBAClD,yBAAe,IAAI,CAAC,QAAQ,UAAO;AA3c9C,cAAAA;AA4cY,qCAAC,QACE,0BACC;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA,WAAW,OAAO;AAAA,cAClB;AAAA,cACA,MAAM,OAAO;AAAA,cACb;AAAA;AAAA,UACF,IAEA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA,WAAW,OAAO;AAAA,cAClB;AAAA,cACA;AAAA;AAAA,UACF,MAfKA,MAAA,OAAO,OAAP,OAAAA,MAAa,GAAG,OAAO,GAAG,WAAW,KAAK,EAiBnD;AAAA,SACD,GACH,IACE,OAAO,YAAY,OAAO,SAAS,SAAS,IAC9C,oBAAC,QAAG,WAAU,oBAAmB,cAAW,oBACzC,iBAAO,SAAS,IAAI,CAAC,UAAU,UAC9B,qBAAC,QAA2C,WAAU,4CACpD;AAAA,8BAAC,UAAK,WAAU,mDAAkD;AAAA,UAClE,oBAAC,UAAK,WAAU,mBAAmB,oBAAS;AAAA,aAFrC,GAAG,OAAO,GAAG,aAAa,KAAK,EAGxC,CACD,GACH,IACE;AAAA,QAGH,CAAC,WAAW,cAAc,KACzB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM,WAAW,IAAI;AAAA,YAC9B,WAAU;AAAA,YAEV;AAAA,kCAAC,eAAY,WAAU,WAAU;AAAA,cAAE;AAAA,cAC7B;AAAA,cAAY;AAAA;AAAA;AAAA,QACpB;AAAA,QAID,oBACC,oBAAC,SAAI,WAAU,uCACb;AAAA,UAAC;AAAA;AAAA,YACC,UAAU;AAAA,YACV,kBAAkB;AAAA,YAClB,UAAU,CAAC,SAAS,iBAAiB,OAAO,KAAK,IAAI;AAAA,YACrD,eAAe;AAAA,YACf,gBAAe;AAAA,YACf,gBAAe;AAAA,YACf,iBAAiB;AAAA,YACjB,aAAa,OAAO;AAAA;AAAA,QACtB,GACF;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAaO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AA1hBvB;AA2hBE,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAwB,IAAI;AAEpF,QAAM,UAAU,MAAM;AACpB,yBAAqB,IAAI;AAAA,EAC3B,GAAG,CAAC,KAAK,EAAE,CAAC;AAEZ,QAAM,UAAU,MAAM,MAAM;AAC5B,QAAM,WAAW,UAAU,aAAa,SAAS,KAAK,EAAE;AACxD,QAAM,UAAU,MAAM,QAAQ,MAAM,sBAAsB,UAAU,GAAG,CAAC,UAAU,CAAC;AACnF,QAAM,kBAAiB,aAAQ,KAAK,CAAC,WAAW,OAAO,QAAQ,iBAAiB,MAAzD,YAA8D;AACrF,QAAM,wBAAwB,iBAAiB,iBAAiB,cAAc,IAAI,CAAC;AACnF,QAAM,kBAAkB,iBAAiB,GAAG,QAAQ,UAAU,UAAU,eAAe,GAAG,CAAC,KAAK;AAEhG,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,SACE,qBAAC,SAAI,WAAW,GAAG,QAAQ,SAAS,GAClC;AAAA,wBAAC,SAAI,WAAU,gCACb,8BAAC,UAAK,WAAU,wEAAuE,iBAAG,GAC5F;AAAA,IACA,oBAAC,SAAI,WAAU,0BACZ,kBAAQ,IAAI,CAAC,WAAW;AAhjBjC,UAAAA;AAijBU,YAAM,aAAa,sBAAsB,OAAO;AAChD,YAAM,UAAU,GAAG,QAAQ,UAAU,UAAU,OAAO,GAAG,CAAC;AAC1D,YAAM,UAAU,iBAAiB,MAAM;AACvC,YAAM,eAAcA,MAAA,OAAO,gBAAP,OAAAA,MAAsB,QAAQ;AAClD,aACE,oBAAC,SAAqB,WAAU,iBAC9B;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,UAAU,MAAM,qBAAqB,CAAC,SAAU,SAAS,OAAO,MAAM,OAAO,OAAO,GAAI;AAAA,UACxF,SAAS,MAAM,qBAAqB,IAAI;AAAA;AAAA,MAC1C,KARQ,OAAO,GASjB;AAAA,IAEJ,CAAC,GACH;AAAA,IAEC,kBAAkB,mBACjB;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ;AAAA,QACR,SAAS;AAAA,QACT;AAAA,QACA,SAAS;AAAA,QACT;AAAA,QACA,kBAAkB,WAAW;AAAA,QAC7B,wBAAuB,gBAAW,0BAAX,mBAAmC,eAAe;AAAA;AAAA,IAC3E;AAAA,KAEJ;AAEJ;","names":["_a"]}
|
|
@@ -4,7 +4,6 @@ export { P as PriorityFactor, S as SignalPriorityPopover, k as SignalPriorityPop
|
|
|
4
4
|
import './quick-action-sidebar-nav.js';
|
|
5
5
|
import './quick-action-modal.js';
|
|
6
6
|
import './score-breakdown.js';
|
|
7
|
-
import './score-semantics.js';
|
|
8
7
|
import './suggested-actions.js';
|
|
9
8
|
import './detail-view.js';
|
|
10
9
|
import './inbox-toolbar.js';
|