@handled-ai/design-system 0.18.2 → 0.18.4
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/feedback-primitives.d.ts +21 -1
- package/dist/components/feedback-primitives.js +167 -16
- package/dist/components/feedback-primitives.js.map +1 -1
- package/dist/components/signal-priority-popover.js +12 -151
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/wit-636-feedback-states.test.tsx +2 -2
- package/src/components/feedback-primitives.tsx +200 -15
- package/src/components/signal-priority-popover.tsx +7 -158
- package/src/index.ts +2 -2
|
@@ -81,5 +81,25 @@ interface FeedbackFooterProps {
|
|
|
81
81
|
feedbackKey?: string;
|
|
82
82
|
}
|
|
83
83
|
declare function FeedbackFooter({ feedback, onFeedbackChange, onSubmit, metaText, positivePrompt, negativePrompt, negativeChips, positiveChips, className, initialFeedback, submittedLabel, feedbackKey, }: FeedbackFooterProps): React.JSX.Element;
|
|
84
|
+
interface InlineFeedbackControlProps {
|
|
85
|
+
/** Unique key identifying the feedback target (e.g. factor key). */
|
|
86
|
+
feedbackKey: string;
|
|
87
|
+
/** Persisted/initial feedback to hydrate from. */
|
|
88
|
+
initialFeedback?: {
|
|
89
|
+
type: "up" | "down";
|
|
90
|
+
detail: string;
|
|
91
|
+
ownershipLabel?: string;
|
|
92
|
+
};
|
|
93
|
+
/** Called when user submits or clears feedback. */
|
|
94
|
+
onFeedback?: (key: string, type: "up" | "down" | null, detail?: string) => void;
|
|
95
|
+
/** Test ID prefix for all sub-elements. */
|
|
96
|
+
testIdPrefix?: string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Compact inline thumb-up/thumb-down feedback with optional detail text.
|
|
100
|
+
* Used by PriorityFactorRow and any other component that needs
|
|
101
|
+
* a lightweight feedback control.
|
|
102
|
+
*/
|
|
103
|
+
declare function InlineFeedbackControl({ feedbackKey, initialFeedback, onFeedback, testIdPrefix, }: InlineFeedbackControlProps): React.JSX.Element;
|
|
84
104
|
|
|
85
|
-
export { FeedbackActions, type FeedbackActionsProps, FeedbackChipGroup, type FeedbackChipGroupProps, type FeedbackChipTree, FeedbackFooter, type FeedbackFooterProps, FeedbackInput, type FeedbackInputProps, type FeedbackSubmitData, type PersistedFeedbackData };
|
|
105
|
+
export { FeedbackActions, type FeedbackActionsProps, FeedbackChipGroup, type FeedbackChipGroupProps, type FeedbackChipTree, FeedbackFooter, type FeedbackFooterProps, FeedbackInput, type FeedbackInputProps, type FeedbackSubmitData, InlineFeedbackControl, type InlineFeedbackControlProps, type PersistedFeedbackData };
|
|
@@ -124,8 +124,11 @@ function FeedbackFooter({
|
|
|
124
124
|
const [persisted, setPersisted] = React.useState(
|
|
125
125
|
initialFeedback != null ? initialFeedback : null
|
|
126
126
|
);
|
|
127
|
-
const
|
|
127
|
+
const isEditingRef = React.useRef(false);
|
|
128
128
|
const lastKeyRef = React.useRef(feedbackKey);
|
|
129
|
+
const setIsEditing = React.useCallback((value) => {
|
|
130
|
+
isEditingRef.current = value;
|
|
131
|
+
}, []);
|
|
129
132
|
React.useEffect(() => {
|
|
130
133
|
const keyChanged = feedbackKey !== lastKeyRef.current;
|
|
131
134
|
lastKeyRef.current = feedbackKey;
|
|
@@ -133,19 +136,19 @@ function FeedbackFooter({
|
|
|
133
136
|
setPersisted(initialFeedback != null ? initialFeedback : null);
|
|
134
137
|
setSubmitted(false);
|
|
135
138
|
setExpanded(false);
|
|
136
|
-
|
|
139
|
+
isEditingRef.current = false;
|
|
137
140
|
if (initialFeedback) {
|
|
138
141
|
onFeedbackChange(initialFeedback.sentiment);
|
|
139
142
|
} else {
|
|
140
143
|
onFeedbackChange(null);
|
|
141
144
|
}
|
|
142
|
-
} else if (!
|
|
145
|
+
} else if (!isEditingRef.current) {
|
|
143
146
|
setPersisted(initialFeedback != null ? initialFeedback : null);
|
|
144
147
|
if (initialFeedback) {
|
|
145
148
|
onFeedbackChange(initialFeedback.sentiment);
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
|
-
}, [initialFeedback, feedbackKey]);
|
|
151
|
+
}, [initialFeedback, feedbackKey, onFeedbackChange]);
|
|
149
152
|
const resetState = React.useCallback(() => {
|
|
150
153
|
setExpanded(false);
|
|
151
154
|
setSelectedTier1(null);
|
|
@@ -154,7 +157,7 @@ function FeedbackFooter({
|
|
|
154
157
|
setDetailText("");
|
|
155
158
|
setActiveTreeIndex(null);
|
|
156
159
|
setIsEditing(false);
|
|
157
|
-
}, []);
|
|
160
|
+
}, [setIsEditing]);
|
|
158
161
|
const handleSentimentClick = React.useCallback(
|
|
159
162
|
(sentiment) => {
|
|
160
163
|
onFeedbackChange(sentiment);
|
|
@@ -164,7 +167,7 @@ function FeedbackFooter({
|
|
|
164
167
|
setPersisted(null);
|
|
165
168
|
setIsEditing(true);
|
|
166
169
|
},
|
|
167
|
-
[onFeedbackChange, resetState]
|
|
170
|
+
[onFeedbackChange, resetState, setIsEditing]
|
|
168
171
|
);
|
|
169
172
|
const handlePersistedClick = React.useCallback(() => {
|
|
170
173
|
var _a, _b, _c, _d;
|
|
@@ -177,7 +180,7 @@ function FeedbackFooter({
|
|
|
177
180
|
setExpanded(true);
|
|
178
181
|
setSubmitted(false);
|
|
179
182
|
setIsEditing(true);
|
|
180
|
-
}, [persisted, onFeedbackChange]);
|
|
183
|
+
}, [persisted, onFeedbackChange, setIsEditing]);
|
|
181
184
|
const handleTier1Toggle = React.useCallback(
|
|
182
185
|
(chipLabel) => {
|
|
183
186
|
if (selectedTier1 === chipLabel) {
|
|
@@ -228,20 +231,15 @@ function FeedbackFooter({
|
|
|
228
231
|
detail: detailText
|
|
229
232
|
});
|
|
230
233
|
setSubmitted(true);
|
|
231
|
-
|
|
232
|
-
setSelectedTier1(null);
|
|
233
|
-
setSelectedTier2(null);
|
|
234
|
-
setAdditionalPills([]);
|
|
235
|
-
setDetailText("");
|
|
236
|
-
setActiveTreeIndex(null);
|
|
237
|
-
setIsEditing(false);
|
|
234
|
+
resetState();
|
|
238
235
|
}, [
|
|
239
236
|
feedback,
|
|
240
237
|
selectedTier1,
|
|
241
238
|
selectedTier2,
|
|
242
239
|
additionalPills,
|
|
243
240
|
detailText,
|
|
244
|
-
onSubmit
|
|
241
|
+
onSubmit,
|
|
242
|
+
resetState
|
|
245
243
|
]);
|
|
246
244
|
const handleCancel = React.useCallback(() => {
|
|
247
245
|
resetState();
|
|
@@ -370,10 +368,163 @@ function FeedbackFooter({
|
|
|
370
368
|
] })
|
|
371
369
|
] });
|
|
372
370
|
}
|
|
371
|
+
function InlineFeedbackControl({
|
|
372
|
+
feedbackKey,
|
|
373
|
+
initialFeedback,
|
|
374
|
+
onFeedback,
|
|
375
|
+
testIdPrefix = "inline-feedback"
|
|
376
|
+
}) {
|
|
377
|
+
var _a, _b, _c, _d;
|
|
378
|
+
const [thumbState, setThumbState] = React.useState(
|
|
379
|
+
(_a = initialFeedback == null ? void 0 : initialFeedback.type) != null ? _a : null
|
|
380
|
+
);
|
|
381
|
+
const [showInput, setShowInput] = React.useState(false);
|
|
382
|
+
const [detailText, setDetailText] = React.useState((_b = initialFeedback == null ? void 0 : initialFeedback.detail) != null ? _b : "");
|
|
383
|
+
const [saved, setSaved] = React.useState(!!initialFeedback);
|
|
384
|
+
const [savedDetail, setSavedDetail] = React.useState((_c = initialFeedback == null ? void 0 : initialFeedback.detail) != null ? _c : "");
|
|
385
|
+
const ownershipLabel = (_d = initialFeedback == null ? void 0 : initialFeedback.ownershipLabel) != null ? _d : "Your feedback";
|
|
386
|
+
React.useEffect(() => {
|
|
387
|
+
if (initialFeedback) {
|
|
388
|
+
setThumbState(initialFeedback.type);
|
|
389
|
+
setSaved(true);
|
|
390
|
+
setSavedDetail(initialFeedback.detail);
|
|
391
|
+
}
|
|
392
|
+
}, [initialFeedback]);
|
|
393
|
+
const handleThumbClick = React.useCallback(
|
|
394
|
+
(type) => {
|
|
395
|
+
if (thumbState === type) {
|
|
396
|
+
setThumbState(null);
|
|
397
|
+
setShowInput(false);
|
|
398
|
+
setSaved(false);
|
|
399
|
+
onFeedback == null ? void 0 : onFeedback(feedbackKey, null);
|
|
400
|
+
} else {
|
|
401
|
+
setThumbState(type);
|
|
402
|
+
setShowInput(true);
|
|
403
|
+
setSaved(false);
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
[thumbState, feedbackKey, onFeedback]
|
|
407
|
+
);
|
|
408
|
+
const handleSubmitDetail = React.useCallback(() => {
|
|
409
|
+
if (!thumbState) return;
|
|
410
|
+
const text = detailText.trim();
|
|
411
|
+
onFeedback == null ? void 0 : onFeedback(feedbackKey, thumbState, text);
|
|
412
|
+
setSaved(true);
|
|
413
|
+
setSavedDetail(text);
|
|
414
|
+
setShowInput(false);
|
|
415
|
+
}, [thumbState, detailText, feedbackKey, onFeedback]);
|
|
416
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
417
|
+
saved && !showInput ? (
|
|
418
|
+
/* Persisted / saved indicator */
|
|
419
|
+
/* @__PURE__ */ jsxs(
|
|
420
|
+
"button",
|
|
421
|
+
{
|
|
422
|
+
type: "button",
|
|
423
|
+
onClick: () => {
|
|
424
|
+
setDetailText(savedDetail);
|
|
425
|
+
setShowInput(true);
|
|
426
|
+
setSaved(false);
|
|
427
|
+
},
|
|
428
|
+
className: "group flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors",
|
|
429
|
+
"data-testid": `${testIdPrefix}-feedback-persisted-${feedbackKey}`,
|
|
430
|
+
children: [
|
|
431
|
+
/* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
|
|
432
|
+
ownershipLabel,
|
|
433
|
+
":"
|
|
434
|
+
] }),
|
|
435
|
+
thumbState === "up" ? /* @__PURE__ */ jsx(ThumbsUp, { className: "h-[10px] w-[10px]" }) : /* @__PURE__ */ jsx(ThumbsDown, { className: "h-[10px] w-[10px]" }),
|
|
436
|
+
savedDetail && /* @__PURE__ */ jsx("span", { className: "max-w-[180px] truncate text-muted-foreground/70", children: savedDetail }),
|
|
437
|
+
/* @__PURE__ */ jsx(Pencil, { className: "h-[9px] w-[9px] opacity-0 group-hover:opacity-100 transition-opacity" })
|
|
438
|
+
]
|
|
439
|
+
}
|
|
440
|
+
)
|
|
441
|
+
) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
442
|
+
/* @__PURE__ */ jsx(
|
|
443
|
+
"button",
|
|
444
|
+
{
|
|
445
|
+
type: "button",
|
|
446
|
+
onClick: () => handleThumbClick("up"),
|
|
447
|
+
className: cn(
|
|
448
|
+
"p-1 rounded transition-colors",
|
|
449
|
+
thumbState === "up" ? "text-foreground bg-muted" : "text-muted-foreground/40 hover:text-foreground hover:bg-muted/50"
|
|
450
|
+
),
|
|
451
|
+
title: "This is accurate",
|
|
452
|
+
"data-testid": `${testIdPrefix}-thumb-up-${feedbackKey}`,
|
|
453
|
+
children: /* @__PURE__ */ jsx(ThumbsUp, { className: "h-[10px] w-[10px]" })
|
|
454
|
+
}
|
|
455
|
+
),
|
|
456
|
+
/* @__PURE__ */ jsx(
|
|
457
|
+
"button",
|
|
458
|
+
{
|
|
459
|
+
type: "button",
|
|
460
|
+
onClick: () => handleThumbClick("down"),
|
|
461
|
+
className: cn(
|
|
462
|
+
"p-1 rounded transition-colors",
|
|
463
|
+
thumbState === "down" ? "text-red-600 bg-red-50" : "text-muted-foreground/40 hover:text-red-600 hover:bg-red-50/50"
|
|
464
|
+
),
|
|
465
|
+
title: "Report issue",
|
|
466
|
+
"data-testid": `${testIdPrefix}-thumb-down-${feedbackKey}`,
|
|
467
|
+
children: /* @__PURE__ */ jsx(ThumbsDown, { className: "h-[10px] w-[10px]" })
|
|
468
|
+
}
|
|
469
|
+
),
|
|
470
|
+
saved && /* @__PURE__ */ jsxs(
|
|
471
|
+
"span",
|
|
472
|
+
{
|
|
473
|
+
className: "inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600",
|
|
474
|
+
role: "status",
|
|
475
|
+
"data-testid": `${testIdPrefix}-saved-${feedbackKey}`,
|
|
476
|
+
children: [
|
|
477
|
+
/* @__PURE__ */ jsx(Check, { className: "h-[10px] w-[10px]" }),
|
|
478
|
+
"Saved"
|
|
479
|
+
]
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
] }),
|
|
483
|
+
showInput && thumbState && /* @__PURE__ */ jsxs("div", { className: "mt-1.5", children: [
|
|
484
|
+
/* @__PURE__ */ jsx(
|
|
485
|
+
"input",
|
|
486
|
+
{
|
|
487
|
+
type: "text",
|
|
488
|
+
value: detailText,
|
|
489
|
+
onChange: (e) => setDetailText(e.target.value),
|
|
490
|
+
onKeyDown: (e) => {
|
|
491
|
+
if (e.key === "Enter") handleSubmitDetail();
|
|
492
|
+
if (e.key === "Escape") setShowInput(false);
|
|
493
|
+
},
|
|
494
|
+
placeholder: thumbState === "up" ? "What\u2019s accurate? (optional)" : "What\u2019s wrong? (optional)",
|
|
495
|
+
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",
|
|
496
|
+
"data-testid": `${testIdPrefix}-detail-input-${feedbackKey}`
|
|
497
|
+
}
|
|
498
|
+
),
|
|
499
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-1.5", children: [
|
|
500
|
+
/* @__PURE__ */ jsx(
|
|
501
|
+
"button",
|
|
502
|
+
{
|
|
503
|
+
type: "button",
|
|
504
|
+
onClick: handleSubmitDetail,
|
|
505
|
+
className: "bg-foreground text-background rounded px-2 py-0.5 text-[10px] font-semibold",
|
|
506
|
+
"data-testid": `${testIdPrefix}-submit-${feedbackKey}`,
|
|
507
|
+
children: "Submit"
|
|
508
|
+
}
|
|
509
|
+
),
|
|
510
|
+
/* @__PURE__ */ jsx(
|
|
511
|
+
"button",
|
|
512
|
+
{
|
|
513
|
+
type: "button",
|
|
514
|
+
onClick: () => setShowInput(false),
|
|
515
|
+
className: "border border-border rounded px-2 py-0.5 text-[10px] font-medium",
|
|
516
|
+
children: "Cancel"
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
] })
|
|
520
|
+
] })
|
|
521
|
+
] });
|
|
522
|
+
}
|
|
373
523
|
export {
|
|
374
524
|
FeedbackActions,
|
|
375
525
|
FeedbackChipGroup,
|
|
376
526
|
FeedbackFooter,
|
|
377
|
-
FeedbackInput
|
|
527
|
+
FeedbackInput,
|
|
528
|
+
InlineFeedbackControl
|
|
378
529
|
};
|
|
379
530
|
//# sourceMappingURL=feedback-primitives.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/feedback-primitives.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ThumbsUp, ThumbsDown, Check, Pencil } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Structured feedback data shape.\n *\n * Preserves the tree structure for DB persistence:\n * reasonTop -> tier-1 chip label (maps to case_feedback.reason_top)\n * reasonSub -> tier-2 sub-chip label (maps to case_feedback.reason_sub)\n * pills -> any additional selected chips (maps to case_feedback.pills)\n * detail -> free-text input (maps to case_feedback.free_text)\n */\nexport interface FeedbackSubmitData {\n sentiment: \"positive\" | \"negative\"\n reasonTop?: string\n reasonSub?: string\n pills: string[]\n detail: string\n}\n\n/**\n * Persisted feedback data from a previous submission, used to hydrate the\n * footer into its \"already submitted\" visual state.\n */\nexport interface PersistedFeedbackData {\n sentiment: \"positive\" | \"negative\"\n reasonTop?: string\n reasonSub?: string\n pills?: string[]\n detail?: string\n ownershipLabel: \"Your feedback\" | \"Team feedback\"\n}\n\n/**\n * Defines a tier-1 chip that may have tier-2 sub-chips.\n */\nexport interface FeedbackChipTree {\n label: string\n subPrompt?: string\n subChips?: string[]\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackChipGroup\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackChipGroupProps {\n chips: string[]\n selected: string[]\n onToggle: (chip: string) => void\n flavor: \"positive\" | \"negative\"\n className?: string\n}\n\nconst CHIP_SELECTED_CLASSES: Record<\"positive\" | \"negative\", string> = {\n negative: \"bg-red-50 text-red-700 border-red-200\",\n positive: \"bg-muted text-foreground border-border\",\n}\n\nconst CHIP_IDLE_CLASS =\n \"bg-background text-muted-foreground border-border hover:bg-muted/50\"\n\nexport function FeedbackChipGroup({\n chips,\n selected,\n onToggle,\n flavor,\n className,\n}: FeedbackChipGroupProps) {\n return (\n <div className={cn(\"flex flex-wrap gap-1.5\", className)}>\n {chips.map((chip) => {\n const isSelected = selected.includes(chip)\n return (\n <button\n key={chip}\n type=\"button\"\n onClick={() => onToggle(chip)}\n className={cn(\n \"rounded-md px-2.5 py-1 text-[11px] font-medium border transition-colors\",\n isSelected ? CHIP_SELECTED_CLASSES[flavor] : CHIP_IDLE_CLASS,\n )}\n >\n {chip}\n </button>\n )\n })}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackInput\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackInputProps {\n placeholder?: string\n value: string\n onChange: (value: string) => void\n onSubmit?: () => void\n className?: string\n}\n\nexport function FeedbackInput({\n placeholder,\n value,\n onChange,\n onSubmit,\n className,\n}: FeedbackInputProps) {\n return (\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && onSubmit) {\n e.preventDefault()\n onSubmit()\n }\n }}\n placeholder={placeholder}\n className={cn(\n \"w-full text-xs bg-background border border-border rounded-md px-2.5 py-2 text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring\",\n className,\n )}\n />\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackActions\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackActionsProps {\n onSubmit: () => void\n onCancel: () => void\n submitDisabled?: boolean\n submitLabel?: string\n cancelLabel?: string\n hint?: string\n className?: string\n}\n\nexport function FeedbackActions({\n onSubmit,\n onCancel,\n submitDisabled = false,\n submitLabel = \"Submit\",\n cancelLabel = \"Cancel\",\n hint,\n className,\n}: FeedbackActionsProps) {\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <button\n type=\"button\"\n onClick={onSubmit}\n disabled={submitDisabled}\n className=\"bg-foreground text-background rounded-md px-3 py-1.5 text-xs font-semibold disabled:opacity-50\"\n >\n {submitLabel}\n </button>\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"border border-border rounded-md px-3 py-1.5 text-xs font-medium\"\n >\n {cancelLabel}\n </button>\n {hint && (\n <span className=\"ml-auto text-[11px] text-muted-foreground\">\n {hint}\n </span>\n )}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackFooter\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackFooterProps {\n feedback: \"positive\" | \"negative\" | null\n onFeedbackChange: (value: \"positive\" | \"negative\" | null) => void\n onSubmit: (data: FeedbackSubmitData) => void\n metaText?: string\n positivePrompt?: string\n negativePrompt?: string\n negativeChips?: FeedbackChipTree[]\n positiveChips?: string[]\n className?: string\n /** Pre-existing feedback to hydrate from (e.g. after page reload). */\n initialFeedback?: PersistedFeedbackData | null\n /** Label shown in the transient confirmation pill after submit. */\n submittedLabel?: string\n /** Stable key for syncing initialFeedback into local state. When this\n * changes, the component resets to the new initialFeedback value. */\n feedbackKey?: string\n}\n\nconst SENTIMENT_BUTTON_ACTIVE: Record<\"positive\" | \"negative\", string> = {\n negative: \"text-red-600 bg-red-50 border-red-200\",\n positive: \"text-foreground bg-muted border-border\",\n}\n\nconst SENTIMENT_BUTTON_IDLE =\n \"text-muted-foreground hover:text-foreground\"\n\nexport function FeedbackFooter({\n feedback,\n onFeedbackChange,\n onSubmit,\n metaText,\n positivePrompt = \"Thanks! Anything to keep about this score?\",\n negativePrompt = \"What's the issue?\",\n negativeChips = [],\n positiveChips = [],\n className,\n initialFeedback,\n submittedLabel = \"Saved\",\n feedbackKey,\n}: FeedbackFooterProps) {\n const [expanded, setExpanded] = React.useState(false)\n const [selectedTier1, setSelectedTier1] = React.useState<string | null>(null)\n const [selectedTier2, setSelectedTier2] = React.useState<string | null>(null)\n const [additionalPills, setAdditionalPills] = React.useState<string[]>([])\n const [detailText, setDetailText] = React.useState(\"\")\n const [activeTreeIndex, setActiveTreeIndex] = React.useState<number | null>(\n null,\n )\n /** Transient \"Saved\" confirmation — shown after successful submit. */\n const [submitted, setSubmitted] = React.useState(false)\n /** Persisted feedback shown as a clickable indicator (survives reload). */\n const [persisted, setPersisted] = React.useState<PersistedFeedbackData | null>(\n initialFeedback ?? null,\n )\n /** Tracks whether the user is actively editing (to guard against prop overwrites). */\n const [isEditing, setIsEditing] = React.useState(false)\n /** Track the last synced feedbackKey to detect key changes. */\n const lastKeyRef = React.useRef<string | undefined>(feedbackKey)\n\n // Sync initialFeedback into local state via useEffect keyed on feedbackKey.\n // When feedbackKey changes, reset to new target. Preserve active edits\n // when feedbackKey stays the same.\n React.useEffect(() => {\n const keyChanged = feedbackKey !== lastKeyRef.current\n lastKeyRef.current = feedbackKey\n\n if (keyChanged) {\n // Key changed — full reset to new target\n setPersisted(initialFeedback ?? null)\n setSubmitted(false)\n setExpanded(false)\n setIsEditing(false)\n if (initialFeedback) {\n onFeedbackChange(initialFeedback.sentiment)\n } else {\n onFeedbackChange(null)\n }\n } else if (!isEditing) {\n // Same key, not actively editing — safe to sync\n setPersisted(initialFeedback ?? null)\n if (initialFeedback) {\n onFeedbackChange(initialFeedback.sentiment)\n }\n }\n }, [initialFeedback, feedbackKey]) // eslint-disable-line react-hooks/exhaustive-deps -- reads isEditing as guard, not trigger\n\n // Reset state when feedback collapses\n const resetState = React.useCallback(() => {\n setExpanded(false)\n setSelectedTier1(null)\n setSelectedTier2(null)\n setAdditionalPills([])\n setDetailText(\"\")\n setActiveTreeIndex(null)\n setIsEditing(false)\n }, [])\n\n const handleSentimentClick = React.useCallback(\n (sentiment: \"positive\" | \"negative\") => {\n onFeedbackChange(sentiment)\n // Reset chip state when switching sentiment, then expand\n resetState()\n setExpanded(true)\n setSubmitted(false)\n setPersisted(null)\n setIsEditing(true)\n },\n [onFeedbackChange, resetState],\n )\n\n /** Open the persisted indicator for editing. */\n const handlePersistedClick = React.useCallback(() => {\n if (!persisted) return\n onFeedbackChange(persisted.sentiment)\n setSelectedTier1(persisted.reasonTop ?? null)\n setSelectedTier2(persisted.reasonSub ?? null)\n setAdditionalPills(persisted.pills ?? [])\n setDetailText(persisted.detail ?? \"\")\n setExpanded(true)\n setSubmitted(false)\n setIsEditing(true)\n }, [persisted, onFeedbackChange])\n\n const handleTier1Toggle = React.useCallback(\n (chipLabel: string) => {\n if (selectedTier1 === chipLabel) {\n // Deselect the tier-1 chip\n setSelectedTier1(null)\n setSelectedTier2(null)\n setActiveTreeIndex(null)\n } else if (selectedTier1 === null) {\n // First selection becomes the primary reasonTop\n setSelectedTier1(chipLabel)\n setSelectedTier2(null)\n // Find the chip's tree index to show sub-chips\n const idx = negativeChips.findIndex((c) => c.label === chipLabel)\n if (idx !== -1 && negativeChips[idx].subChips) {\n setActiveTreeIndex(idx)\n } else {\n setActiveTreeIndex(null)\n }\n } else {\n // Additional selections become pills\n setAdditionalPills((prev) =>\n prev.includes(chipLabel)\n ? prev.filter((p) => p !== chipLabel)\n : [...prev, chipLabel],\n )\n }\n },\n [selectedTier1, negativeChips],\n )\n\n const handleTier2Toggle = React.useCallback((subChip: string) => {\n setSelectedTier2((prev) => (prev === subChip ? null : subChip))\n }, [])\n\n const handlePositiveChipToggle = React.useCallback(\n (chip: string) => {\n if (selectedTier1 === chip) {\n setSelectedTier1(null)\n } else if (selectedTier1 === null) {\n setSelectedTier1(chip)\n } else {\n setAdditionalPills((prev) =>\n prev.includes(chip)\n ? prev.filter((p) => p !== chip)\n : [...prev, chip],\n )\n }\n },\n [selectedTier1],\n )\n\n const handleSubmit = React.useCallback(() => {\n if (!feedback) return\n onSubmit({\n sentiment: feedback,\n reasonTop: selectedTier1 ?? undefined,\n reasonSub: selectedTier2 ?? undefined,\n pills: additionalPills,\n detail: detailText,\n })\n // Show transient \"Saved\" confirmation\n setSubmitted(true)\n // Collapse expansion but keep sentiment visible\n setExpanded(false)\n setSelectedTier1(null)\n setSelectedTier2(null)\n setAdditionalPills([])\n setDetailText(\"\")\n setActiveTreeIndex(null)\n setIsEditing(false)\n }, [\n feedback,\n selectedTier1,\n selectedTier2,\n additionalPills,\n detailText,\n onSubmit,\n ])\n\n const handleCancel = React.useCallback(() => {\n resetState()\n onFeedbackChange(null)\n }, [resetState, onFeedbackChange])\n\n // Determine which chips are selected (combining tier1 + additionalPills)\n const allSelectedChips = React.useMemo(() => {\n const result: string[] = []\n if (selectedTier1) result.push(selectedTier1)\n result.push(...additionalPills)\n return result\n }, [selectedTier1, additionalPills])\n\n // Active tier-1 chip tree (for showing sub-chips)\n const activeTree =\n activeTreeIndex !== null ? negativeChips[activeTreeIndex] : null\n\n // Determine if we should show the persisted indicator instead of bare buttons\n const showPersistedIndicator = persisted && !expanded && !submitted\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* Sentiment buttons + meta text bar */}\n <div className=\"flex items-center justify-between\">\n {showPersistedIndicator ? (\n /* Persisted feedback indicator — clickable to reopen editor */\n <button\n type=\"button\"\n onClick={handlePersistedClick}\n className=\"group flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors\"\n data-testid=\"persisted-feedback-indicator\"\n >\n <span className=\"font-medium\">{persisted.ownershipLabel}:</span>\n {persisted.sentiment === \"positive\" ? (\n <ThumbsUp className=\"h-[11px] w-[11px]\" />\n ) : (\n <ThumbsDown className=\"h-[11px] w-[11px]\" />\n )}\n {persisted.detail && (\n <span className=\"max-w-[200px] truncate text-muted-foreground/70\">\n {persisted.detail}\n </span>\n )}\n <Pencil className=\"h-[9px] w-[9px] opacity-0 group-hover:opacity-100 transition-opacity\" />\n </button>\n ) : (\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => handleSentimentClick(\"positive\")}\n className={cn(\n \"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors\",\n feedback === \"positive\"\n ? SENTIMENT_BUTTON_ACTIVE.positive\n : SENTIMENT_BUTTON_IDLE,\n )}\n >\n <ThumbsUp className=\"h-[11px] w-[11px]\" />\n Helpful\n </button>\n <button\n type=\"button\"\n onClick={() => handleSentimentClick(\"negative\")}\n className={cn(\n \"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors\",\n feedback === \"negative\"\n ? SENTIMENT_BUTTON_ACTIVE.negative\n : SENTIMENT_BUTTON_IDLE,\n )}\n >\n <ThumbsDown className=\"h-[11px] w-[11px]\" />\n Not helpful\n </button>\n {/* Transient \"Saved\" confirmation pill */}\n {submitted && feedback && (\n <span\n className=\"inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600\"\n role=\"status\"\n data-testid=\"feedback-submitted-pill\"\n >\n <Check className=\"h-[11px] w-[11px]\" />\n {submittedLabel}\n </span>\n )}\n </div>\n )}\n {metaText && (\n <span className=\"text-[11px] text-muted-foreground\">{metaText}</span>\n )}\n </div>\n\n {/* Expanded feedback area */}\n {expanded && feedback && (\n <div className=\"space-y-3\">\n {/* Prompt text */}\n <p className=\"text-xs text-muted-foreground\">\n {feedback === \"negative\" ? negativePrompt : positivePrompt}\n </p>\n\n {/* Chip area */}\n {feedback === \"negative\" && negativeChips.length > 0 && (\n <div className=\"space-y-2\">\n {/* Tier-1 chips */}\n <FeedbackChipGroup\n chips={negativeChips.map((c) => c.label)}\n selected={allSelectedChips}\n onToggle={handleTier1Toggle}\n flavor=\"negative\"\n />\n\n {/* Tier-2 sub-chips (shown when a tier-1 with sub-chips is active) */}\n {activeTree && activeTree.subChips && (\n <div className=\"pl-3 space-y-1.5\">\n {activeTree.subPrompt && (\n <p className=\"text-[11px] text-muted-foreground\">\n {activeTree.subPrompt}\n </p>\n )}\n <FeedbackChipGroup\n chips={activeTree.subChips}\n selected={selectedTier2 ? [selectedTier2] : []}\n onToggle={handleTier2Toggle}\n flavor=\"negative\"\n />\n </div>\n )}\n </div>\n )}\n\n {feedback === \"positive\" && positiveChips.length > 0 && (\n <FeedbackChipGroup\n chips={positiveChips}\n selected={allSelectedChips}\n onToggle={handlePositiveChipToggle}\n flavor=\"positive\"\n />\n )}\n\n {/* Detail text input */}\n <FeedbackInput\n placeholder=\"Add optional detail…\"\n value={detailText}\n onChange={setDetailText}\n onSubmit={handleSubmit}\n />\n\n {/* Action buttons */}\n <FeedbackActions onSubmit={handleSubmit} onCancel={handleCancel} />\n </div>\n )}\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/feedback-primitives.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ThumbsUp, ThumbsDown, Check, Pencil } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Structured feedback data shape.\n *\n * Preserves the tree structure for DB persistence:\n * reasonTop -> tier-1 chip label (maps to case_feedback.reason_top)\n * reasonSub -> tier-2 sub-chip label (maps to case_feedback.reason_sub)\n * pills -> any additional selected chips (maps to case_feedback.pills)\n * detail -> free-text input (maps to case_feedback.free_text)\n */\nexport interface FeedbackSubmitData {\n sentiment: \"positive\" | \"negative\"\n reasonTop?: string\n reasonSub?: string\n pills: string[]\n detail: string\n}\n\n/**\n * Persisted feedback data from a previous submission, used to hydrate the\n * footer into its \"already submitted\" visual state.\n */\nexport interface PersistedFeedbackData {\n sentiment: \"positive\" | \"negative\"\n reasonTop?: string\n reasonSub?: string\n pills?: string[]\n detail?: string\n ownershipLabel: \"Your feedback\" | \"Team feedback\"\n}\n\n/**\n * Defines a tier-1 chip that may have tier-2 sub-chips.\n */\nexport interface FeedbackChipTree {\n label: string\n subPrompt?: string\n subChips?: string[]\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackChipGroup\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackChipGroupProps {\n chips: string[]\n selected: string[]\n onToggle: (chip: string) => void\n flavor: \"positive\" | \"negative\"\n className?: string\n}\n\nconst CHIP_SELECTED_CLASSES: Record<\"positive\" | \"negative\", string> = {\n negative: \"bg-red-50 text-red-700 border-red-200\",\n positive: \"bg-muted text-foreground border-border\",\n}\n\nconst CHIP_IDLE_CLASS =\n \"bg-background text-muted-foreground border-border hover:bg-muted/50\"\n\nexport function FeedbackChipGroup({\n chips,\n selected,\n onToggle,\n flavor,\n className,\n}: FeedbackChipGroupProps) {\n return (\n <div className={cn(\"flex flex-wrap gap-1.5\", className)}>\n {chips.map((chip) => {\n const isSelected = selected.includes(chip)\n return (\n <button\n key={chip}\n type=\"button\"\n onClick={() => onToggle(chip)}\n className={cn(\n \"rounded-md px-2.5 py-1 text-[11px] font-medium border transition-colors\",\n isSelected ? CHIP_SELECTED_CLASSES[flavor] : CHIP_IDLE_CLASS,\n )}\n >\n {chip}\n </button>\n )\n })}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackInput\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackInputProps {\n placeholder?: string\n value: string\n onChange: (value: string) => void\n onSubmit?: () => void\n className?: string\n}\n\nexport function FeedbackInput({\n placeholder,\n value,\n onChange,\n onSubmit,\n className,\n}: FeedbackInputProps) {\n return (\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && onSubmit) {\n e.preventDefault()\n onSubmit()\n }\n }}\n placeholder={placeholder}\n className={cn(\n \"w-full text-xs bg-background border border-border rounded-md px-2.5 py-2 text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring\",\n className,\n )}\n />\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackActions\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackActionsProps {\n onSubmit: () => void\n onCancel: () => void\n submitDisabled?: boolean\n submitLabel?: string\n cancelLabel?: string\n hint?: string\n className?: string\n}\n\nexport function FeedbackActions({\n onSubmit,\n onCancel,\n submitDisabled = false,\n submitLabel = \"Submit\",\n cancelLabel = \"Cancel\",\n hint,\n className,\n}: FeedbackActionsProps) {\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <button\n type=\"button\"\n onClick={onSubmit}\n disabled={submitDisabled}\n className=\"bg-foreground text-background rounded-md px-3 py-1.5 text-xs font-semibold disabled:opacity-50\"\n >\n {submitLabel}\n </button>\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"border border-border rounded-md px-3 py-1.5 text-xs font-medium\"\n >\n {cancelLabel}\n </button>\n {hint && (\n <span className=\"ml-auto text-[11px] text-muted-foreground\">\n {hint}\n </span>\n )}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// FeedbackFooter\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackFooterProps {\n feedback: \"positive\" | \"negative\" | null\n onFeedbackChange: (value: \"positive\" | \"negative\" | null) => void\n onSubmit: (data: FeedbackSubmitData) => void\n metaText?: string\n positivePrompt?: string\n negativePrompt?: string\n negativeChips?: FeedbackChipTree[]\n positiveChips?: string[]\n className?: string\n /** Pre-existing feedback to hydrate from (e.g. after page reload). */\n initialFeedback?: PersistedFeedbackData | null\n /** Label shown in the transient confirmation pill after submit. */\n submittedLabel?: string\n /** Stable key for syncing initialFeedback into local state. When this\n * changes, the component resets to the new initialFeedback value. */\n feedbackKey?: string\n}\n\nconst SENTIMENT_BUTTON_ACTIVE: Record<\"positive\" | \"negative\", string> = {\n negative: \"text-red-600 bg-red-50 border-red-200\",\n positive: \"text-foreground bg-muted border-border\",\n}\n\nconst SENTIMENT_BUTTON_IDLE =\n \"text-muted-foreground hover:text-foreground\"\n\nexport function FeedbackFooter({\n feedback,\n onFeedbackChange,\n onSubmit,\n metaText,\n positivePrompt = \"Thanks! Anything to keep about this score?\",\n negativePrompt = \"What's the issue?\",\n negativeChips = [],\n positiveChips = [],\n className,\n initialFeedback,\n submittedLabel = \"Saved\",\n feedbackKey,\n}: FeedbackFooterProps) {\n const [expanded, setExpanded] = React.useState(false)\n const [selectedTier1, setSelectedTier1] = React.useState<string | null>(null)\n const [selectedTier2, setSelectedTier2] = React.useState<string | null>(null)\n const [additionalPills, setAdditionalPills] = React.useState<string[]>([])\n const [detailText, setDetailText] = React.useState(\"\")\n const [activeTreeIndex, setActiveTreeIndex] = React.useState<number | null>(\n null,\n )\n /** Transient \"Saved\" confirmation — shown after successful submit. */\n const [submitted, setSubmitted] = React.useState(false)\n /** Persisted feedback shown as a clickable indicator (survives reload). */\n const [persisted, setPersisted] = React.useState<PersistedFeedbackData | null>(\n initialFeedback ?? null,\n )\n /** Tracks whether the user is actively editing (ref to guard against prop overwrites without triggering re-syncs). */\n const isEditingRef = React.useRef(false)\n /** Track the last synced feedbackKey to detect key changes. */\n const lastKeyRef = React.useRef<string | undefined>(feedbackKey)\n\n /** Helper to update the editing ref. */\n const setIsEditing = React.useCallback((value: boolean) => {\n isEditingRef.current = value\n }, [])\n\n // Sync initialFeedback into local state via useEffect keyed on feedbackKey.\n // When feedbackKey changes, reset to new target. Preserve active edits\n // when feedbackKey stays the same.\n React.useEffect(() => {\n const keyChanged = feedbackKey !== lastKeyRef.current\n lastKeyRef.current = feedbackKey\n\n if (keyChanged) {\n // Key changed — full reset to new target\n setPersisted(initialFeedback ?? null)\n setSubmitted(false)\n setExpanded(false)\n isEditingRef.current = false\n if (initialFeedback) {\n onFeedbackChange(initialFeedback.sentiment)\n } else {\n onFeedbackChange(null)\n }\n } else if (!isEditingRef.current) {\n // Same key, not actively editing — safe to sync\n setPersisted(initialFeedback ?? null)\n if (initialFeedback) {\n onFeedbackChange(initialFeedback.sentiment)\n }\n }\n }, [initialFeedback, feedbackKey, onFeedbackChange])\n\n // Reset state when feedback collapses\n const resetState = React.useCallback(() => {\n setExpanded(false)\n setSelectedTier1(null)\n setSelectedTier2(null)\n setAdditionalPills([])\n setDetailText(\"\")\n setActiveTreeIndex(null)\n setIsEditing(false)\n }, [setIsEditing])\n\n const handleSentimentClick = React.useCallback(\n (sentiment: \"positive\" | \"negative\") => {\n onFeedbackChange(sentiment)\n // Reset chip state when switching sentiment, then expand\n resetState()\n setExpanded(true)\n setSubmitted(false)\n setPersisted(null)\n setIsEditing(true)\n },\n [onFeedbackChange, resetState, setIsEditing],\n )\n\n /** Open the persisted indicator for editing. */\n const handlePersistedClick = React.useCallback(() => {\n if (!persisted) return\n onFeedbackChange(persisted.sentiment)\n setSelectedTier1(persisted.reasonTop ?? null)\n setSelectedTier2(persisted.reasonSub ?? null)\n setAdditionalPills(persisted.pills ?? [])\n setDetailText(persisted.detail ?? \"\")\n setExpanded(true)\n setSubmitted(false)\n setIsEditing(true)\n }, [persisted, onFeedbackChange, setIsEditing])\n\n const handleTier1Toggle = React.useCallback(\n (chipLabel: string) => {\n if (selectedTier1 === chipLabel) {\n // Deselect the tier-1 chip\n setSelectedTier1(null)\n setSelectedTier2(null)\n setActiveTreeIndex(null)\n } else if (selectedTier1 === null) {\n // First selection becomes the primary reasonTop\n setSelectedTier1(chipLabel)\n setSelectedTier2(null)\n // Find the chip's tree index to show sub-chips\n const idx = negativeChips.findIndex((c) => c.label === chipLabel)\n if (idx !== -1 && negativeChips[idx].subChips) {\n setActiveTreeIndex(idx)\n } else {\n setActiveTreeIndex(null)\n }\n } else {\n // Additional selections become pills\n setAdditionalPills((prev) =>\n prev.includes(chipLabel)\n ? prev.filter((p) => p !== chipLabel)\n : [...prev, chipLabel],\n )\n }\n },\n [selectedTier1, negativeChips],\n )\n\n const handleTier2Toggle = React.useCallback((subChip: string) => {\n setSelectedTier2((prev) => (prev === subChip ? null : subChip))\n }, [])\n\n const handlePositiveChipToggle = React.useCallback(\n (chip: string) => {\n if (selectedTier1 === chip) {\n setSelectedTier1(null)\n } else if (selectedTier1 === null) {\n setSelectedTier1(chip)\n } else {\n setAdditionalPills((prev) =>\n prev.includes(chip)\n ? prev.filter((p) => p !== chip)\n : [...prev, chip],\n )\n }\n },\n [selectedTier1],\n )\n\n const handleSubmit = React.useCallback(() => {\n if (!feedback) return\n onSubmit({\n sentiment: feedback,\n reasonTop: selectedTier1 ?? undefined,\n reasonSub: selectedTier2 ?? undefined,\n pills: additionalPills,\n detail: detailText,\n })\n // Show transient \"Saved\" confirmation\n setSubmitted(true)\n // Collapse expansion but keep sentiment visible\n resetState()\n }, [\n feedback,\n selectedTier1,\n selectedTier2,\n additionalPills,\n detailText,\n onSubmit,\n resetState,\n ])\n\n const handleCancel = React.useCallback(() => {\n resetState()\n onFeedbackChange(null)\n }, [resetState, onFeedbackChange])\n\n // Determine which chips are selected (combining tier1 + additionalPills)\n const allSelectedChips = React.useMemo(() => {\n const result: string[] = []\n if (selectedTier1) result.push(selectedTier1)\n result.push(...additionalPills)\n return result\n }, [selectedTier1, additionalPills])\n\n // Active tier-1 chip tree (for showing sub-chips)\n const activeTree =\n activeTreeIndex !== null ? negativeChips[activeTreeIndex] : null\n\n // Determine if we should show the persisted indicator instead of bare buttons\n const showPersistedIndicator = persisted && !expanded && !submitted\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* Sentiment buttons + meta text bar */}\n <div className=\"flex items-center justify-between\">\n {showPersistedIndicator ? (\n /* Persisted feedback indicator — clickable to reopen editor */\n <button\n type=\"button\"\n onClick={handlePersistedClick}\n className=\"group flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors\"\n data-testid=\"persisted-feedback-indicator\"\n >\n <span className=\"font-medium\">{persisted.ownershipLabel}:</span>\n {persisted.sentiment === \"positive\" ? (\n <ThumbsUp className=\"h-[11px] w-[11px]\" />\n ) : (\n <ThumbsDown className=\"h-[11px] w-[11px]\" />\n )}\n {persisted.detail && (\n <span className=\"max-w-[200px] truncate text-muted-foreground/70\">\n {persisted.detail}\n </span>\n )}\n <Pencil className=\"h-[9px] w-[9px] opacity-0 group-hover:opacity-100 transition-opacity\" />\n </button>\n ) : (\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => handleSentimentClick(\"positive\")}\n className={cn(\n \"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors\",\n feedback === \"positive\"\n ? SENTIMENT_BUTTON_ACTIVE.positive\n : SENTIMENT_BUTTON_IDLE,\n )}\n >\n <ThumbsUp className=\"h-[11px] w-[11px]\" />\n Helpful\n </button>\n <button\n type=\"button\"\n onClick={() => handleSentimentClick(\"negative\")}\n className={cn(\n \"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors\",\n feedback === \"negative\"\n ? SENTIMENT_BUTTON_ACTIVE.negative\n : SENTIMENT_BUTTON_IDLE,\n )}\n >\n <ThumbsDown className=\"h-[11px] w-[11px]\" />\n Not helpful\n </button>\n {/* Transient \"Saved\" confirmation pill */}\n {submitted && feedback && (\n <span\n className=\"inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600\"\n role=\"status\"\n data-testid=\"feedback-submitted-pill\"\n >\n <Check className=\"h-[11px] w-[11px]\" />\n {submittedLabel}\n </span>\n )}\n </div>\n )}\n {metaText && (\n <span className=\"text-[11px] text-muted-foreground\">{metaText}</span>\n )}\n </div>\n\n {/* Expanded feedback area */}\n {expanded && feedback && (\n <div className=\"space-y-3\">\n {/* Prompt text */}\n <p className=\"text-xs text-muted-foreground\">\n {feedback === \"negative\" ? negativePrompt : positivePrompt}\n </p>\n\n {/* Chip area */}\n {feedback === \"negative\" && negativeChips.length > 0 && (\n <div className=\"space-y-2\">\n {/* Tier-1 chips */}\n <FeedbackChipGroup\n chips={negativeChips.map((c) => c.label)}\n selected={allSelectedChips}\n onToggle={handleTier1Toggle}\n flavor=\"negative\"\n />\n\n {/* Tier-2 sub-chips (shown when a tier-1 with sub-chips is active) */}\n {activeTree && activeTree.subChips && (\n <div className=\"pl-3 space-y-1.5\">\n {activeTree.subPrompt && (\n <p className=\"text-[11px] text-muted-foreground\">\n {activeTree.subPrompt}\n </p>\n )}\n <FeedbackChipGroup\n chips={activeTree.subChips}\n selected={selectedTier2 ? [selectedTier2] : []}\n onToggle={handleTier2Toggle}\n flavor=\"negative\"\n />\n </div>\n )}\n </div>\n )}\n\n {feedback === \"positive\" && positiveChips.length > 0 && (\n <FeedbackChipGroup\n chips={positiveChips}\n selected={allSelectedChips}\n onToggle={handlePositiveChipToggle}\n flavor=\"positive\"\n />\n )}\n\n {/* Detail text input */}\n <FeedbackInput\n placeholder=\"Add optional detail…\"\n value={detailText}\n onChange={setDetailText}\n onSubmit={handleSubmit}\n />\n\n {/* Action buttons */}\n <FeedbackActions onSubmit={handleSubmit} onCancel={handleCancel} />\n </div>\n )}\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// InlineFeedbackControl — shared thumb+detail inline feedback widget\n// ---------------------------------------------------------------------------\n\nexport interface InlineFeedbackControlProps {\n /** Unique key identifying the feedback target (e.g. factor key). */\n feedbackKey: string\n /** Persisted/initial feedback to hydrate from. */\n initialFeedback?: { type: \"up\" | \"down\"; detail: string; ownershipLabel?: string }\n /** Called when user submits or clears feedback. */\n onFeedback?: (key: string, type: \"up\" | \"down\" | null, detail?: string) => void\n /** Test ID prefix for all sub-elements. */\n testIdPrefix?: string\n}\n\n/**\n * Compact inline thumb-up/thumb-down feedback with optional detail text.\n * Used by PriorityFactorRow and any other component that needs\n * a lightweight feedback control.\n */\nexport function InlineFeedbackControl({\n feedbackKey,\n initialFeedback,\n onFeedback,\n testIdPrefix = \"inline-feedback\",\n}: InlineFeedbackControlProps) {\n const [thumbState, setThumbState] = React.useState<\"up\" | \"down\" | null>(\n initialFeedback?.type ?? null,\n )\n const [showInput, setShowInput] = React.useState(false)\n const [detailText, setDetailText] = React.useState(initialFeedback?.detail ?? \"\")\n const [saved, setSaved] = React.useState(!!initialFeedback)\n const [savedDetail, setSavedDetail] = React.useState(initialFeedback?.detail ?? \"\")\n const ownershipLabel = initialFeedback?.ownershipLabel ?? \"Your feedback\"\n\n // Sync with initialFeedback prop changes\n React.useEffect(() => {\n if (initialFeedback) {\n setThumbState(initialFeedback.type)\n setSaved(true)\n setSavedDetail(initialFeedback.detail)\n }\n }, [initialFeedback])\n\n const handleThumbClick = React.useCallback(\n (type: \"up\" | \"down\") => {\n if (thumbState === type) {\n // Toggle off\n setThumbState(null)\n setShowInput(false)\n setSaved(false)\n onFeedback?.(feedbackKey, null)\n } else {\n setThumbState(type)\n setShowInput(true)\n setSaved(false)\n }\n },\n [thumbState, feedbackKey, onFeedback],\n )\n\n const handleSubmitDetail = React.useCallback(() => {\n if (!thumbState) return\n const text = detailText.trim()\n onFeedback?.(feedbackKey, thumbState, text)\n setSaved(true)\n setSavedDetail(text)\n setShowInput(false)\n }, [thumbState, detailText, feedbackKey, onFeedback])\n\n return (\n <div>\n {saved && !showInput ? (\n /* Persisted / saved indicator */\n <button\n type=\"button\"\n onClick={() => {\n setDetailText(savedDetail)\n setShowInput(true)\n setSaved(false)\n }}\n className=\"group flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors\"\n data-testid={`${testIdPrefix}-feedback-persisted-${feedbackKey}`}\n >\n <span className=\"font-medium\">{ownershipLabel}:</span>\n {thumbState === \"up\" ? (\n <ThumbsUp className=\"h-[10px] w-[10px]\" />\n ) : (\n <ThumbsDown className=\"h-[10px] w-[10px]\" />\n )}\n {savedDetail && (\n <span className=\"max-w-[180px] truncate text-muted-foreground/70\">\n {savedDetail}\n </span>\n )}\n <Pencil className=\"h-[9px] w-[9px] opacity-0 group-hover:opacity-100 transition-opacity\" />\n </button>\n ) : (\n <div className=\"flex items-center gap-1.5\">\n {/* Inline thumb buttons */}\n <button\n type=\"button\"\n onClick={() => handleThumbClick(\"up\")}\n className={cn(\n \"p-1 rounded transition-colors\",\n thumbState === \"up\"\n ? \"text-foreground bg-muted\"\n : \"text-muted-foreground/40 hover:text-foreground hover:bg-muted/50\",\n )}\n title=\"This is accurate\"\n data-testid={`${testIdPrefix}-thumb-up-${feedbackKey}`}\n >\n <ThumbsUp className=\"h-[10px] w-[10px]\" />\n </button>\n <button\n type=\"button\"\n onClick={() => handleThumbClick(\"down\")}\n className={cn(\n \"p-1 rounded transition-colors\",\n thumbState === \"down\"\n ? \"text-red-600 bg-red-50\"\n : \"text-muted-foreground/40 hover:text-red-600 hover:bg-red-50/50\",\n )}\n title=\"Report issue\"\n data-testid={`${testIdPrefix}-thumb-down-${feedbackKey}`}\n >\n <ThumbsDown className=\"h-[10px] w-[10px]\" />\n </button>\n\n {/* Transient \"Saved\" pill */}\n {saved && (\n <span\n className=\"inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600\"\n role=\"status\"\n data-testid={`${testIdPrefix}-saved-${feedbackKey}`}\n >\n <Check className=\"h-[10px] w-[10px]\" />\n Saved\n </span>\n )}\n </div>\n )}\n\n {/* Inline detail input */}\n {showInput && thumbState && (\n <div className=\"mt-1.5\">\n <input\n type=\"text\"\n value={detailText}\n onChange={(e) => setDetailText(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") handleSubmitDetail()\n if (e.key === \"Escape\") setShowInput(false)\n }}\n placeholder={\n thumbState === \"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 data-testid={`${testIdPrefix}-detail-input-${feedbackKey}`}\n />\n <div className=\"mt-1 flex items-center gap-1.5\">\n <button\n type=\"button\"\n onClick={handleSubmitDetail}\n className=\"bg-foreground text-background rounded px-2 py-0.5 text-[10px] font-semibold\"\n data-testid={`${testIdPrefix}-submit-${feedbackKey}`}\n >\n Submit\n </button>\n <button\n type=\"button\"\n onClick={() => setShowInput(false)}\n className=\"border border-border rounded px-2 py-0.5 text-[10px] font-medium\"\n >\n Cancel\n </button>\n </div>\n </div>\n )}\n </div>\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -17,14 +17,10 @@ import {
|
|
|
17
17
|
Minus,
|
|
18
18
|
ChevronDown,
|
|
19
19
|
ChevronUp,
|
|
20
|
-
Info
|
|
21
|
-
ThumbsUp,
|
|
22
|
-
ThumbsDown,
|
|
23
|
-
Check,
|
|
24
|
-
Pencil
|
|
20
|
+
Info
|
|
25
21
|
} from "lucide-react";
|
|
26
22
|
import { cn } from "../lib/utils.js";
|
|
27
|
-
import { FeedbackFooter } from "./feedback-primitives.js";
|
|
23
|
+
import { FeedbackFooter, InlineFeedbackControl } from "./feedback-primitives.js";
|
|
28
24
|
import { getSignalScoreUrgencyLabel, scoreRangeForUrgency, SIGNAL_TONE_CLASSES } from "./score-why-chips.js";
|
|
29
25
|
const URGENCY_TRIGGER_DEFAULT = {
|
|
30
26
|
Urgent: "border-red-200 bg-red-50 text-red-700",
|
|
@@ -85,49 +81,11 @@ function DirectionIcon({ direction }) {
|
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
83
|
function PriorityFactorRow({ factor, initialFeedback, onFactorFeedback }) {
|
|
88
|
-
var _a
|
|
84
|
+
var _a;
|
|
89
85
|
const IconComponent = (_a = FACTOR_ICONS[factor.icon]) != null ? _a : Activity;
|
|
90
86
|
const toneClasses = TONE_ICON_CLASSES[factor.tone];
|
|
91
87
|
const directionClasses = DIRECTION_CLASSES[factor.direction];
|
|
92
88
|
const directionLabel = factor.direction === "raises" ? "Raises" : factor.direction === "lowers" ? "Lowers" : "Neutral";
|
|
93
|
-
const [thumbState, setThumbState] = React.useState(
|
|
94
|
-
(_b = initialFeedback == null ? void 0 : initialFeedback.type) != null ? _b : null
|
|
95
|
-
);
|
|
96
|
-
const [showInput, setShowInput] = React.useState(false);
|
|
97
|
-
const [detailText, setDetailText] = React.useState((_c = initialFeedback == null ? void 0 : initialFeedback.detail) != null ? _c : "");
|
|
98
|
-
const [saved, setSaved] = React.useState(!!initialFeedback);
|
|
99
|
-
const [savedDetail, setSavedDetail] = React.useState((_d = initialFeedback == null ? void 0 : initialFeedback.detail) != null ? _d : "");
|
|
100
|
-
const ownershipLabel = (_e = initialFeedback == null ? void 0 : initialFeedback.ownershipLabel) != null ? _e : "Your feedback";
|
|
101
|
-
React.useEffect(() => {
|
|
102
|
-
if (initialFeedback) {
|
|
103
|
-
setThumbState(initialFeedback.type);
|
|
104
|
-
setSaved(true);
|
|
105
|
-
setSavedDetail(initialFeedback.detail);
|
|
106
|
-
}
|
|
107
|
-
}, [initialFeedback]);
|
|
108
|
-
const handleThumbClick = React.useCallback(
|
|
109
|
-
(type) => {
|
|
110
|
-
if (thumbState === type) {
|
|
111
|
-
setThumbState(null);
|
|
112
|
-
setShowInput(false);
|
|
113
|
-
setSaved(false);
|
|
114
|
-
onFactorFeedback == null ? void 0 : onFactorFeedback(factor.key, null);
|
|
115
|
-
} else {
|
|
116
|
-
setThumbState(type);
|
|
117
|
-
setShowInput(true);
|
|
118
|
-
setSaved(false);
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
[thumbState, factor.key, onFactorFeedback]
|
|
122
|
-
);
|
|
123
|
-
const handleSubmitDetail = React.useCallback(() => {
|
|
124
|
-
if (!thumbState) return;
|
|
125
|
-
const text = detailText.trim();
|
|
126
|
-
onFactorFeedback == null ? void 0 : onFactorFeedback(factor.key, thumbState, text);
|
|
127
|
-
setSaved(true);
|
|
128
|
-
setSavedDetail(text);
|
|
129
|
-
setShowInput(false);
|
|
130
|
-
}, [thumbState, detailText, factor.key, onFactorFeedback]);
|
|
131
89
|
return /* @__PURE__ */ jsxs(
|
|
132
90
|
"div",
|
|
133
91
|
{
|
|
@@ -178,112 +136,15 @@ function PriorityFactorRow({ factor, initialFeedback, onFactorFeedback }) {
|
|
|
178
136
|
/* @__PURE__ */ jsx("div", {}),
|
|
179
137
|
onFactorFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
180
138
|
/* @__PURE__ */ jsx("div", {}),
|
|
181
|
-
/* @__PURE__ */
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
setShowInput(true);
|
|
191
|
-
setSaved(false);
|
|
192
|
-
},
|
|
193
|
-
className: "group flex items-center gap-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors",
|
|
194
|
-
"data-testid": `factor-feedback-persisted-${factor.key}`,
|
|
195
|
-
children: [
|
|
196
|
-
/* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
|
|
197
|
-
ownershipLabel,
|
|
198
|
-
":"
|
|
199
|
-
] }),
|
|
200
|
-
thumbState === "up" ? /* @__PURE__ */ jsx(ThumbsUp, { className: "h-[10px] w-[10px]" }) : /* @__PURE__ */ jsx(ThumbsDown, { className: "h-[10px] w-[10px]" }),
|
|
201
|
-
savedDetail && /* @__PURE__ */ jsx("span", { className: "max-w-[180px] truncate text-muted-foreground/70", children: savedDetail }),
|
|
202
|
-
/* @__PURE__ */ jsx(Pencil, { className: "h-[9px] w-[9px] opacity-0 group-hover:opacity-100 transition-opacity" })
|
|
203
|
-
]
|
|
204
|
-
}
|
|
205
|
-
)
|
|
206
|
-
) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
207
|
-
/* @__PURE__ */ jsx(
|
|
208
|
-
"button",
|
|
209
|
-
{
|
|
210
|
-
type: "button",
|
|
211
|
-
onClick: () => handleThumbClick("up"),
|
|
212
|
-
className: cn(
|
|
213
|
-
"p-1 rounded transition-colors",
|
|
214
|
-
thumbState === "up" ? "text-foreground bg-muted" : "text-muted-foreground/40 hover:text-foreground hover:bg-muted/50"
|
|
215
|
-
),
|
|
216
|
-
title: "This factor is accurate",
|
|
217
|
-
"data-testid": `factor-thumb-up-${factor.key}`,
|
|
218
|
-
children: /* @__PURE__ */ jsx(ThumbsUp, { className: "h-[10px] w-[10px]" })
|
|
219
|
-
}
|
|
220
|
-
),
|
|
221
|
-
/* @__PURE__ */ jsx(
|
|
222
|
-
"button",
|
|
223
|
-
{
|
|
224
|
-
type: "button",
|
|
225
|
-
onClick: () => handleThumbClick("down"),
|
|
226
|
-
className: cn(
|
|
227
|
-
"p-1 rounded transition-colors",
|
|
228
|
-
thumbState === "down" ? "text-red-600 bg-red-50" : "text-muted-foreground/40 hover:text-red-600 hover:bg-red-50/50"
|
|
229
|
-
),
|
|
230
|
-
title: "Report issue with this factor",
|
|
231
|
-
"data-testid": `factor-thumb-down-${factor.key}`,
|
|
232
|
-
children: /* @__PURE__ */ jsx(ThumbsDown, { className: "h-[10px] w-[10px]" })
|
|
233
|
-
}
|
|
234
|
-
),
|
|
235
|
-
saved && /* @__PURE__ */ jsxs(
|
|
236
|
-
"span",
|
|
237
|
-
{
|
|
238
|
-
className: "inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600",
|
|
239
|
-
role: "status",
|
|
240
|
-
"data-testid": `factor-saved-${factor.key}`,
|
|
241
|
-
children: [
|
|
242
|
-
/* @__PURE__ */ jsx(Check, { className: "h-[10px] w-[10px]" }),
|
|
243
|
-
"Saved"
|
|
244
|
-
]
|
|
245
|
-
}
|
|
246
|
-
)
|
|
247
|
-
] }),
|
|
248
|
-
showInput && thumbState && /* @__PURE__ */ jsxs("div", { className: "mt-1.5", children: [
|
|
249
|
-
/* @__PURE__ */ jsx(
|
|
250
|
-
"input",
|
|
251
|
-
{
|
|
252
|
-
type: "text",
|
|
253
|
-
value: detailText,
|
|
254
|
-
onChange: (e) => setDetailText(e.target.value),
|
|
255
|
-
onKeyDown: (e) => {
|
|
256
|
-
if (e.key === "Enter") handleSubmitDetail();
|
|
257
|
-
if (e.key === "Escape") setShowInput(false);
|
|
258
|
-
},
|
|
259
|
-
placeholder: thumbState === "up" ? "What\u2019s accurate? (optional)" : "What\u2019s wrong? (optional)",
|
|
260
|
-
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",
|
|
261
|
-
"data-testid": `factor-detail-input-${factor.key}`
|
|
262
|
-
}
|
|
263
|
-
),
|
|
264
|
-
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-1.5", children: [
|
|
265
|
-
/* @__PURE__ */ jsx(
|
|
266
|
-
"button",
|
|
267
|
-
{
|
|
268
|
-
type: "button",
|
|
269
|
-
onClick: handleSubmitDetail,
|
|
270
|
-
className: "bg-foreground text-background rounded px-2 py-0.5 text-[10px] font-semibold",
|
|
271
|
-
"data-testid": `factor-submit-${factor.key}`,
|
|
272
|
-
children: "Submit"
|
|
273
|
-
}
|
|
274
|
-
),
|
|
275
|
-
/* @__PURE__ */ jsx(
|
|
276
|
-
"button",
|
|
277
|
-
{
|
|
278
|
-
type: "button",
|
|
279
|
-
onClick: () => setShowInput(false),
|
|
280
|
-
className: "border border-border rounded px-2 py-0.5 text-[10px] font-medium",
|
|
281
|
-
children: "Cancel"
|
|
282
|
-
}
|
|
283
|
-
)
|
|
284
|
-
] })
|
|
285
|
-
] })
|
|
286
|
-
] })
|
|
139
|
+
/* @__PURE__ */ jsx("div", { className: "col-span-2 mt-1", children: /* @__PURE__ */ jsx(
|
|
140
|
+
InlineFeedbackControl,
|
|
141
|
+
{
|
|
142
|
+
feedbackKey: factor.key,
|
|
143
|
+
initialFeedback,
|
|
144
|
+
onFeedback: onFactorFeedback,
|
|
145
|
+
testIdPrefix: "factor"
|
|
146
|
+
}
|
|
147
|
+
) })
|
|
287
148
|
] })
|
|
288
149
|
]
|
|
289
150
|
}
|