@handled-ai/design-system 0.18.2 → 0.18.3
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/charts/chart.d.ts +1 -1
- package/dist/components/feedback-primitives.d.ts +2 -21
- package/dist/components/feedback-primitives.js +6 -90
- package/dist/components/feedback-primitives.js.map +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/score-why-chips.js +5 -26
- package/dist/components/score-why-chips.js.map +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +7 -172
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/components/timeline-activity.d.ts +16 -1
- package/dist/components/timeline-activity.js +69 -1
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js.map +1 -1
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-accounts-view.d.ts +1 -1
- package/dist/prototype/prototype-admin-view.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +12 -2
- package/dist/prototype/prototype-inbox-view.js +102 -37
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +1 -1
- package/dist/prototype/prototype-shell.d.ts +1 -1
- package/dist/{signal-priority-popover-DWaAMhPI.d.ts → signal-priority-popover-DQ_VuHac.d.ts} +2 -26
- package/package.json +1 -3
- package/src/components/__tests__/timeline-activity.test.tsx +137 -0
- package/src/components/feedback-primitives.tsx +26 -148
- package/src/components/score-why-chips.tsx +2 -28
- package/src/components/signal-priority-popover.tsx +3 -194
- package/src/components/timeline-activity.tsx +112 -1
- package/src/index.ts +1 -1
- package/src/prototype/__tests__/detail-view-attention.test.tsx +2 -2
- package/src/prototype/__tests__/detail-view-timeline-system-events.test.tsx +322 -0
- package/src/prototype/prototype-config.ts +1 -11
- package/src/prototype/prototype-inbox-view.tsx +131 -33
- package/src/components/__tests__/wit-636-feedback-states.test.tsx +0 -546
package/dist/{signal-priority-popover-DWaAMhPI.d.ts → signal-priority-popover-DQ_VuHac.d.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { FeedbackChipTree, FeedbackSubmitData } from './components/feedback-primitives.js';
|
|
3
3
|
import { SidebarNavSection } from './components/quick-action-sidebar-nav.js';
|
|
4
4
|
import { ScoreFactor } from './components/score-breakdown.js';
|
|
5
5
|
import { SuggestedContact, SuggestedAction } from './components/suggested-actions.js';
|
|
@@ -48,10 +48,6 @@ interface SignalScoreExplanationSignal {
|
|
|
48
48
|
type: string;
|
|
49
49
|
count: number;
|
|
50
50
|
}>;
|
|
51
|
-
/** Current balance value (e.g., "$3.0M") for balance context strip. */
|
|
52
|
-
currentBalance?: string;
|
|
53
|
-
/** Additional balance context text (e.g., "down from $23M"). */
|
|
54
|
-
balanceContext?: string;
|
|
55
51
|
}
|
|
56
52
|
interface SignalScoreExplanationBucket {
|
|
57
53
|
key: string;
|
|
@@ -97,16 +93,6 @@ interface SignalScoreData {
|
|
|
97
93
|
type: "up" | "down";
|
|
98
94
|
detail: string;
|
|
99
95
|
}>;
|
|
100
|
-
/** Factor-level feedback for the priority popover rows (keyed by factor key). */
|
|
101
|
-
initialFactorPopoverFeedback?: Record<string, {
|
|
102
|
-
type: "up" | "down";
|
|
103
|
-
detail: string;
|
|
104
|
-
ownershipLabel?: string;
|
|
105
|
-
}>;
|
|
106
|
-
/** Persisted bucket-level feedback, keyed by bucket key. */
|
|
107
|
-
initialBucketFeedback?: Record<string, PersistedFeedbackData>;
|
|
108
|
-
/** Persisted priority-level feedback for the popover footer. */
|
|
109
|
-
initialPriorityFeedback?: PersistedFeedbackData | null;
|
|
110
96
|
/** Priority factors for the popover breakdown. */
|
|
111
97
|
priorityFactors?: PriorityFactor[];
|
|
112
98
|
/** Negative feedback chip tree for the priority popover. */
|
|
@@ -398,17 +384,7 @@ interface SignalPriorityPopoverProps {
|
|
|
398
384
|
feedbackChips?: FeedbackChipTree[];
|
|
399
385
|
onFeedbackSubmit?: (data: FeedbackSubmitData) => void;
|
|
400
386
|
className?: string;
|
|
401
|
-
/** Persisted factor-level feedback (keyed by factor key). */
|
|
402
|
-
initialFactorFeedback?: Record<string, {
|
|
403
|
-
type: "up" | "down";
|
|
404
|
-
detail: string;
|
|
405
|
-
ownershipLabel?: string;
|
|
406
|
-
}>;
|
|
407
|
-
/** Callback when user submits factor-level feedback. */
|
|
408
|
-
onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void;
|
|
409
|
-
/** Persisted priority-level feedback for the footer. */
|
|
410
|
-
initialPriorityFeedback?: PersistedFeedbackData | null;
|
|
411
387
|
}
|
|
412
|
-
declare function SignalPriorityPopover({ score, urgencyLabel: providedLabel, urgencyExplanation, factors, metaText, feedbackChips, onFeedbackSubmit, className,
|
|
388
|
+
declare function SignalPriorityPopover({ score, urgencyLabel: providedLabel, urgencyExplanation, factors, metaText, feedbackChips, onFeedbackSubmit, className, }: SignalPriorityPopoverProps): React.JSX.Element;
|
|
413
389
|
|
|
414
390
|
export { type AccountFilterTab as A, type BriefStyleVariant as B, type EntityPanelConfig as E, type InboxDetailSections as I, type PriorityFactor as P, type QueueItem as Q, SignalPriorityPopover as S, type WorkQueueViewConfig as W, type AccountsViewConfig as a, type AdminTab as b, type AdminViewConfig as c, type EntityPanelSection as d, type InboxSortOption as e, type InboxViewConfig as f, type InsightsCustomTab as g, type InsightsViewConfig as h, type PrototypeBrandConfig as i, type PrototypeConfig as j, type SignalPriorityPopoverProps as k, type SignalScoreData as l, type SignalScoreExplanationBucket as m, type SignalScoreExplanationSignal as n, type SignalScoreUrgencyLabel as o };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@handled-ai/design-system",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
4
4
|
"description": "Handled UI component library (shadcn-style, New York)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@9.12.0",
|
|
@@ -171,11 +171,9 @@
|
|
|
171
171
|
"eslint": "^9.32.0",
|
|
172
172
|
"eslint-config-next": "15.3.1",
|
|
173
173
|
"happy-dom": "^20.9.0",
|
|
174
|
-
"lucide-react": "^1.16.0",
|
|
175
174
|
"next": "15.5.9",
|
|
176
175
|
"react": "19.1.0",
|
|
177
176
|
"react-dom": "19.1.0",
|
|
178
|
-
"recharts": "^3.8.1",
|
|
179
177
|
"shadcn": "^3.0.0",
|
|
180
178
|
"tailwindcss": "^4.1.11",
|
|
181
179
|
"three": "^0.183.1",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import React from "react"
|
|
3
|
+
import { render, screen } from "@testing-library/react"
|
|
4
|
+
import {
|
|
5
|
+
TimelineActivity,
|
|
6
|
+
TONE_CLASSES,
|
|
7
|
+
type TimelineEvent,
|
|
8
|
+
} from "../timeline-activity"
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
function minimal(overrides: Partial<TimelineEvent> = {}): TimelineEvent {
|
|
15
|
+
return {
|
|
16
|
+
id: "e1",
|
|
17
|
+
icon: React.createElement("span", { "data-testid": "icon" }, "⚡"),
|
|
18
|
+
title: "Test event",
|
|
19
|
+
time: "2h ago",
|
|
20
|
+
...overrides,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Tests
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
describe("TimelineActivity", () => {
|
|
29
|
+
// --- Tone rendering ---
|
|
30
|
+
|
|
31
|
+
it("renders red dot classes when tone is 'red'", () => {
|
|
32
|
+
const event = minimal({ tone: "red" })
|
|
33
|
+
const { container } = render(<TimelineActivity events={[event]} />)
|
|
34
|
+
const dot = container.querySelector('[data-testid="timeline-dot"]')!
|
|
35
|
+
expect(dot).not.toBeNull()
|
|
36
|
+
const cls = dot.className
|
|
37
|
+
// Should contain all the red tone dot classes
|
|
38
|
+
expect(cls).toContain("bg-red-50")
|
|
39
|
+
expect(cls).toContain("border-red-200")
|
|
40
|
+
// Should contain the red icon classes
|
|
41
|
+
expect(cls).toContain("text-red-600")
|
|
42
|
+
// Should NOT contain neutral classes
|
|
43
|
+
expect(cls).not.toContain("border-border/60")
|
|
44
|
+
expect(cls).not.toContain("text-muted-foreground")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("renders neutral dot classes when tone is absent", () => {
|
|
48
|
+
const event = minimal()
|
|
49
|
+
const { container } = render(<TimelineActivity events={[event]} />)
|
|
50
|
+
const dot = container.querySelector('[data-testid="timeline-dot"]')!
|
|
51
|
+
expect(dot).not.toBeNull()
|
|
52
|
+
const cls = dot.className
|
|
53
|
+
expect(cls).toContain("border-border/60")
|
|
54
|
+
expect(cls).toContain("bg-background")
|
|
55
|
+
expect(cls).toContain("text-muted-foreground")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// --- Actor byline ---
|
|
59
|
+
|
|
60
|
+
it("renders actor byline with name when actor.kind is 'user'", () => {
|
|
61
|
+
const event = minimal({
|
|
62
|
+
actor: { kind: "user", name: "Alice" },
|
|
63
|
+
})
|
|
64
|
+
render(<TimelineActivity events={[event]} />)
|
|
65
|
+
const byline = screen.getByTestId("actor-byline")
|
|
66
|
+
expect(byline).not.toBeNull()
|
|
67
|
+
expect(byline.textContent).toContain("Alice")
|
|
68
|
+
expect(byline.textContent).toContain("performed this action")
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("renders no byline when actor.kind is 'system'", () => {
|
|
72
|
+
const event = minimal({
|
|
73
|
+
actor: { kind: "system" },
|
|
74
|
+
})
|
|
75
|
+
const { container } = render(<TimelineActivity events={[event]} />)
|
|
76
|
+
const byline = container.querySelector('[data-testid="actor-byline"]')
|
|
77
|
+
expect(byline).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("renders 'Integration' text when actor.kind is 'integration'", () => {
|
|
81
|
+
const event = minimal({
|
|
82
|
+
actor: { kind: "integration" },
|
|
83
|
+
})
|
|
84
|
+
render(<TimelineActivity events={[event]} />)
|
|
85
|
+
const byline = screen.getByTestId("actor-byline")
|
|
86
|
+
expect(byline).not.toBeNull()
|
|
87
|
+
expect(byline.textContent).toContain("Integration")
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("renders custom verb for user actor", () => {
|
|
91
|
+
const event = minimal({
|
|
92
|
+
actor: { kind: "user", name: "Bob", verb: "approved this case" },
|
|
93
|
+
})
|
|
94
|
+
render(<TimelineActivity events={[event]} />)
|
|
95
|
+
const byline = screen.getByTestId("actor-byline")
|
|
96
|
+
expect(byline.textContent).toContain("approved this case")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("renders no byline when actor is absent", () => {
|
|
100
|
+
const event = minimal()
|
|
101
|
+
const { container } = render(<TimelineActivity events={[event]} />)
|
|
102
|
+
const byline = container.querySelector('[data-testid="actor-byline"]')
|
|
103
|
+
expect(byline).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// --- Backwards compatibility ---
|
|
107
|
+
|
|
108
|
+
it("renders correctly with minimal TimelineEvent (only id, icon, title, time)", () => {
|
|
109
|
+
const event: TimelineEvent = {
|
|
110
|
+
id: "min-1",
|
|
111
|
+
icon: React.createElement("span", null, "📌"),
|
|
112
|
+
title: "Minimal event",
|
|
113
|
+
time: "5m ago",
|
|
114
|
+
}
|
|
115
|
+
const { container } = render(<TimelineActivity events={[event]} />)
|
|
116
|
+
// Should render without errors
|
|
117
|
+
expect(container.textContent).toContain("Minimal event")
|
|
118
|
+
expect(container.textContent).toContain("5m ago")
|
|
119
|
+
// Dot should have neutral classes
|
|
120
|
+
const dot = container.querySelector('[data-testid="timeline-dot"]')!
|
|
121
|
+
expect(dot.className).toContain("border-border/60")
|
|
122
|
+
// No byline
|
|
123
|
+
const byline = container.querySelector('[data-testid="actor-byline"]')
|
|
124
|
+
expect(byline).toBeNull()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// --- TONE_CLASSES export ---
|
|
128
|
+
|
|
129
|
+
it("exports TONE_CLASSES with all expected tones", () => {
|
|
130
|
+
const tones = ["red", "amber", "emerald", "violet", "blue", "slate", "salesforce", "gmail"] as const
|
|
131
|
+
for (const tone of tones) {
|
|
132
|
+
expect(TONE_CLASSES[tone]).toBeDefined()
|
|
133
|
+
expect(TONE_CLASSES[tone].dot).toBeTruthy()
|
|
134
|
+
expect(TONE_CLASSES[tone].icon).toBeTruthy()
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
-
import { ThumbsUp, ThumbsDown
|
|
4
|
+
import { ThumbsUp, ThumbsDown } from "lucide-react"
|
|
5
5
|
import { cn } from "../lib/utils"
|
|
6
6
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
@@ -25,19 +25,6 @@ export interface FeedbackSubmitData {
|
|
|
25
25
|
detail: string
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* Persisted feedback data from a previous submission, used to hydrate the
|
|
30
|
-
* footer into its "already submitted" visual state.
|
|
31
|
-
*/
|
|
32
|
-
export interface PersistedFeedbackData {
|
|
33
|
-
sentiment: "positive" | "negative"
|
|
34
|
-
reasonTop?: string
|
|
35
|
-
reasonSub?: string
|
|
36
|
-
pills?: string[]
|
|
37
|
-
detail?: string
|
|
38
|
-
ownershipLabel: "Your feedback" | "Team feedback"
|
|
39
|
-
}
|
|
40
|
-
|
|
41
28
|
/**
|
|
42
29
|
* Defines a tier-1 chip that may have tier-2 sub-chips.
|
|
43
30
|
*/
|
|
@@ -198,13 +185,6 @@ export interface FeedbackFooterProps {
|
|
|
198
185
|
negativeChips?: FeedbackChipTree[]
|
|
199
186
|
positiveChips?: string[]
|
|
200
187
|
className?: string
|
|
201
|
-
/** Pre-existing feedback to hydrate from (e.g. after page reload). */
|
|
202
|
-
initialFeedback?: PersistedFeedbackData | null
|
|
203
|
-
/** Label shown in the transient confirmation pill after submit. */
|
|
204
|
-
submittedLabel?: string
|
|
205
|
-
/** Stable key for syncing initialFeedback into local state. When this
|
|
206
|
-
* changes, the component resets to the new initialFeedback value. */
|
|
207
|
-
feedbackKey?: string
|
|
208
188
|
}
|
|
209
189
|
|
|
210
190
|
const SENTIMENT_BUTTON_ACTIVE: Record<"positive" | "negative", string> = {
|
|
@@ -225,9 +205,6 @@ export function FeedbackFooter({
|
|
|
225
205
|
negativeChips = [],
|
|
226
206
|
positiveChips = [],
|
|
227
207
|
className,
|
|
228
|
-
initialFeedback,
|
|
229
|
-
submittedLabel = "Saved",
|
|
230
|
-
feedbackKey,
|
|
231
208
|
}: FeedbackFooterProps) {
|
|
232
209
|
const [expanded, setExpanded] = React.useState(false)
|
|
233
210
|
const [selectedTier1, setSelectedTier1] = React.useState<string | null>(null)
|
|
@@ -237,43 +214,6 @@ export function FeedbackFooter({
|
|
|
237
214
|
const [activeTreeIndex, setActiveTreeIndex] = React.useState<number | null>(
|
|
238
215
|
null,
|
|
239
216
|
)
|
|
240
|
-
/** Transient "Saved" confirmation — shown after successful submit. */
|
|
241
|
-
const [submitted, setSubmitted] = React.useState(false)
|
|
242
|
-
/** Persisted feedback shown as a clickable indicator (survives reload). */
|
|
243
|
-
const [persisted, setPersisted] = React.useState<PersistedFeedbackData | null>(
|
|
244
|
-
initialFeedback ?? null,
|
|
245
|
-
)
|
|
246
|
-
/** Tracks whether the user is actively editing (to guard against prop overwrites). */
|
|
247
|
-
const [isEditing, setIsEditing] = React.useState(false)
|
|
248
|
-
/** Track the last synced feedbackKey to detect key changes. */
|
|
249
|
-
const lastKeyRef = React.useRef<string | undefined>(feedbackKey)
|
|
250
|
-
|
|
251
|
-
// Sync initialFeedback into local state via useEffect keyed on feedbackKey.
|
|
252
|
-
// When feedbackKey changes, reset to new target. Preserve active edits
|
|
253
|
-
// when feedbackKey stays the same.
|
|
254
|
-
React.useEffect(() => {
|
|
255
|
-
const keyChanged = feedbackKey !== lastKeyRef.current
|
|
256
|
-
lastKeyRef.current = feedbackKey
|
|
257
|
-
|
|
258
|
-
if (keyChanged) {
|
|
259
|
-
// Key changed — full reset to new target
|
|
260
|
-
setPersisted(initialFeedback ?? null)
|
|
261
|
-
setSubmitted(false)
|
|
262
|
-
setExpanded(false)
|
|
263
|
-
setIsEditing(false)
|
|
264
|
-
if (initialFeedback) {
|
|
265
|
-
onFeedbackChange(initialFeedback.sentiment)
|
|
266
|
-
} else {
|
|
267
|
-
onFeedbackChange(null)
|
|
268
|
-
}
|
|
269
|
-
} else if (!isEditing) {
|
|
270
|
-
// Same key, not actively editing — safe to sync
|
|
271
|
-
setPersisted(initialFeedback ?? null)
|
|
272
|
-
if (initialFeedback) {
|
|
273
|
-
onFeedbackChange(initialFeedback.sentiment)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}, [initialFeedback, feedbackKey]) // eslint-disable-line react-hooks/exhaustive-deps -- reads isEditing as guard, not trigger
|
|
277
217
|
|
|
278
218
|
// Reset state when feedback collapses
|
|
279
219
|
const resetState = React.useCallback(() => {
|
|
@@ -283,7 +223,6 @@ export function FeedbackFooter({
|
|
|
283
223
|
setAdditionalPills([])
|
|
284
224
|
setDetailText("")
|
|
285
225
|
setActiveTreeIndex(null)
|
|
286
|
-
setIsEditing(false)
|
|
287
226
|
}, [])
|
|
288
227
|
|
|
289
228
|
const handleSentimentClick = React.useCallback(
|
|
@@ -292,26 +231,10 @@ export function FeedbackFooter({
|
|
|
292
231
|
// Reset chip state when switching sentiment, then expand
|
|
293
232
|
resetState()
|
|
294
233
|
setExpanded(true)
|
|
295
|
-
setSubmitted(false)
|
|
296
|
-
setPersisted(null)
|
|
297
|
-
setIsEditing(true)
|
|
298
234
|
},
|
|
299
235
|
[onFeedbackChange, resetState],
|
|
300
236
|
)
|
|
301
237
|
|
|
302
|
-
/** Open the persisted indicator for editing. */
|
|
303
|
-
const handlePersistedClick = React.useCallback(() => {
|
|
304
|
-
if (!persisted) return
|
|
305
|
-
onFeedbackChange(persisted.sentiment)
|
|
306
|
-
setSelectedTier1(persisted.reasonTop ?? null)
|
|
307
|
-
setSelectedTier2(persisted.reasonSub ?? null)
|
|
308
|
-
setAdditionalPills(persisted.pills ?? [])
|
|
309
|
-
setDetailText(persisted.detail ?? "")
|
|
310
|
-
setExpanded(true)
|
|
311
|
-
setSubmitted(false)
|
|
312
|
-
setIsEditing(true)
|
|
313
|
-
}, [persisted, onFeedbackChange])
|
|
314
|
-
|
|
315
238
|
const handleTier1Toggle = React.useCallback(
|
|
316
239
|
(chipLabel: string) => {
|
|
317
240
|
if (selectedTier1 === chipLabel) {
|
|
@@ -372,16 +295,7 @@ export function FeedbackFooter({
|
|
|
372
295
|
pills: additionalPills,
|
|
373
296
|
detail: detailText,
|
|
374
297
|
})
|
|
375
|
-
|
|
376
|
-
setSubmitted(true)
|
|
377
|
-
// Collapse expansion but keep sentiment visible
|
|
378
|
-
setExpanded(false)
|
|
379
|
-
setSelectedTier1(null)
|
|
380
|
-
setSelectedTier2(null)
|
|
381
|
-
setAdditionalPills([])
|
|
382
|
-
setDetailText("")
|
|
383
|
-
setActiveTreeIndex(null)
|
|
384
|
-
setIsEditing(false)
|
|
298
|
+
resetState()
|
|
385
299
|
}, [
|
|
386
300
|
feedback,
|
|
387
301
|
selectedTier1,
|
|
@@ -389,6 +303,7 @@ export function FeedbackFooter({
|
|
|
389
303
|
additionalPills,
|
|
390
304
|
detailText,
|
|
391
305
|
onSubmit,
|
|
306
|
+
resetState,
|
|
392
307
|
])
|
|
393
308
|
|
|
394
309
|
const handleCancel = React.useCallback(() => {
|
|
@@ -408,75 +323,38 @@ export function FeedbackFooter({
|
|
|
408
323
|
const activeTree =
|
|
409
324
|
activeTreeIndex !== null ? negativeChips[activeTreeIndex] : null
|
|
410
325
|
|
|
411
|
-
// Determine if we should show the persisted indicator instead of bare buttons
|
|
412
|
-
const showPersistedIndicator = persisted && !expanded && !submitted
|
|
413
|
-
|
|
414
326
|
return (
|
|
415
327
|
<div className={cn("space-y-3", className)}>
|
|
416
328
|
{/* Sentiment buttons + meta text bar */}
|
|
417
329
|
<div className="flex items-center justify-between">
|
|
418
|
-
|
|
419
|
-
/* Persisted feedback indicator — clickable to reopen editor */
|
|
330
|
+
<div className="flex items-center gap-3">
|
|
420
331
|
<button
|
|
421
332
|
type="button"
|
|
422
|
-
onClick={
|
|
423
|
-
className=
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
<ThumbsUp className="h-[11px] w-[11px]" />
|
|
429
|
-
) : (
|
|
430
|
-
<ThumbsDown className="h-[11px] w-[11px]" />
|
|
431
|
-
)}
|
|
432
|
-
{persisted.detail && (
|
|
433
|
-
<span className="max-w-[200px] truncate text-muted-foreground/70">
|
|
434
|
-
{persisted.detail}
|
|
435
|
-
</span>
|
|
333
|
+
onClick={() => handleSentimentClick("positive")}
|
|
334
|
+
className={cn(
|
|
335
|
+
"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors",
|
|
336
|
+
feedback === "positive"
|
|
337
|
+
? SENTIMENT_BUTTON_ACTIVE.positive
|
|
338
|
+
: SENTIMENT_BUTTON_IDLE,
|
|
436
339
|
)}
|
|
437
|
-
|
|
340
|
+
>
|
|
341
|
+
<ThumbsUp className="h-[11px] w-[11px]" />
|
|
342
|
+
Helpful
|
|
438
343
|
</button>
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
? SENTIMENT_BUTTON_ACTIVE.positive
|
|
448
|
-
: SENTIMENT_BUTTON_IDLE,
|
|
449
|
-
)}
|
|
450
|
-
>
|
|
451
|
-
<ThumbsUp className="h-[11px] w-[11px]" />
|
|
452
|
-
Helpful
|
|
453
|
-
</button>
|
|
454
|
-
<button
|
|
455
|
-
type="button"
|
|
456
|
-
onClick={() => handleSentimentClick("negative")}
|
|
457
|
-
className={cn(
|
|
458
|
-
"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors",
|
|
459
|
-
feedback === "negative"
|
|
460
|
-
? SENTIMENT_BUTTON_ACTIVE.negative
|
|
461
|
-
: SENTIMENT_BUTTON_IDLE,
|
|
462
|
-
)}
|
|
463
|
-
>
|
|
464
|
-
<ThumbsDown className="h-[11px] w-[11px]" />
|
|
465
|
-
Not helpful
|
|
466
|
-
</button>
|
|
467
|
-
{/* Transient "Saved" confirmation pill */}
|
|
468
|
-
{submitted && feedback && (
|
|
469
|
-
<span
|
|
470
|
-
className="inline-flex items-center gap-1 text-[11px] font-medium text-emerald-600"
|
|
471
|
-
role="status"
|
|
472
|
-
data-testid="feedback-submitted-pill"
|
|
473
|
-
>
|
|
474
|
-
<Check className="h-[11px] w-[11px]" />
|
|
475
|
-
{submittedLabel}
|
|
476
|
-
</span>
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={() => handleSentimentClick("negative")}
|
|
347
|
+
className={cn(
|
|
348
|
+
"flex gap-1 items-center text-[11px] font-medium rounded-md px-2 py-1 transition-colors",
|
|
349
|
+
feedback === "negative"
|
|
350
|
+
? SENTIMENT_BUTTON_ACTIVE.negative
|
|
351
|
+
: SENTIMENT_BUTTON_IDLE,
|
|
477
352
|
)}
|
|
478
|
-
|
|
479
|
-
|
|
353
|
+
>
|
|
354
|
+
<ThumbsDown className="h-[11px] w-[11px]" />
|
|
355
|
+
Not helpful
|
|
356
|
+
</button>
|
|
357
|
+
</div>
|
|
480
358
|
{metaText && (
|
|
481
359
|
<span className="text-[11px] text-muted-foreground">{metaText}</span>
|
|
482
360
|
)}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "lucide-react"
|
|
16
16
|
import type { LucideIcon } from "lucide-react"
|
|
17
17
|
import { FeedbackFooter } from "./feedback-primitives"
|
|
18
|
-
import type { FeedbackChipTree, FeedbackSubmitData
|
|
18
|
+
import type { FeedbackChipTree, FeedbackSubmitData } from "./feedback-primitives"
|
|
19
19
|
import { cn } from "../lib/utils"
|
|
20
20
|
import type {
|
|
21
21
|
QueueItem,
|
|
@@ -266,7 +266,6 @@ function StructuredSignalRow({ item, bucketKey, signal, tone, onOpenSignalBucket
|
|
|
266
266
|
const IconComponent = resolveIcon(signal.signalTypeName)
|
|
267
267
|
const toneClass = tone ? (SIGNAL_TONE_CLASSES[tone] ?? DEFAULT_TONE_CLASS) : DEFAULT_TONE_CLASS
|
|
268
268
|
const isCombined = signal.signalTypeName === "combined_signal" && signal.components && signal.components.length > 0
|
|
269
|
-
const hasBalance = Boolean(signal.currentBalance || signal.balanceContext)
|
|
270
269
|
|
|
271
270
|
const rowContent = (
|
|
272
271
|
<>
|
|
@@ -305,26 +304,6 @@ function StructuredSignalRow({ item, bucketKey, signal, tone, onOpenSignalBucket
|
|
|
305
304
|
|
|
306
305
|
{/* Slot 5: Chevron */}
|
|
307
306
|
<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" />
|
|
308
|
-
|
|
309
|
-
{/* Balance context strip — spans full row below grid columns */}
|
|
310
|
-
{hasBalance && (
|
|
311
|
-
<div
|
|
312
|
-
className="col-span-full mt-0.5 text-[10px] text-muted-foreground/70"
|
|
313
|
-
data-testid="balance-context-strip"
|
|
314
|
-
>
|
|
315
|
-
{signal.currentBalance && (
|
|
316
|
-
<span>
|
|
317
|
-
Current balance <span className="font-medium text-muted-foreground">{signal.currentBalance}</span>
|
|
318
|
-
</span>
|
|
319
|
-
)}
|
|
320
|
-
{signal.balanceContext && (
|
|
321
|
-
<span>
|
|
322
|
-
{signal.currentBalance ? " · " : ""}
|
|
323
|
-
{signal.balanceContext}
|
|
324
|
-
</span>
|
|
325
|
-
)}
|
|
326
|
-
</div>
|
|
327
|
-
)}
|
|
328
307
|
</>
|
|
329
308
|
)
|
|
330
309
|
|
|
@@ -426,11 +405,9 @@ interface WhyCardProps {
|
|
|
426
405
|
panelId: string
|
|
427
406
|
onOpenSignalBucket?: ScoreWhyChipsProps["onOpenSignalBucket"]
|
|
428
407
|
onBucketFeedback?: (bucketKey: string, data: FeedbackSubmitData) => void
|
|
429
|
-
/** Persisted bucket-level feedback to hydrate from. */
|
|
430
|
-
initialBucketFeedback?: PersistedFeedbackData | null
|
|
431
408
|
}
|
|
432
409
|
|
|
433
|
-
function WhyCard({ bucket, signals, item, panelId, onOpenSignalBucket, onBucketFeedback
|
|
410
|
+
function WhyCard({ bucket, signals, item, panelId, onOpenSignalBucket, onBucketFeedback }: WhyCardProps) {
|
|
434
411
|
const [showAll, setShowAll] = React.useState(false)
|
|
435
412
|
const [bucketFeedback, setBucketFeedback] = React.useState<"positive" | "negative" | null>(null)
|
|
436
413
|
const totalCount = bucket.signalCount ?? signals.length
|
|
@@ -511,8 +488,6 @@ function WhyCard({ bucket, signals, item, panelId, onOpenSignalBucket, onBucketF
|
|
|
511
488
|
negativeChips={BUCKET_NEGATIVE_CHIPS}
|
|
512
489
|
negativePrompt="Was this bucket useful?"
|
|
513
490
|
positivePrompt="Thanks! What was useful about this bucket?"
|
|
514
|
-
initialFeedback={initialBucketFeedback}
|
|
515
|
-
feedbackKey={bucket.key}
|
|
516
491
|
/>
|
|
517
492
|
</div>
|
|
518
493
|
)}
|
|
@@ -586,7 +561,6 @@ export function ScoreWhyChips({
|
|
|
586
561
|
panelId={selectedPanelId}
|
|
587
562
|
onOpenSignalBucket={onOpenSignalBucket}
|
|
588
563
|
onBucketFeedback={signalData.onBucketFeedback}
|
|
589
|
-
initialBucketFeedback={signalData.initialBucketFeedback?.[selectedBucket.key]}
|
|
590
564
|
/>
|
|
591
565
|
)}
|
|
592
566
|
</div>
|