@handled-ai/design-system 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/README.md +14 -4
  2. package/dist/charts/bar-chart-component.d.ts +24 -0
  3. package/dist/charts/bar-chart-component.js +123 -0
  4. package/dist/charts/bar-chart-component.js.map +1 -0
  5. package/dist/charts/chart-tooltip.d.ts +26 -0
  6. package/dist/charts/chart-tooltip.js +69 -0
  7. package/dist/charts/chart-tooltip.js.map +1 -0
  8. package/dist/charts/chart.d.ts +64 -0
  9. package/dist/charts/chart.js +285 -0
  10. package/dist/charts/chart.js.map +1 -0
  11. package/dist/charts/donut-chart.d.ts +21 -0
  12. package/dist/charts/donut-chart.js +96 -0
  13. package/dist/charts/donut-chart.js.map +1 -0
  14. package/dist/charts/index.d.ts +11 -0
  15. package/dist/charts/index.js +10 -0
  16. package/dist/charts/index.js.map +1 -0
  17. package/dist/charts/pipeline-overview.d.ts +76 -0
  18. package/dist/charts/pipeline-overview.js +372 -0
  19. package/dist/charts/pipeline-overview.js.map +1 -0
  20. package/dist/charts/sankey-chart.d.ts +52 -0
  21. package/dist/charts/sankey-chart.js +219 -0
  22. package/dist/charts/sankey-chart.js.map +1 -0
  23. package/dist/charts/top-line-metrics.d.ts +26 -0
  24. package/dist/charts/top-line-metrics.js +224 -0
  25. package/dist/charts/top-line-metrics.js.map +1 -0
  26. package/dist/charts/trend-area-chart.d.ts +21 -0
  27. package/dist/charts/trend-area-chart.js +150 -0
  28. package/dist/charts/trend-area-chart.js.map +1 -0
  29. package/dist/charts/volume-analysis-chart.d.ts +19 -0
  30. package/dist/charts/volume-analysis-chart.js +121 -0
  31. package/dist/charts/volume-analysis-chart.js.map +1 -0
  32. package/dist/components/activity-detail.d.ts +38 -0
  33. package/dist/components/activity-detail.js +163 -0
  34. package/dist/components/activity-detail.js.map +1 -0
  35. package/dist/components/activity-log.d.ts +21 -0
  36. package/dist/components/activity-log.js +61 -0
  37. package/dist/components/activity-log.js.map +1 -0
  38. package/dist/components/agent-popover.d.ts +71 -0
  39. package/dist/components/agent-popover.js +282 -0
  40. package/dist/components/agent-popover.js.map +1 -0
  41. package/dist/components/agent-widget.d.ts +24 -0
  42. package/dist/components/agent-widget.js +117 -0
  43. package/dist/components/agent-widget.js.map +1 -0
  44. package/dist/components/avatar.d.ts +13 -0
  45. package/dist/components/avatar.js +140 -0
  46. package/dist/components/avatar.js.map +1 -0
  47. package/dist/components/badge.d.ts +12 -0
  48. package/dist/components/badge.js +75 -0
  49. package/dist/components/badge.js.map +1 -0
  50. package/dist/components/button.d.ts +13 -0
  51. package/dist/components/button.js +83 -0
  52. package/dist/components/button.js.map +1 -0
  53. package/dist/components/card.d.ts +11 -0
  54. package/dist/components/card.js +119 -0
  55. package/dist/components/card.js.map +1 -0
  56. package/dist/components/contact-list.d.ts +34 -0
  57. package/dist/components/contact-list.js +84 -0
  58. package/dist/components/contact-list.js.map +1 -0
  59. package/dist/components/dashboard-cards.d.ts +10 -0
  60. package/dist/components/dashboard-cards.js +164 -0
  61. package/dist/components/dashboard-cards.js.map +1 -0
  62. package/dist/components/data-table-display.d.ts +19 -0
  63. package/dist/components/data-table-display.js +109 -0
  64. package/dist/components/data-table-display.js.map +1 -0
  65. package/dist/components/data-table-filter.d.ts +18 -0
  66. package/dist/components/data-table-filter.js +107 -0
  67. package/dist/components/data-table-filter.js.map +1 -0
  68. package/dist/components/data-table-quick-views.d.ts +13 -0
  69. package/dist/components/data-table-quick-views.js +90 -0
  70. package/dist/components/data-table-quick-views.js.map +1 -0
  71. package/dist/components/data-table-toolbar.d.ts +18 -0
  72. package/dist/components/data-table-toolbar.js +45 -0
  73. package/dist/components/data-table-toolbar.js.map +1 -0
  74. package/dist/components/data-table.d.ts +39 -0
  75. package/dist/components/data-table.js +821 -0
  76. package/dist/components/data-table.js.map +1 -0
  77. package/dist/components/detail-view.d.ts +44 -0
  78. package/dist/components/detail-view.js +165 -0
  79. package/dist/components/detail-view.js.map +1 -0
  80. package/dist/components/dialog.d.ts +19 -0
  81. package/dist/components/dialog.js +188 -0
  82. package/dist/components/dialog.js.map +1 -0
  83. package/dist/components/dropdown-menu.d.ts +27 -0
  84. package/dist/components/dropdown-menu.js +279 -0
  85. package/dist/components/dropdown-menu.js.map +1 -0
  86. package/dist/components/entity-panel.d.ts +69 -0
  87. package/dist/components/entity-panel.js +584 -0
  88. package/dist/components/entity-panel.js.map +1 -0
  89. package/dist/components/inbox-row.d.ts +27 -0
  90. package/dist/components/inbox-row.js +139 -0
  91. package/dist/components/inbox-row.js.map +1 -0
  92. package/dist/components/inbox-toolbar.d.ts +21 -0
  93. package/dist/components/inbox-toolbar.js +203 -0
  94. package/dist/components/inbox-toolbar.js.map +1 -0
  95. package/dist/components/input.d.ts +5 -0
  96. package/dist/components/input.js +50 -0
  97. package/dist/components/input.js.map +1 -0
  98. package/dist/components/insights-filter-bar.d.ts +21 -0
  99. package/dist/components/insights-filter-bar.js +99 -0
  100. package/dist/components/insights-filter-bar.js.map +1 -0
  101. package/dist/components/item-list-display.d.ts +22 -0
  102. package/dist/components/item-list-display.js +240 -0
  103. package/dist/components/item-list-display.js.map +1 -0
  104. package/dist/components/item-list-filter.d.ts +16 -0
  105. package/dist/components/item-list-filter.js +87 -0
  106. package/dist/components/item-list-filter.js.map +1 -0
  107. package/dist/components/item-list-toolbar.d.ts +25 -0
  108. package/dist/components/item-list-toolbar.js +79 -0
  109. package/dist/components/item-list-toolbar.js.map +1 -0
  110. package/dist/components/item-list.d.ts +20 -0
  111. package/dist/components/item-list.js +702 -0
  112. package/dist/components/item-list.js.map +1 -0
  113. package/dist/components/label.d.ts +6 -0
  114. package/dist/components/label.js +55 -0
  115. package/dist/components/label.js.map +1 -0
  116. package/dist/components/message.d.ts +23 -0
  117. package/dist/components/message.js +117 -0
  118. package/dist/components/message.js.map +1 -0
  119. package/dist/components/metric-card.d.ts +25 -0
  120. package/dist/components/metric-card.js +107 -0
  121. package/dist/components/metric-card.js.map +1 -0
  122. package/dist/components/performance-metrics-table.d.ts +38 -0
  123. package/dist/components/performance-metrics-table.js +342 -0
  124. package/dist/components/performance-metrics-table.js.map +1 -0
  125. package/dist/components/preview-list.d.ts +14 -0
  126. package/dist/components/preview-list.js +83 -0
  127. package/dist/components/preview-list.js.map +1 -0
  128. package/dist/components/progress.d.ts +6 -0
  129. package/dist/components/progress.js +69 -0
  130. package/dist/components/progress.js.map +1 -0
  131. package/dist/components/quick-action-chat-area.d.ts +24 -0
  132. package/dist/components/quick-action-chat-area.js +178 -0
  133. package/dist/components/quick-action-chat-area.js.map +1 -0
  134. package/dist/components/quick-action-modal.d.ts +30 -0
  135. package/dist/components/quick-action-modal.js +288 -0
  136. package/dist/components/quick-action-modal.js.map +1 -0
  137. package/dist/components/quick-action-sidebar-nav.d.ts +51 -0
  138. package/dist/components/quick-action-sidebar-nav.js +528 -0
  139. package/dist/components/quick-action-sidebar-nav.js.map +1 -0
  140. package/dist/components/recommended-actions-section.d.ts +23 -0
  141. package/dist/components/recommended-actions-section.js +215 -0
  142. package/dist/components/recommended-actions-section.js.map +1 -0
  143. package/dist/components/report-card.d.ts +26 -0
  144. package/dist/components/report-card.js +69 -0
  145. package/dist/components/report-card.js.map +1 -0
  146. package/dist/components/score-analysis-modal.d.ts +26 -0
  147. package/dist/components/score-analysis-modal.js +141 -0
  148. package/dist/components/score-analysis-modal.js.map +1 -0
  149. package/dist/components/score-breakdown.d.ts +17 -0
  150. package/dist/components/score-breakdown.js +162 -0
  151. package/dist/components/score-breakdown.js.map +1 -0
  152. package/dist/components/score-feedback.d.ts +40 -0
  153. package/dist/components/score-feedback.js +209 -0
  154. package/dist/components/score-feedback.js.map +1 -0
  155. package/dist/components/score-ring.d.ts +14 -0
  156. package/dist/components/score-ring.js +79 -0
  157. package/dist/components/score-ring.js.map +1 -0
  158. package/dist/components/scroll-area.d.ts +7 -0
  159. package/dist/components/scroll-area.js +101 -0
  160. package/dist/components/scroll-area.js.map +1 -0
  161. package/dist/components/select.d.ts +17 -0
  162. package/dist/components/select.js +228 -0
  163. package/dist/components/select.js.map +1 -0
  164. package/dist/components/separator.d.ts +6 -0
  165. package/dist/components/separator.js +61 -0
  166. package/dist/components/separator.js.map +1 -0
  167. package/dist/components/sheet.d.ts +16 -0
  168. package/dist/components/sheet.js +168 -0
  169. package/dist/components/sheet.js.map +1 -0
  170. package/dist/components/sidebar.d.ts +73 -0
  171. package/dist/components/sidebar.js +723 -0
  172. package/dist/components/sidebar.js.map +1 -0
  173. package/dist/components/signal-feedback-inline.d.ts +51 -0
  174. package/dist/components/signal-feedback-inline.js +548 -0
  175. package/dist/components/signal-feedback-inline.js.map +1 -0
  176. package/dist/components/simple-data-table.d.ts +15 -0
  177. package/dist/components/simple-data-table.js +91 -0
  178. package/dist/components/simple-data-table.js.map +1 -0
  179. package/dist/components/skeleton.d.ts +5 -0
  180. package/dist/components/skeleton.js +44 -0
  181. package/dist/components/skeleton.js.map +1 -0
  182. package/dist/components/status-badge.d.ts +10 -0
  183. package/dist/components/status-badge.js +82 -0
  184. package/dist/components/status-badge.js.map +1 -0
  185. package/dist/components/styled-bar-list.d.ts +20 -0
  186. package/dist/components/styled-bar-list.js +59 -0
  187. package/dist/components/styled-bar-list.js.map +1 -0
  188. package/dist/components/suggested-actions.d.ts +110 -0
  189. package/dist/components/suggested-actions.js +1538 -0
  190. package/dist/components/suggested-actions.js.map +1 -0
  191. package/dist/components/table.d.ts +12 -0
  192. package/dist/components/table.js +147 -0
  193. package/dist/components/table.js.map +1 -0
  194. package/dist/components/tabs.d.ts +14 -0
  195. package/dist/components/tabs.js +129 -0
  196. package/dist/components/tabs.js.map +1 -0
  197. package/dist/components/textarea.d.ts +5 -0
  198. package/dist/components/textarea.js +47 -0
  199. package/dist/components/textarea.js.map +1 -0
  200. package/dist/components/timeline-activity.d.ts +34 -0
  201. package/dist/components/timeline-activity.js +181 -0
  202. package/dist/components/timeline-activity.js.map +1 -0
  203. package/dist/components/tooltip.d.ts +9 -0
  204. package/dist/components/tooltip.js +93 -0
  205. package/dist/components/tooltip.js.map +1 -0
  206. package/dist/components/view-mode-toggle.d.ts +16 -0
  207. package/dist/components/view-mode-toggle.js +24 -0
  208. package/dist/components/view-mode-toggle.js.map +1 -0
  209. package/dist/hooks/use-mobile.d.ts +3 -0
  210. package/dist/hooks/use-mobile.js +21 -0
  211. package/dist/hooks/use-mobile.js.map +1 -0
  212. package/dist/index.d.ts +68 -1878
  213. package/dist/index.js +69 -10918
  214. package/dist/index.js.map +1 -1
  215. package/dist/lib/icons.d.ts +18 -0
  216. package/dist/lib/icons.js +21 -0
  217. package/dist/lib/icons.js.map +1 -0
  218. package/dist/lib/utils.d.ts +5 -0
  219. package/dist/lib/utils.js +9 -0
  220. package/dist/lib/utils.js.map +1 -0
  221. package/dist/prototype/index.d.ts +20 -0
  222. package/dist/prototype/index.js +8 -0
  223. package/dist/prototype/index.js.map +1 -0
  224. package/dist/prototype/prototype-accounts-view.d.ts +22 -0
  225. package/dist/prototype/prototype-accounts-view.js +70 -0
  226. package/dist/prototype/prototype-accounts-view.js.map +1 -0
  227. package/dist/prototype/prototype-admin-view.d.ts +21 -0
  228. package/dist/prototype/prototype-admin-view.js +53 -0
  229. package/dist/prototype/prototype-admin-view.js.map +1 -0
  230. package/dist/prototype/prototype-config.d.ts +226 -0
  231. package/dist/prototype/prototype-config.js +1 -0
  232. package/dist/prototype/prototype-config.js.map +1 -0
  233. package/dist/prototype/prototype-inbox-view.d.ts +48 -0
  234. package/dist/prototype/prototype-inbox-view.js +701 -0
  235. package/dist/prototype/prototype-inbox-view.js.map +1 -0
  236. package/dist/prototype/prototype-insights-view.d.ts +23 -0
  237. package/dist/prototype/prototype-insights-view.js +335 -0
  238. package/dist/prototype/prototype-insights-view.js.map +1 -0
  239. package/dist/prototype/prototype-shell.d.ts +40 -0
  240. package/dist/prototype/prototype-shell.js +190 -0
  241. package/dist/prototype/prototype-shell.js.map +1 -0
  242. package/dist/prototype/prototype-work-queue-view.d.ts +8 -0
  243. package/dist/prototype/prototype-work-queue-view.js +17 -0
  244. package/dist/prototype/prototype-work-queue-view.js.map +1 -0
  245. package/dist/three/agent-orb.d.ts +39 -0
  246. package/dist/three/agent-orb.js +500 -0
  247. package/dist/three/agent-orb.js.map +1 -0
  248. package/dist/three/index.d.ts +2 -0
  249. package/dist/three/index.js +2 -0
  250. package/dist/three/index.js.map +1 -0
  251. package/package.json +98 -17
  252. package/src/charts/bar-chart-component.tsx +150 -0
  253. package/src/charts/chart-tooltip.tsx +86 -0
  254. package/src/charts/chart.tsx +371 -0
  255. package/src/charts/donut-chart.tsx +112 -0
  256. package/src/charts/index.ts +13 -0
  257. package/src/charts/pipeline-overview.tsx +476 -0
  258. package/src/charts/sankey-chart.tsx +290 -0
  259. package/src/charts/top-line-metrics.tsx +261 -0
  260. package/src/charts/trend-area-chart.tsx +150 -0
  261. package/src/charts/volume-analysis-chart.tsx +124 -0
  262. package/src/components/activity-detail.tsx +233 -0
  263. package/src/components/activity-log.tsx +89 -0
  264. package/src/components/agent-popover.tsx +373 -0
  265. package/src/components/agent-widget.tsx +163 -0
  266. package/src/components/avatar.tsx +109 -0
  267. package/src/components/badge.tsx +48 -0
  268. package/src/components/button.tsx +59 -0
  269. package/src/components/card.tsx +92 -0
  270. package/src/components/contact-list.tsx +121 -0
  271. package/src/components/dashboard-cards.tsx +170 -0
  272. package/src/components/data-table-display.tsx +139 -0
  273. package/src/components/data-table-filter.tsx +138 -0
  274. package/src/components/data-table-quick-views.tsx +103 -0
  275. package/src/components/data-table-toolbar.tsx +56 -0
  276. package/src/components/data-table.tsx +915 -0
  277. package/src/components/detail-view.tsx +237 -0
  278. package/src/components/dialog.tsx +158 -0
  279. package/src/components/dropdown-menu.tsx +257 -0
  280. package/src/components/entity-panel.tsx +767 -0
  281. package/src/components/inbox-row.tsx +132 -0
  282. package/src/components/inbox-toolbar.tsx +213 -0
  283. package/src/components/input.tsx +21 -0
  284. package/src/components/insights-filter-bar.tsx +132 -0
  285. package/src/components/item-list-display.tsx +278 -0
  286. package/src/components/item-list-filter.tsx +118 -0
  287. package/src/components/item-list-toolbar.tsx +97 -0
  288. package/src/components/item-list.tsx +843 -0
  289. package/src/components/label.tsx +24 -0
  290. package/src/components/message.tsx +83 -0
  291. package/src/components/metric-card.tsx +178 -0
  292. package/src/components/performance-metrics-table.tsx +442 -0
  293. package/src/components/preview-list.tsx +62 -0
  294. package/src/components/progress.tsx +31 -0
  295. package/src/components/quick-action-chat-area.tsx +156 -0
  296. package/src/components/quick-action-modal.tsx +331 -0
  297. package/src/components/quick-action-sidebar-nav.tsx +592 -0
  298. package/src/components/recommended-actions-section.tsx +258 -0
  299. package/src/components/report-card.tsx +106 -0
  300. package/src/components/score-analysis-modal.tsx +172 -0
  301. package/src/components/score-breakdown.tsx +179 -0
  302. package/src/components/score-feedback.tsx +288 -0
  303. package/src/components/score-ring.tsx +87 -0
  304. package/src/components/scroll-area.tsx +58 -0
  305. package/src/components/select.tsx +190 -0
  306. package/src/components/separator.tsx +28 -0
  307. package/src/components/sheet.tsx +143 -0
  308. package/src/components/sidebar.tsx +726 -0
  309. package/src/components/signal-feedback-inline.tsx +591 -0
  310. package/src/components/simple-data-table.tsx +124 -0
  311. package/src/components/skeleton.tsx +15 -0
  312. package/src/components/status-badge.tsx +63 -0
  313. package/src/components/styled-bar-list.tsx +70 -0
  314. package/src/components/suggested-actions.tsx +1985 -0
  315. package/src/components/table.tsx +116 -0
  316. package/src/components/tabs.tsx +91 -0
  317. package/src/components/textarea.tsx +18 -0
  318. package/src/components/timeline-activity.tsx +234 -0
  319. package/src/components/tooltip.tsx +57 -0
  320. package/src/components/view-mode-toggle.tsx +39 -0
  321. package/src/hooks/use-mobile.ts +21 -0
  322. package/src/index.ts +77 -0
  323. package/src/lib/icons.ts +18 -0
  324. package/src/lib/utils.ts +6 -0
  325. package/src/prototype/index.ts +11 -0
  326. package/src/prototype/prototype-accounts-view.tsx +112 -0
  327. package/src/prototype/prototype-admin-view.tsx +67 -0
  328. package/src/prototype/prototype-config.ts +243 -0
  329. package/src/prototype/prototype-inbox-view.tsx +810 -0
  330. package/src/prototype/prototype-insights-view.tsx +379 -0
  331. package/src/prototype/prototype-shell.tsx +219 -0
  332. package/src/prototype/prototype-work-queue-view.tsx +30 -0
  333. package/src/styles/globals.css +299 -0
  334. package/src/three/agent-orb.tsx +557 -0
  335. package/src/three/index.ts +5 -0
  336. package/src/types/r3f.d.ts +8 -0
@@ -0,0 +1,1985 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ ChevronDown,
6
+ ChevronUp,
7
+ Clock,
8
+ MessageSquare,
9
+ Undo2,
10
+ Redo2,
11
+ Bold,
12
+ Italic,
13
+ Underline,
14
+ AlignLeft,
15
+ List,
16
+ Trash,
17
+ Sparkles,
18
+ ThumbsUp,
19
+ ThumbsDown,
20
+ Check,
21
+ RefreshCw,
22
+ ArrowLeft,
23
+ Mail,
24
+ Phone,
25
+ X,
26
+ Users,
27
+ ExternalLink,
28
+ Copy,
29
+ PenLine,
30
+ ClipboardList,
31
+ Globe,
32
+ } from "lucide-react"
33
+ import { Button } from "./button"
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Brand Icons (image-based from registry)
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function BrandIcon({ src, alt, className }: { src: string; alt: string; className?: string }) {
40
+ return (
41
+ <img
42
+ src={src}
43
+ alt={alt}
44
+ className={`${className ?? ""} object-contain`}
45
+ draggable={false}
46
+ />
47
+ )
48
+ }
49
+
50
+ export interface SuggestedActionsIconMap {
51
+ gmail?: string
52
+ slack?: string
53
+ zendesk?: string
54
+ salesforce?: string
55
+ }
56
+
57
+ function getActionTypeIcon(type: string, className?: string, iconMap?: SuggestedActionsIconMap) {
58
+ switch (type) {
59
+ case "email":
60
+ return iconMap?.gmail
61
+ ? <BrandIcon src={iconMap.gmail} alt="Gmail" className={className} />
62
+ : <Mail className={className} />
63
+ case "slack":
64
+ return iconMap?.slack
65
+ ? <BrandIcon src={iconMap.slack} alt="Slack" className={className} />
66
+ : <MessageSquare className={className} />
67
+ case "ticket":
68
+ return iconMap?.zendesk
69
+ ? <BrandIcon src={iconMap.zendesk} alt="Zendesk" className={className} />
70
+ : <MessageSquare className={className} />
71
+ case "call":
72
+ return <Phone className={className} />
73
+ case "manual":
74
+ return <ClipboardList className={className} />
75
+ case "browser":
76
+ return <Globe className={className} />
77
+ default:
78
+ return <Mail className={className} />
79
+ }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Types
84
+ // ---------------------------------------------------------------------------
85
+
86
+ export interface SuggestedContact {
87
+ name: string
88
+ role: string
89
+ email?: string
90
+ emails?: string[]
91
+ phone?: string
92
+ phones?: string[]
93
+ confirmed: boolean
94
+ salesforceUrl?: string
95
+ lastActivity?: {
96
+ date: string
97
+ type: string
98
+ }
99
+ }
100
+
101
+ export interface SuggestedActionThreadMessage {
102
+ id: string
103
+ from: string
104
+ initials: string
105
+ time: string
106
+ preview: string
107
+ content: string
108
+ }
109
+
110
+ export interface SuggestedActionReplyTo {
111
+ from: string
112
+ time: string
113
+ content: string
114
+ channel?: string
115
+ }
116
+
117
+ export interface SuggestedActionTicket {
118
+ system: string
119
+ priority: string
120
+ type: string
121
+ subject: string
122
+ description: string
123
+ assignee?: string
124
+ tags?: string[]
125
+ }
126
+
127
+ export interface SuggestedActionFollowUp {
128
+ enabled: boolean
129
+ days: number
130
+ }
131
+
132
+ export interface SuggestedActionEmailMeta {
133
+ from: string
134
+ fromEmail: string
135
+ to?: SuggestedContact
136
+ cc?: SuggestedContact[]
137
+ bcc?: string
138
+ subject?: string
139
+ }
140
+
141
+ export interface SuggestedActionCallMeta {
142
+ contact?: SuggestedContact
143
+ talkTrack: string
144
+ allowDispatchAgent?: boolean
145
+ }
146
+
147
+ export interface SuggestedActionManualMeta {
148
+ taskDescription: string
149
+ }
150
+
151
+ export interface SuggestedActionBrowserMeta {
152
+ url: string
153
+ actionDescription: string
154
+ }
155
+
156
+ export interface SuggestedAction {
157
+ id: number | string
158
+ type: "email" | "ticket" | "slack" | "call" | "manual" | "browser"
159
+ label: string
160
+ status: "pending" | "sent" | "dismissed"
161
+ content?: string
162
+ replyTo?: SuggestedActionReplyTo
163
+ threadMessages?: SuggestedActionThreadMessage[]
164
+ ticket?: SuggestedActionTicket
165
+ followUp?: SuggestedActionFollowUp
166
+ emailMeta?: SuggestedActionEmailMeta
167
+ callMeta?: SuggestedActionCallMeta
168
+ manualMeta?: SuggestedActionManualMeta
169
+ browserMeta?: SuggestedActionBrowserMeta
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // AiEditPanel
174
+ // ---------------------------------------------------------------------------
175
+
176
+ const aiEditPills = ["Shorten it", "Make sound more like me", "Make longer", "Other"]
177
+
178
+ function AiEditPanel({ onApply }: { onApply?: (pills: string[], description: string) => void }) {
179
+ const [selectedPills, setSelectedPills] = React.useState<string[]>([])
180
+ const [description, setDescription] = React.useState("")
181
+ const [applying, setApplying] = React.useState(false)
182
+
183
+ const togglePill = React.useCallback((pill: string) => {
184
+ setSelectedPills((prev) => (prev.includes(pill) ? prev.filter((p) => p !== pill) : [...prev, pill]))
185
+ }, [])
186
+
187
+ const handleApply = React.useCallback(() => {
188
+ if (selectedPills.length === 0 && description.trim().length === 0) return
189
+ setApplying(true)
190
+ onApply?.(selectedPills, description)
191
+ setTimeout(() => {
192
+ setApplying(false)
193
+ setSelectedPills([])
194
+ setDescription("")
195
+ }, 2000)
196
+ }, [selectedPills, description, onApply])
197
+
198
+ const hasInput = selectedPills.length > 0 || description.trim().length > 0
199
+
200
+ return (
201
+ <div className="mb-4 space-y-2.5 animate-in fade-in slide-in-from-top-2 duration-200">
202
+ <div className="flex flex-wrap gap-1.5">
203
+ {aiEditPills.map((pill) => (
204
+ <button
205
+ key={pill}
206
+ onClick={() => togglePill(pill)}
207
+ className={`px-2.5 py-1 rounded-full text-[11px] font-medium border transition-colors ${
208
+ selectedPills.includes(pill)
209
+ ? "bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-300 dark:border-indigo-800"
210
+ : "bg-background text-muted-foreground border-border hover:bg-muted/50 hover:text-foreground"
211
+ }`}
212
+ >
213
+ {pill}
214
+ </button>
215
+ ))}
216
+ </div>
217
+ <div className="flex gap-2">
218
+ <input
219
+ className="flex-1 px-3 py-2 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-indigo-500 transition-colors placeholder:text-muted-foreground/50"
220
+ placeholder="Describe changes..."
221
+ value={description}
222
+ onChange={(e) => setDescription(e.target.value)}
223
+ onKeyDown={(e) => {
224
+ if (e.key === "Enter" && hasInput) handleApply()
225
+ }}
226
+ />
227
+ <button
228
+ onClick={handleApply}
229
+ disabled={!hasInput || applying}
230
+ className={`px-4 h-9 rounded-md text-xs font-semibold transition-colors flex items-center gap-1.5 shrink-0 ${
231
+ hasInput && !applying
232
+ ? "bg-foreground text-background hover:bg-foreground/90"
233
+ : "bg-muted text-muted-foreground cursor-not-allowed"
234
+ }`}
235
+ >
236
+ {applying ? (
237
+ <>
238
+ <Sparkles className="w-3 h-3 animate-pulse" />
239
+ Applying...
240
+ </>
241
+ ) : (
242
+ "Apply"
243
+ )}
244
+ </button>
245
+ </div>
246
+ </div>
247
+ )
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // DraftFeedbackInline
252
+ // ---------------------------------------------------------------------------
253
+
254
+ const positivePills = ["Tone", "Personalization", "Length", "CTA", "Other"]
255
+ const negativePills = ["Too formal", "Too casual", "Too long", "Missing context", "Wrong angle", "Factual error", "Other"]
256
+
257
+ function DraftFeedbackInline({
258
+ onRegenerateRequest,
259
+ onSubmitFeedback,
260
+ onDiscardRequest,
261
+ }: {
262
+ onRegenerateRequest?: (pills: string[], detail: string) => void
263
+ onSubmitFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
264
+ onDiscardRequest?: (pills: string[], detail: string) => void
265
+ }) {
266
+ const [thumbState, setThumbState] = React.useState<"up" | "down" | null>(null)
267
+ const [selectedPills, setSelectedPills] = React.useState<string[]>([])
268
+ const [detailText, setDetailText] = React.useState("")
269
+ const [noted, setNoted] = React.useState(false)
270
+ const [regenerated, setRegenerated] = React.useState(false)
271
+
272
+ const togglePill = React.useCallback((pill: string) => {
273
+ setSelectedPills((prev) => (prev.includes(pill) ? prev.filter((p) => p !== pill) : [...prev, pill]))
274
+ }, [])
275
+
276
+ const handleSubmit = React.useCallback(() => {
277
+ if (!thumbState) return
278
+ onSubmitFeedback?.(thumbState, selectedPills, detailText)
279
+ setNoted(true)
280
+ setTimeout(() => {
281
+ setThumbState(null)
282
+ setSelectedPills([])
283
+ setDetailText("")
284
+ setNoted(false)
285
+ }, 3000)
286
+ }, [thumbState, selectedPills, detailText, onSubmitFeedback])
287
+
288
+ const handleRegenerate = React.useCallback(() => {
289
+ if (!thumbState) return
290
+ onRegenerateRequest?.(selectedPills, detailText)
291
+ setRegenerated(true)
292
+ setTimeout(() => {
293
+ setThumbState(null)
294
+ setSelectedPills([])
295
+ setDetailText("")
296
+ setRegenerated(false)
297
+ }, 3000)
298
+ }, [thumbState, selectedPills, detailText, onRegenerateRequest])
299
+
300
+ const handleDiscard = React.useCallback(() => {
301
+ if (!thumbState) return
302
+ onDiscardRequest?.(selectedPills, detailText)
303
+ }, [thumbState, selectedPills, detailText, onDiscardRequest])
304
+
305
+ if (noted) {
306
+ return (
307
+ <div className="flex items-center gap-1.5 py-1 animate-in fade-in slide-in-from-top-1 duration-200">
308
+ <Check className="w-3.5 h-3.5 text-emerald-500" />
309
+ <span className="text-xs text-muted-foreground">Feedback recorded</span>
310
+ </div>
311
+ )
312
+ }
313
+
314
+ if (regenerated) {
315
+ return (
316
+ <div className="py-2 animate-in fade-in slide-in-from-top-1 duration-200">
317
+ <div className="flex items-center gap-2 px-3 py-2 rounded-md bg-indigo-50 dark:bg-indigo-950/30 border border-indigo-200 dark:border-indigo-800">
318
+ <RefreshCw className="w-3 h-3 text-indigo-500 animate-spin" />
319
+ <span className="text-xs font-medium text-indigo-600 dark:text-indigo-400">Regenerating draft...</span>
320
+ </div>
321
+ </div>
322
+ )
323
+ }
324
+
325
+ return (
326
+ <div className="space-y-0">
327
+ <div className="flex items-center justify-between">
328
+ <span className="text-sm text-foreground font-medium">How&apos;s this draft?</span>
329
+ <div className="flex gap-1">
330
+ <button
331
+ onClick={() => {
332
+ setThumbState(thumbState === "up" ? null : "up")
333
+ setSelectedPills([])
334
+ setDetailText("")
335
+ }}
336
+ className={`p-1.5 rounded transition-colors ${
337
+ thumbState === "up"
338
+ ? "bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400"
339
+ : "hover:bg-muted text-muted-foreground hover:text-foreground"
340
+ }`}
341
+ >
342
+ <ThumbsUp className="w-4 h-4" fill={thumbState === "up" ? "currentColor" : "none"} />
343
+ </button>
344
+ <button
345
+ onClick={() => {
346
+ setThumbState(thumbState === "down" ? null : "down")
347
+ setSelectedPills([])
348
+ setDetailText("")
349
+ }}
350
+ className={`p-1.5 rounded transition-colors ${
351
+ thumbState === "down"
352
+ ? "bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400"
353
+ : "hover:bg-muted text-muted-foreground hover:text-foreground"
354
+ }`}
355
+ >
356
+ <ThumbsDown className="w-4 h-4" fill={thumbState === "down" ? "currentColor" : "none"} />
357
+ </button>
358
+ </div>
359
+ </div>
360
+
361
+ {thumbState && (
362
+ <div className="pt-3 space-y-3 animate-in fade-in slide-in-from-top-2 duration-200">
363
+ <div>
364
+ <span className="text-xs text-muted-foreground mb-2 block font-medium">
365
+ {thumbState === "up" ? "What worked well?" : "What needs improvement?"}
366
+ </span>
367
+ <div className="flex flex-wrap gap-1.5">
368
+ {(thumbState === "up" ? positivePills : negativePills).map((pill) => (
369
+ <button
370
+ key={pill}
371
+ onClick={() => togglePill(pill)}
372
+ className={`px-2.5 py-1 rounded-full text-[11px] font-medium border transition-colors ${
373
+ selectedPills.includes(pill)
374
+ ? thumbState === "up"
375
+ ? "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-800"
376
+ : "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800"
377
+ : "bg-background text-muted-foreground border-border hover:bg-muted/50 hover:text-foreground"
378
+ }`}
379
+ >
380
+ {pill}
381
+ </button>
382
+ ))}
383
+ </div>
384
+ </div>
385
+
386
+ <textarea
387
+ value={detailText}
388
+ onChange={(e) => setDetailText(e.target.value)}
389
+ placeholder={thumbState === "up" ? "Add specific praise (optional)..." : "Provide specific instructions (optional)..."}
390
+ className="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-indigo-500/50 focus:border-indigo-500/50 resize-none min-h-[60px]"
391
+ />
392
+
393
+ <div className="flex items-center gap-2 pt-1">
394
+ {thumbState === "down" ? (
395
+ <>
396
+ <button
397
+ onClick={handleRegenerate}
398
+ disabled={selectedPills.length === 0 && detailText.length === 0}
399
+ className={`flex-1 py-1.5 rounded-md text-xs font-semibold transition-colors flex items-center justify-center gap-1.5 ${
400
+ selectedPills.length > 0 || detailText.length > 0
401
+ ? "bg-foreground text-background hover:bg-foreground/90"
402
+ : "bg-muted text-muted-foreground cursor-not-allowed"
403
+ }`}
404
+ >
405
+ <RefreshCw className="w-3 h-3" />
406
+ Regenerate draft
407
+ </button>
408
+ <button
409
+ onClick={handleDiscard}
410
+ className="flex-1 py-1.5 rounded-md text-xs font-medium transition-colors border bg-background text-foreground border-border hover:bg-muted/50 flex items-center justify-center gap-1.5"
411
+ >
412
+ Discard draft
413
+ </button>
414
+ </>
415
+ ) : (
416
+ <button
417
+ onClick={handleSubmit}
418
+ className="flex-1 py-1.5 rounded-md text-xs font-semibold transition-colors bg-foreground text-background hover:bg-foreground/90 border-transparent"
419
+ >
420
+ Submit feedback
421
+ </button>
422
+ )}
423
+ </div>
424
+ </div>
425
+ )}
426
+ </div>
427
+ )
428
+ }
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // EditorToolbar
432
+ // ---------------------------------------------------------------------------
433
+
434
+ function EditorToolbar() {
435
+ return (
436
+ <div className="flex items-center gap-1 px-4 py-2 border-t border-border bg-muted/5 flex-wrap">
437
+ <div className="flex items-center mr-2 gap-1 shrink-0">
438
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground">
439
+ <Undo2 className="h-4 w-4" />
440
+ </Button>
441
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground">
442
+ <Redo2 className="h-4 w-4" />
443
+ </Button>
444
+ </div>
445
+ <div className="w-px h-4 bg-border mx-1 shrink-0" />
446
+ <Button variant="ghost" size="sm" className="h-7 text-xs font-medium text-muted-foreground gap-1 px-2 hover:text-foreground shrink-0">
447
+ Sans Serif <ChevronDown className="h-3 w-3" />
448
+ </Button>
449
+ <div className="w-px h-4 bg-border mx-1 shrink-0" />
450
+ <div className="flex items-center gap-1 shrink-0">
451
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground">
452
+ <Bold className="h-4 w-4" />
453
+ </Button>
454
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground">
455
+ <Italic className="h-4 w-4" />
456
+ </Button>
457
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground">
458
+ <Underline className="h-4 w-4" />
459
+ </Button>
460
+ </div>
461
+ <div className="w-px h-4 bg-border mx-1 shrink-0" />
462
+ <div className="flex items-center gap-1 shrink-0">
463
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground">
464
+ <AlignLeft className="h-4 w-4" />
465
+ </Button>
466
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground">
467
+ <List className="h-4 w-4" />
468
+ </Button>
469
+ </div>
470
+ <div className="flex-1" />
471
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-red-600 hover:bg-red-50 shrink-0 ml-auto">
472
+ <Trash className="h-4 w-4" />
473
+ </Button>
474
+ </div>
475
+ )
476
+ }
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // FollowUpToggle (simple checkbox-based switch)
480
+ // ---------------------------------------------------------------------------
481
+
482
+ function FollowUpToggle({
483
+ checked,
484
+ onCheckedChange,
485
+ days,
486
+ }: {
487
+ checked: boolean
488
+ onCheckedChange: (checked: boolean) => void
489
+ days: number
490
+ }) {
491
+ return (
492
+ <div className="flex items-center justify-between">
493
+ <div className="flex items-center gap-2 text-sm text-foreground">
494
+ <Clock className="w-4 h-4 text-muted-foreground" />
495
+ <span>Auto-draft follow-up in {days}d if no response</span>
496
+ </div>
497
+ <button
498
+ role="switch"
499
+ aria-checked={checked}
500
+ onClick={() => onCheckedChange(!checked)}
501
+ className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors ${
502
+ checked ? "bg-foreground" : "bg-muted"
503
+ }`}
504
+ >
505
+ <span
506
+ className={`pointer-events-none block h-4 w-4 rounded-full bg-background shadow-sm transition-transform ${
507
+ checked ? "translate-x-4" : "translate-x-0"
508
+ }`}
509
+ />
510
+ </button>
511
+ </div>
512
+ )
513
+ }
514
+
515
+ // ---------------------------------------------------------------------------
516
+ // ContactCard
517
+ // ---------------------------------------------------------------------------
518
+
519
+ function ContactCard({
520
+ contact,
521
+ onConfirm,
522
+ onRemove,
523
+ onSwap,
524
+ variant = "primary",
525
+ showPhone = false,
526
+ }: {
527
+ contact: SuggestedContact
528
+ onConfirm?: () => void
529
+ onRemove?: () => void
530
+ onSwap?: () => void
531
+ variant?: "primary" | "secondary" | "alternative"
532
+ showPhone?: boolean
533
+ }) {
534
+ const [selectedEmail, setSelectedEmail] = React.useState(
535
+ contact.email ?? contact.emails?.[0] ?? ""
536
+ )
537
+ const [selectedPhone, setSelectedPhone] = React.useState(
538
+ contact.phone ?? contact.phones?.[0] ?? ""
539
+ )
540
+ const [showEmailPicker, setShowEmailPicker] = React.useState(false)
541
+ const [showPhonePicker, setShowPhonePicker] = React.useState(false)
542
+ const hasMultipleEmails = (contact.emails?.length ?? 0) > 1
543
+ const hasMultiplePhones = (contact.phones?.length ?? 0) > 1
544
+ const initials = contact.name.split(" ").map((n) => n[0]).join("")
545
+
546
+ if (variant === "alternative") {
547
+ const detail = showPhone
548
+ ? contact.phone ?? contact.phones?.[0] ?? ""
549
+ : contact.email ?? contact.emails?.[0] ?? ""
550
+ return (
551
+ <button
552
+ onClick={onSwap}
553
+ className="flex items-center gap-3 w-full px-3 py-2 text-left hover:bg-muted/50 rounded-md transition-colors group"
554
+ >
555
+ <div className="w-7 h-7 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium text-muted-foreground shrink-0">
556
+ {initials}
557
+ </div>
558
+ <div className="flex-1 min-w-0">
559
+ <div className="text-sm text-foreground">{contact.name}</div>
560
+ <div className="text-xs text-muted-foreground truncate">
561
+ {contact.role}{detail ? ` · ${detail}` : ""}
562
+ </div>
563
+ </div>
564
+ <span className="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
565
+ Select
566
+ </span>
567
+ </button>
568
+ )
569
+ }
570
+
571
+ if (variant === "secondary") {
572
+ return (
573
+ <div className="flex items-center gap-2 px-2.5 py-1.5 bg-muted/40 rounded-md text-sm group">
574
+ <span className="text-foreground text-xs">{contact.name}</span>
575
+ <span className="text-muted-foreground text-xs truncate">
576
+ {contact.email ?? selectedEmail}
577
+ </span>
578
+ {onRemove && (
579
+ <button
580
+ onClick={onRemove}
581
+ className="ml-auto text-muted-foreground hover:text-foreground transition-colors opacity-0 group-hover:opacity-100"
582
+ >
583
+ <X className="w-3 h-3" />
584
+ </button>
585
+ )}
586
+ </div>
587
+ )
588
+ }
589
+
590
+ const needsConfirmation = !contact.confirmed && !!onConfirm
591
+
592
+ return (
593
+ <div
594
+ className={`flex flex-wrap items-start gap-3 rounded-md ${
595
+ needsConfirmation ? "border border-emerald-200/70 bg-emerald-50/40 p-2.5" : ""
596
+ }`}
597
+ >
598
+ <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center text-xs font-medium text-muted-foreground shrink-0 mt-0.5">
599
+ {initials}
600
+ </div>
601
+ <div className="flex-1 min-w-0">
602
+ <div className="flex items-baseline gap-2">
603
+ <span className="text-sm font-medium text-foreground">{contact.name}</span>
604
+ <span className="text-xs text-muted-foreground">{contact.role}</span>
605
+ {contact.confirmed && (
606
+ <span className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-emerald-100 text-emerald-700">
607
+ <Check className="h-3 w-3" />
608
+ </span>
609
+ )}
610
+ </div>
611
+ <div className="flex items-center gap-1.5 mt-0.5">
612
+ {showPhone ? (
613
+ <>
614
+ <span className="text-xs text-muted-foreground">{selectedPhone}</span>
615
+ {hasMultiplePhones && (
616
+ <div className="relative">
617
+ <button
618
+ onClick={() => setShowPhonePicker(!showPhonePicker)}
619
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors"
620
+ >
621
+ <ChevronDown className="w-3 h-3" />
622
+ </button>
623
+ {showPhonePicker && (
624
+ <div className="absolute top-full left-0 mt-1 bg-background border border-border rounded-md shadow-lg z-10 py-1 min-w-[180px]">
625
+ {contact.phones!.map((p) => (
626
+ <button
627
+ key={p}
628
+ onClick={() => { setSelectedPhone(p); setShowPhonePicker(false) }}
629
+ className={`w-full text-left px-3 py-1.5 text-xs hover:bg-muted/50 transition-colors ${
630
+ p === selectedPhone ? "text-foreground font-medium" : "text-muted-foreground"
631
+ }`}
632
+ >
633
+ {p}
634
+ </button>
635
+ ))}
636
+ </div>
637
+ )}
638
+ </div>
639
+ )}
640
+ </>
641
+ ) : (
642
+ <>
643
+ <span className="text-xs text-muted-foreground">{selectedEmail}</span>
644
+ {hasMultipleEmails && (
645
+ <div className="relative">
646
+ <button
647
+ onClick={() => setShowEmailPicker(!showEmailPicker)}
648
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors"
649
+ >
650
+ <ChevronDown className="w-3 h-3" />
651
+ </button>
652
+ {showEmailPicker && (
653
+ <div className="absolute top-full left-0 mt-1 bg-background border border-border rounded-md shadow-lg z-10 py-1 min-w-[220px]">
654
+ {contact.emails!.map((e) => (
655
+ <button
656
+ key={e}
657
+ onClick={() => { setSelectedEmail(e); setShowEmailPicker(false) }}
658
+ className={`w-full text-left px-3 py-1.5 text-xs hover:bg-muted/50 transition-colors ${
659
+ e === selectedEmail ? "text-foreground font-medium" : "text-muted-foreground"
660
+ }`}
661
+ >
662
+ {e}
663
+ </button>
664
+ ))}
665
+ </div>
666
+ )}
667
+ </div>
668
+ )}
669
+ </>
670
+ )}
671
+ </div>
672
+ </div>
673
+ {(onRemove || (!contact.confirmed && onConfirm)) && (
674
+ <div className="ml-auto flex items-center gap-1.5 shrink-0 self-center">
675
+ {!contact.confirmed && onConfirm && (
676
+ <button
677
+ onClick={onConfirm}
678
+ className="h-7 rounded-md border border-emerald-700 bg-emerald-700 px-2.5 text-xs font-semibold text-white hover:bg-emerald-600 hover:border-emerald-600 transition-colors"
679
+ >
680
+ Confirm recipient
681
+ </button>
682
+ )}
683
+ {onRemove && (
684
+ <button
685
+ onClick={onRemove}
686
+ className="h-7 w-7 inline-flex items-center justify-center rounded-md border border-border bg-background text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors"
687
+ aria-label="Remove contact"
688
+ >
689
+ <X className="w-3.5 h-3.5" />
690
+ </button>
691
+ )}
692
+ </div>
693
+ )}
694
+ </div>
695
+ )
696
+ }
697
+
698
+ // ---------------------------------------------------------------------------
699
+ // AccountContactsPopover
700
+ // ---------------------------------------------------------------------------
701
+
702
+ function AccountContactsPopover({
703
+ contacts,
704
+ onSelect,
705
+ onSelectTo,
706
+ onSelectCc,
707
+ onSelectBcc,
708
+ onViewAll,
709
+ onOpenRecentActivity,
710
+ trigger,
711
+ iconMap,
712
+ }: {
713
+ contacts: SuggestedContact[]
714
+ onSelect: (contact: SuggestedContact) => void
715
+ onSelectTo?: (contact: SuggestedContact) => void
716
+ onSelectCc?: (contact: SuggestedContact) => void
717
+ onSelectBcc?: (contact: SuggestedContact) => void
718
+ onViewAll?: () => void
719
+ onOpenRecentActivity?: () => void
720
+ trigger: React.ReactNode
721
+ iconMap?: SuggestedActionsIconMap
722
+ }) {
723
+ const [open, setOpen] = React.useState(false)
724
+ const triggerRef = React.useRef<HTMLDivElement>(null)
725
+ const [popoverStyle, setPopoverStyle] = React.useState<React.CSSProperties>({})
726
+
727
+ React.useEffect(() => {
728
+ if (open && triggerRef.current) {
729
+ const rect = triggerRef.current.getBoundingClientRect()
730
+ const popoverWidth = Math.min(448, window.innerWidth - 32)
731
+ let left = rect.right - popoverWidth
732
+ if (left < 16) left = 16
733
+ if (left + popoverWidth > window.innerWidth - 16) left = window.innerWidth - 16 - popoverWidth
734
+ setPopoverStyle({ position: "fixed", top: rect.bottom + 4, left })
735
+ }
736
+ }, [open])
737
+
738
+ return (
739
+ <div>
740
+ <div ref={triggerRef} onClick={() => setOpen(!open)}>{trigger}</div>
741
+ {open && (
742
+ <>
743
+ <div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
744
+ <div style={popoverStyle} className="fixed bg-background border border-border rounded-lg shadow-xl z-50 w-[28rem] max-w-[calc(100vw-2rem)] py-2 animate-in fade-in slide-in-from-top-1 duration-150">
745
+ <div className="px-3 py-1.5 text-[11px] font-medium text-muted-foreground/60 uppercase tracking-wide">
746
+ Account Contacts
747
+ </div>
748
+ <div className="max-h-48 overflow-y-auto">
749
+ {contacts.map((c, i) => (
750
+ <div
751
+ key={i}
752
+ role="button"
753
+ onClick={() => { (onSelectTo ?? onSelect)(c); setOpen(false) }}
754
+ className="flex items-center gap-3 w-full px-3 py-2 text-left hover:bg-muted/50 transition-colors cursor-pointer"
755
+ >
756
+ <div className="w-7 h-7 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium text-muted-foreground shrink-0">
757
+ {c.name.split(" ").map((n) => n[0]).join("")}
758
+ </div>
759
+ <div className="flex-1 min-w-0 overflow-hidden">
760
+ <div className="truncate text-sm font-medium text-foreground">{c.name}</div>
761
+ <div className="truncate text-xs text-muted-foreground leading-tight">
762
+ {c.role} · {c.email ?? c.emails?.[0] ?? c.phone ?? c.phones?.[0] ?? ""}
763
+ </div>
764
+ {c.lastActivity && (
765
+ <button
766
+ type="button"
767
+ onClick={(e) => {
768
+ e.stopPropagation()
769
+ onOpenRecentActivity?.()
770
+ setOpen(false)
771
+ }}
772
+ className="mt-1.5 flex max-w-full items-center gap-1.5 overflow-hidden rounded-md border border-border/70 bg-muted/30 px-2 py-1 text-[11px] text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
773
+ >
774
+ <Clock className="w-3 h-3 shrink-0" />
775
+ <span className="shrink-0 font-medium">Last activity</span>
776
+ <span className="shrink-0 text-muted-foreground/60">·</span>
777
+ <span className="shrink-0">{c.lastActivity.date}</span>
778
+ <span className="shrink-0 text-muted-foreground/60">·</span>
779
+ <span className="truncate capitalize">{c.lastActivity.type}</span>
780
+ </button>
781
+ )}
782
+ </div>
783
+ <div className="ml-2 flex items-center gap-1.5 shrink-0">
784
+ {onSelectTo && (
785
+ <button
786
+ type="button"
787
+ onClick={(e) => {
788
+ e.stopPropagation()
789
+ onSelectTo(c)
790
+ setOpen(false)
791
+ }}
792
+ className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
793
+ >
794
+ To
795
+ </button>
796
+ )}
797
+ {onSelectCc && (
798
+ <button
799
+ type="button"
800
+ onClick={(e) => {
801
+ e.stopPropagation()
802
+ onSelectCc(c)
803
+ setOpen(false)
804
+ }}
805
+ className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
806
+ >
807
+ Cc
808
+ </button>
809
+ )}
810
+ {onSelectBcc && (
811
+ <button
812
+ type="button"
813
+ onClick={(e) => {
814
+ e.stopPropagation()
815
+ onSelectBcc(c)
816
+ setOpen(false)
817
+ }}
818
+ className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
819
+ >
820
+ Bcc
821
+ </button>
822
+ )}
823
+ <button
824
+ type="button"
825
+ onClick={(e) => {
826
+ e.stopPropagation()
827
+ if (c.salesforceUrl) {
828
+ window.open(c.salesforceUrl, "_blank", "noopener,noreferrer")
829
+ } else {
830
+ onViewAll?.()
831
+ }
832
+ }}
833
+ className="h-7 w-7 inline-flex items-center justify-center rounded-md border border-border bg-background hover:bg-muted/40 transition-colors shrink-0"
834
+ aria-label={`Open ${c.name} in Salesforce`}
835
+ >
836
+ {iconMap?.salesforce ? (
837
+ <BrandIcon src={iconMap.salesforce} alt="Salesforce" className="w-3.5 h-3.5" />
838
+ ) : (
839
+ <ExternalLink className="w-3.5 h-3.5 text-muted-foreground" />
840
+ )}
841
+ </button>
842
+ </div>
843
+ </div>
844
+ ))}
845
+ </div>
846
+ {onViewAll && (
847
+ <>
848
+ <div className="h-px bg-border mx-3 my-1" />
849
+ <button
850
+ onClick={() => { onViewAll(); setOpen(false) }}
851
+ className="flex items-center gap-2 w-full px-3 py-2 text-left text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
852
+ >
853
+ <ExternalLink className="w-3 h-3" />
854
+ View all contacts
855
+ </button>
856
+ </>
857
+ )}
858
+ </div>
859
+ </>
860
+ )}
861
+ </div>
862
+ )
863
+ }
864
+
865
+ // ---------------------------------------------------------------------------
866
+ // EmailHeader (Notion Mail style)
867
+ // ---------------------------------------------------------------------------
868
+
869
+ function EmailHeader({
870
+ fromName,
871
+ fromEmail,
872
+ toContacts,
873
+ ccContacts,
874
+ initialSubject = "",
875
+ accountContacts,
876
+ bccContacts,
877
+ onAddToContact,
878
+ onRemoveToContact,
879
+ onConfirmToContact,
880
+ onUpdateToContact,
881
+ onCcAdd,
882
+ onCcRemove,
883
+ onBccAdd,
884
+ onBccRemove,
885
+ onOpenAccountDetails,
886
+ onOpenRecentActivity,
887
+ iconMap,
888
+ showSubject = false,
889
+ accountDetailsLabel = "Account details",
890
+ }: {
891
+ fromName: string
892
+ fromEmail: string
893
+ toContacts: SuggestedContact[]
894
+ ccContacts?: SuggestedContact[]
895
+ initialSubject?: string
896
+ accountContacts?: SuggestedContact[]
897
+ bccContacts?: SuggestedContact[]
898
+ onAddToContact?: (contact: SuggestedContact) => void
899
+ onRemoveToContact?: (index: number) => void
900
+ onConfirmToContact?: (index: number) => void
901
+ onUpdateToContact?: (index: number, contact: SuggestedContact) => void
902
+ onCcAdd?: (contact: SuggestedContact) => void
903
+ onCcRemove?: (index: number) => void
904
+ onBccAdd?: (contact: SuggestedContact) => void
905
+ onBccRemove?: (index: number) => void
906
+ onOpenAccountDetails?: () => void
907
+ onOpenRecentActivity?: () => void
908
+ iconMap?: SuggestedActionsIconMap
909
+ showSubject?: boolean
910
+ accountDetailsLabel?: string
911
+ }) {
912
+ const hasUnconfirmedTo = toContacts.some((c) => !c.confirmed)
913
+ const [expandedToIndex, setExpandedToIndex] = React.useState<number | null>(null)
914
+ const [showCcBcc, setShowCcBcc] = React.useState(
915
+ !!((ccContacts?.length ?? 0) > 0 || (bccContacts?.length ?? 0) > 0)
916
+ )
917
+ const [manualTo, setManualTo] = React.useState("")
918
+ const [manualCc, setManualCc] = React.useState("")
919
+ const [manualBcc, setManualBcc] = React.useState("")
920
+ const [subject, setSubject] = React.useState(initialSubject)
921
+
922
+ React.useEffect(() => {
923
+ if ((ccContacts?.length ?? 0) > 0 || (bccContacts?.length ?? 0) > 0) {
924
+ setShowCcBcc(true)
925
+ }
926
+ }, [ccContacts, bccContacts])
927
+
928
+ return (
929
+ <div className="mx-4 mt-3 rounded-md border border-border/60 bg-muted/[0.16] text-sm">
930
+ <div className="flex items-center gap-3 px-4 py-2 border-b border-border/30">
931
+ <span className="text-xs text-muted-foreground w-10 shrink-0">From</span>
932
+ <div className="flex items-baseline gap-1.5 min-w-0">
933
+ <span className="text-sm text-foreground">{fromName}</span>
934
+ <span className="text-xs text-muted-foreground truncate">{fromEmail}</span>
935
+ </div>
936
+ </div>
937
+
938
+ <div className={`flex items-start gap-3 px-4 py-2 border-b border-border/30 ${hasUnconfirmedTo ? "bg-amber-50/35" : ""}`}>
939
+ <span className="text-xs text-muted-foreground w-10 shrink-0 mt-1.5">To</span>
940
+ <div className="flex-1 min-w-0">
941
+ <div className="flex flex-wrap items-center gap-1.5">
942
+ {toContacts.map((contact, index) => (
943
+ <div
944
+ key={`${contact.name}-${index}`}
945
+ onClick={() => setExpandedToIndex((prev) => (prev === index ? null : index))}
946
+ className={`inline-flex max-w-[300px] cursor-pointer items-start gap-2 rounded-md border px-2 py-1 text-xs ${
947
+ contact.confirmed
948
+ ? "border-border/80 bg-background text-foreground"
949
+ : "border-amber-300 bg-amber-50 text-amber-800"
950
+ }`}
951
+ >
952
+ <div className="min-w-0">
953
+ <div className="flex items-center gap-1">
954
+ <span className="font-medium leading-none">{contact.name}</span>
955
+ {contact.confirmed && <Check className="h-3 w-3 text-emerald-600" />}
956
+ </div>
957
+ <div className="mt-0.5 truncate text-muted-foreground">
958
+ {contact.email ?? contact.emails?.[0] ?? "no email"}
959
+ </div>
960
+ </div>
961
+ <div className="ml-auto flex items-center gap-1">
962
+ {!contact.confirmed && (
963
+ <button
964
+ type="button"
965
+ onClick={(e) => {
966
+ e.stopPropagation()
967
+ onConfirmToContact?.(index)
968
+ }}
969
+ className="h-6 rounded px-1.5 text-[10px] font-semibold hover:bg-amber-100"
970
+ >
971
+ Confirm
972
+ </button>
973
+ )}
974
+ <button
975
+ type="button"
976
+ onClick={(e) => {
977
+ e.stopPropagation()
978
+ onRemoveToContact?.(index)
979
+ setExpandedToIndex((prev) => (prev === index ? null : prev))
980
+ }}
981
+ className="rounded p-0.5 text-muted-foreground hover:text-foreground hover:bg-muted/40"
982
+ aria-label="Remove recipient"
983
+ >
984
+ <X className="h-3 w-3" />
985
+ </button>
986
+ </div>
987
+ </div>
988
+ ))}
989
+ <input
990
+ type="text"
991
+ value={manualTo}
992
+ onChange={(e) => setManualTo(e.target.value)}
993
+ placeholder={toContacts.length ? "Add recipient..." : "Type a name or email..."}
994
+ className="min-w-[140px] flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-muted-foreground/40 py-1"
995
+ onKeyDown={(e) => {
996
+ if (e.key === "Enter" && manualTo.trim()) {
997
+ onAddToContact?.({ name: manualTo.trim(), role: "", email: manualTo.trim(), confirmed: true })
998
+ setManualTo("")
999
+ }
1000
+ }}
1001
+ />
1002
+ </div>
1003
+ {expandedToIndex !== null && toContacts[expandedToIndex] && (
1004
+ <div className="mt-2 rounded-md border border-border bg-background px-3 py-2 text-xs text-muted-foreground">
1005
+ <div className="flex items-center justify-between gap-2">
1006
+ <div className="font-medium text-foreground text-sm">
1007
+ {toContacts[expandedToIndex].name}
1008
+ </div>
1009
+ <button
1010
+ type="button"
1011
+ onClick={() => {
1012
+ const contact = toContacts[expandedToIndex]
1013
+ if (contact.salesforceUrl) {
1014
+ window.open(contact.salesforceUrl, "_blank", "noopener,noreferrer")
1015
+ } else {
1016
+ onOpenAccountDetails?.()
1017
+ }
1018
+ }}
1019
+ className="h-7 w-7 inline-flex items-center justify-center rounded-md border border-border bg-background hover:bg-muted/40 transition-colors shrink-0"
1020
+ aria-label="Open in Salesforce"
1021
+ >
1022
+ {iconMap?.salesforce ? (
1023
+ <BrandIcon src={iconMap.salesforce} alt="Salesforce" className="w-3.5 h-3.5" />
1024
+ ) : (
1025
+ <ExternalLink className="w-3.5 h-3.5 text-muted-foreground" />
1026
+ )}
1027
+ </button>
1028
+ </div>
1029
+ {toContacts[expandedToIndex].role && (
1030
+ <div className="mt-1">{toContacts[expandedToIndex].role}</div>
1031
+ )}
1032
+ {(toContacts[expandedToIndex].emails?.length ?? 0) > 0 ? (
1033
+ <div className="mt-2">
1034
+ <div className="text-[11px] text-muted-foreground/70 mb-1">Send using</div>
1035
+ <select
1036
+ className="h-8 rounded-md border border-border bg-background px-2 text-xs text-foreground"
1037
+ value={toContacts[expandedToIndex].email ?? toContacts[expandedToIndex].emails?.[0] ?? ""}
1038
+ onChange={(e) =>
1039
+ onUpdateToContact?.(expandedToIndex, {
1040
+ ...toContacts[expandedToIndex],
1041
+ email: e.target.value,
1042
+ })
1043
+ }
1044
+ >
1045
+ {toContacts[expandedToIndex].emails?.map((emailOption) => (
1046
+ <option key={emailOption} value={emailOption}>
1047
+ {emailOption}
1048
+ </option>
1049
+ ))}
1050
+ </select>
1051
+ </div>
1052
+ ) : toContacts[expandedToIndex].email ? (
1053
+ <div className="mt-1">{toContacts[expandedToIndex].email}</div>
1054
+ ) : null}
1055
+ {(toContacts[expandedToIndex].phones?.length ?? 0) > 0 ? (
1056
+ <div className="mt-1">{toContacts[expandedToIndex].phones?.join(" · ")}</div>
1057
+ ) : toContacts[expandedToIndex].phone ? (
1058
+ <div className="mt-1">{toContacts[expandedToIndex].phone}</div>
1059
+ ) : null}
1060
+ </div>
1061
+ )}
1062
+ <div className="flex items-center flex-wrap gap-1.5 mt-1.5">
1063
+ {accountContacts && accountContacts.length > 0 && (
1064
+ <AccountContactsPopover
1065
+ contacts={accountContacts}
1066
+ onSelect={(c) => onAddToContact?.({ ...c, confirmed: true })}
1067
+ onSelectTo={(c) => onAddToContact?.({ ...c, confirmed: true })}
1068
+ onSelectCc={(c) => onCcAdd?.({ ...c, confirmed: true })}
1069
+ onSelectBcc={(c) => onBccAdd?.({ ...c, confirmed: true })}
1070
+ onViewAll={onOpenAccountDetails}
1071
+ onOpenRecentActivity={onOpenRecentActivity}
1072
+ iconMap={iconMap}
1073
+ trigger={
1074
+ <button className="h-7 rounded-md border border-border bg-background px-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors">
1075
+ Contacts
1076
+ </button>
1077
+ }
1078
+ />
1079
+ )}
1080
+ <button
1081
+ type="button"
1082
+ onClick={onOpenAccountDetails}
1083
+ className="h-7 rounded-md border border-border bg-background px-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors"
1084
+ >
1085
+ {accountDetailsLabel}
1086
+ </button>
1087
+ <button
1088
+ type="button"
1089
+ onClick={() => setShowCcBcc(true)}
1090
+ className="h-7 rounded-md border border-border bg-background px-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors"
1091
+ >
1092
+ Add Cc/Bcc
1093
+ </button>
1094
+ </div>
1095
+ </div>
1096
+ </div>
1097
+
1098
+ {showCcBcc && (
1099
+ <div className="flex items-start gap-3 px-4 py-2 border-b border-border/30 animate-in fade-in slide-in-from-top-1 duration-150">
1100
+ <span className="text-xs text-muted-foreground w-10 shrink-0 mt-1.5">Cc</span>
1101
+ <div className="flex-1 min-w-0">
1102
+ <div className="flex flex-wrap gap-1.5">
1103
+ {ccContacts?.map((c, i) => (
1104
+ <ContactCard key={i} contact={c} variant="secondary" onRemove={() => onCcRemove?.(i)} />
1105
+ ))}
1106
+ <div className="flex items-center gap-2">
1107
+ <input
1108
+ type="text"
1109
+ value={manualCc}
1110
+ onChange={(e) => setManualCc(e.target.value)}
1111
+ placeholder="Add Cc..."
1112
+ className="text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-muted-foreground/40 py-1 w-28"
1113
+ onKeyDown={(e) => {
1114
+ if (e.key === "Enter" && manualCc.trim()) {
1115
+ onCcAdd?.({ name: manualCc.trim(), role: "", email: manualCc.trim(), confirmed: true })
1116
+ setManualCc("")
1117
+ }
1118
+ }}
1119
+ />
1120
+ {accountContacts && accountContacts.length > 0 && (
1121
+ <AccountContactsPopover
1122
+ contacts={accountContacts}
1123
+ onSelect={(c) => onCcAdd?.(c)}
1124
+ onViewAll={onOpenAccountDetails}
1125
+ onOpenRecentActivity={onOpenRecentActivity}
1126
+ iconMap={iconMap}
1127
+ trigger={
1128
+ <button className="text-xs text-muted-foreground hover:text-foreground transition-colors">
1129
+ <Users className="w-3 h-3" />
1130
+ </button>
1131
+ }
1132
+ />
1133
+ )}
1134
+ </div>
1135
+ </div>
1136
+ </div>
1137
+ </div>
1138
+ )}
1139
+
1140
+ {showCcBcc && (
1141
+ <div className="flex items-start gap-3 px-4 py-2 border-b border-border/30 animate-in fade-in slide-in-from-top-1 duration-150">
1142
+ <span className="text-xs text-muted-foreground w-10 shrink-0 mt-1.5">Bcc</span>
1143
+ <div className="flex-1 min-w-0">
1144
+ <div className="flex flex-wrap gap-1.5">
1145
+ {bccContacts?.map((c, i) => (
1146
+ <ContactCard key={`${c.name}-${i}`} contact={c} variant="secondary" onRemove={() => onBccRemove?.(i)} />
1147
+ ))}
1148
+ <div className="flex items-center gap-2">
1149
+ <input
1150
+ type="text"
1151
+ value={manualBcc}
1152
+ onChange={(e) => setManualBcc(e.target.value)}
1153
+ placeholder="Add Bcc..."
1154
+ className="text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-muted-foreground/40 py-1 w-28"
1155
+ onKeyDown={(e) => {
1156
+ if (e.key === "Enter" && manualBcc.trim()) {
1157
+ onBccAdd?.({ name: manualBcc.trim(), role: "", email: manualBcc.trim(), confirmed: true })
1158
+ setManualBcc("")
1159
+ }
1160
+ }}
1161
+ />
1162
+ {accountContacts && accountContacts.length > 0 && (
1163
+ <AccountContactsPopover
1164
+ contacts={accountContacts}
1165
+ onSelect={(c) => onBccAdd?.(c)}
1166
+ onViewAll={onOpenAccountDetails}
1167
+ onOpenRecentActivity={onOpenRecentActivity}
1168
+ iconMap={iconMap}
1169
+ trigger={
1170
+ <button className="text-xs text-muted-foreground hover:text-foreground transition-colors">
1171
+ <Users className="w-3 h-3" />
1172
+ </button>
1173
+ }
1174
+ />
1175
+ )}
1176
+ </div>
1177
+ </div>
1178
+ </div>
1179
+ </div>
1180
+ )}
1181
+
1182
+ {showSubject && (
1183
+ <div className="flex items-center gap-3 px-4 py-2 border-b border-border/30">
1184
+ <span className="text-xs text-muted-foreground w-10 shrink-0">Subj</span>
1185
+ <input
1186
+ type="text"
1187
+ value={subject}
1188
+ onChange={(e) => setSubject(e.target.value)}
1189
+ placeholder="Subject..."
1190
+ className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-muted-foreground/40 py-1 font-medium"
1191
+ />
1192
+ </div>
1193
+ )}
1194
+ </div>
1195
+ )
1196
+ }
1197
+
1198
+ // ---------------------------------------------------------------------------
1199
+ // SignatureBlock
1200
+ // ---------------------------------------------------------------------------
1201
+
1202
+ function SignatureBlock({
1203
+ signature,
1204
+ enabled,
1205
+ onToggle,
1206
+ }: {
1207
+ signature: string | React.ReactNode
1208
+ enabled: boolean
1209
+ onToggle: () => void
1210
+ }) {
1211
+ if (!enabled) {
1212
+ return (
1213
+ <div className="px-3 py-2 border-t border-border/30">
1214
+ <button
1215
+ onClick={onToggle}
1216
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1.5"
1217
+ >
1218
+ <PenLine className="w-3 h-3" />
1219
+ Add signature
1220
+ </button>
1221
+ </div>
1222
+ )
1223
+ }
1224
+
1225
+ return (
1226
+ <div className="px-3 py-2 border-t border-border/30 bg-background/40">
1227
+ <div className="flex items-center justify-between mb-1">
1228
+ <span className="text-xs text-muted-foreground/60">--</span>
1229
+ <button onClick={onToggle} className="text-xs text-muted-foreground hover:text-foreground transition-colors">
1230
+ Remove
1231
+ </button>
1232
+ </div>
1233
+ <div className="text-xs text-muted-foreground/70 whitespace-pre-line leading-relaxed">
1234
+ {signature}
1235
+ </div>
1236
+ </div>
1237
+ )
1238
+ }
1239
+
1240
+ // ---------------------------------------------------------------------------
1241
+ // SuggestedActionCard
1242
+ // ---------------------------------------------------------------------------
1243
+
1244
+ function SuggestedActionCard({
1245
+ action,
1246
+ onDismiss,
1247
+ onSend,
1248
+ onSaveDraft,
1249
+ accountContacts,
1250
+ signature,
1251
+ onDuplicate,
1252
+ onOpenAccountDetails,
1253
+ onOpenRecentActivity,
1254
+ onMarkComplete,
1255
+ onDispatchAgent,
1256
+ iconMap,
1257
+ sendLabel,
1258
+ accountDetailsLabel,
1259
+ dismissLabel = "Dismiss",
1260
+ }: {
1261
+ action: SuggestedAction
1262
+ onDismiss?: (id: number | string) => void
1263
+ onSend?: (id: number | string) => void
1264
+ onSaveDraft?: (id: number | string) => void
1265
+ accountContacts?: SuggestedContact[]
1266
+ signature?: string | React.ReactNode
1267
+ onDuplicate?: (id: number | string) => void
1268
+ onOpenAccountDetails?: () => void
1269
+ onOpenRecentActivity?: () => void
1270
+ onMarkComplete?: (id: number | string) => void
1271
+ onDispatchAgent?: (id: number | string, editedContent?: string, settings?: { aiDisclosureEnabled?: boolean; maxDurationMinutes?: string; callRecordingEnabled?: boolean; recordingNoticeEnabled?: boolean }) => void
1272
+ iconMap?: SuggestedActionsIconMap
1273
+ sendLabel?: string
1274
+ accountDetailsLabel?: string
1275
+ dismissLabel?: string
1276
+ }) {
1277
+ const isCall = action.type === "call"
1278
+ const [expanded, setExpanded] = React.useState(action.type === "email" || isCall)
1279
+ const [content, setContent] = React.useState(
1280
+ isCall
1281
+ ? action.callMeta?.talkTrack ?? ""
1282
+ : action.content?.replace(/<\/p>/gi, "\n\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").replace(/\n\s*\n/g, "\n\n") ?? ""
1283
+ )
1284
+ const [showAiEdit, setShowAiEdit] = React.useState(false)
1285
+ const [feedbackOpen, setFeedbackOpen] = React.useState(false)
1286
+ const [followUpEnabled, setFollowUpEnabled] = React.useState(action.followUp?.enabled ?? false)
1287
+ const [threadExpanded, setThreadExpanded] = React.useState(false)
1288
+ const [expandedMessageId, setExpandedMessageId] = React.useState<string | null>(null)
1289
+ const [replyingToMessageId, setReplyingToMessageId] = React.useState<string | null>(null)
1290
+
1291
+ const [ticketPriority, setTicketPriority] = React.useState(action.ticket?.priority ?? "Medium")
1292
+ const [ticketType, setTicketType] = React.useState(action.ticket?.type ?? "Support Request")
1293
+ const [ticketSubject, setTicketSubject] = React.useState(action.ticket?.subject ?? "")
1294
+ const [ticketDescription, setTicketDescription] = React.useState(
1295
+ action.ticket?.description?.replace(/<\/p>/gi, "\n\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").trim() ?? ""
1296
+ )
1297
+
1298
+ const [toContacts, setToContacts] = React.useState<SuggestedContact[]>(action.emailMeta?.to ? [action.emailMeta.to] : [])
1299
+ const [ccContacts, setCcContacts] = React.useState<SuggestedContact[]>(action.emailMeta?.cc ?? [])
1300
+ const [bccContacts, setBccContacts] = React.useState<SuggestedContact[]>(
1301
+ action.emailMeta?.bcc
1302
+ ? [{ name: action.emailMeta.bcc, role: "", email: action.emailMeta.bcc, confirmed: true }]
1303
+ : []
1304
+ )
1305
+ const [callContact, setCallContact] = React.useState<SuggestedContact | undefined>(action.callMeta?.contact)
1306
+ const [signatureEnabled, setSignatureEnabled] = React.useState(true)
1307
+
1308
+
1309
+ const isNewEmail = action.type === "email" && !action.replyTo
1310
+ const isThreadReply = action.type === "email" && !!action.replyTo
1311
+
1312
+ const canSend = isCall
1313
+ ? callContact?.confirmed ?? false
1314
+ : action.type === "email"
1315
+ ? toContacts.length > 0 && toContacts.every((contact) => contact.confirmed)
1316
+ : true
1317
+
1318
+ const openInLabel =
1319
+ action.type === "email" ? "Open draft"
1320
+ : action.type === "ticket" ? "Open in Zendesk"
1321
+ : action.type === "slack" ? "Open in Slack"
1322
+ : "Open in App"
1323
+
1324
+ const addUniqueContact = React.useCallback((prev: SuggestedContact[], contact: SuggestedContact) => {
1325
+ const nextKey = `${contact.name}-${contact.email ?? contact.emails?.[0] ?? ""}`
1326
+ if (prev.some((item) => `${item.name}-${item.email ?? item.emails?.[0] ?? ""}` === nextKey)) {
1327
+ return prev
1328
+ }
1329
+ return [...prev, contact]
1330
+ }, [])
1331
+
1332
+ if (!expanded) {
1333
+ return (
1334
+ <div
1335
+ onClick={() => setExpanded(true)}
1336
+ className="p-4 flex items-center justify-between cursor-pointer hover:bg-muted/30 transition-colors"
1337
+ >
1338
+ <div className="flex items-center gap-3">
1339
+ <div className="flex items-center justify-center w-7 h-7 shrink-0">
1340
+ {getActionTypeIcon(action.type, "w-5 h-5", iconMap)}
1341
+ </div>
1342
+ <div>
1343
+ <div className="text-sm font-medium text-foreground">{action.label}</div>
1344
+ {action.followUp?.enabled && (
1345
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground mt-1">
1346
+ <Clock className="w-3.5 h-3.5" />
1347
+ <span>Follow-up in {action.followUp.days}d</span>
1348
+ </div>
1349
+ )}
1350
+ </div>
1351
+ </div>
1352
+ <ChevronDown className="w-5 h-5 text-muted-foreground shrink-0" />
1353
+ </div>
1354
+ )
1355
+ }
1356
+
1357
+ return (
1358
+ <div className="animate-in fade-in zoom-in-95 duration-200">
1359
+ {/* Header */}
1360
+ <div className="px-4 py-3 flex items-center justify-between bg-background border-b border-border/40">
1361
+ <div className="flex items-center gap-3">
1362
+ <div className="flex items-center justify-center w-7 h-7 shrink-0">
1363
+ {getActionTypeIcon(action.type, "w-5 h-5", iconMap)}
1364
+ </div>
1365
+ <div className="flex items-center gap-2">
1366
+ <span className="text-sm font-medium text-foreground">{action.label}</span>
1367
+ {isThreadReply && (
1368
+ <span className="inline-flex items-center gap-1 rounded-md border border-border/80 bg-muted/50 px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
1369
+ <Undo2 className="h-3 w-3" />
1370
+ Reply
1371
+ </span>
1372
+ )}
1373
+ </div>
1374
+ </div>
1375
+ <div className="flex items-center gap-1.5">
1376
+ <button
1377
+ onClick={() => setFeedbackOpen(!feedbackOpen)}
1378
+ className={`p-1.5 rounded transition-colors ${
1379
+ feedbackOpen
1380
+ ? "bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400"
1381
+ : "hover:bg-muted text-muted-foreground hover:text-foreground"
1382
+ }`}
1383
+ >
1384
+ <ThumbsUp className="w-3.5 h-3.5" />
1385
+ </button>
1386
+ <button
1387
+ onClick={() => setFeedbackOpen(!feedbackOpen)}
1388
+ className="p-1.5 rounded transition-colors hover:bg-muted text-muted-foreground hover:text-foreground"
1389
+ >
1390
+ <ThumbsDown className="w-3.5 h-3.5" />
1391
+ </button>
1392
+ <div className="w-px h-4 bg-border/40 mx-0.5" />
1393
+ <span className="text-xs text-muted-foreground">4:15 PM</span>
1394
+ <button
1395
+ onClick={() => setExpanded(false)}
1396
+ className="text-muted-foreground hover:text-foreground hover:bg-muted/50 p-2 rounded transition-colors"
1397
+ aria-label="Collapse"
1398
+ >
1399
+ <ChevronUp className="w-4 h-4" />
1400
+ </button>
1401
+ </div>
1402
+ </div>
1403
+
1404
+ {/* Feedback below header */}
1405
+ {feedbackOpen && (
1406
+ <div className="px-5 py-3 border-b border-border/40 animate-in fade-in slide-in-from-top-2 duration-200">
1407
+ <DraftFeedbackInline
1408
+ onRegenerateRequest={(pills, detail) => console.log("Regenerate:", pills, detail)}
1409
+ onSubmitFeedback={(type, pills, detail) => console.log("Feedback:", type, pills, detail)}
1410
+ onDiscardRequest={(pills, detail) => {
1411
+ console.log("Discard:", pills, detail)
1412
+ onDismiss?.(action.id)
1413
+ }}
1414
+ />
1415
+ </div>
1416
+ )}
1417
+
1418
+ {/* Thread Context (email thread reply only) */}
1419
+ {isThreadReply && action.replyTo && (
1420
+ <div className="border-b border-border/40">
1421
+ {action.threadMessages && action.threadMessages.length > 1 && (
1422
+ <div className="px-5 py-2 border-b border-border/40">
1423
+ <button
1424
+ onClick={() => setThreadExpanded(!threadExpanded)}
1425
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors"
1426
+ >
1427
+ {action.threadMessages.length} messages in this thread
1428
+ <span className="mx-1.5">&middot;</span>
1429
+ <span className="underline">{threadExpanded ? "Hide thread" : "View thread"}</span>
1430
+ </button>
1431
+ </div>
1432
+ )}
1433
+
1434
+ {threadExpanded && action.threadMessages && action.threadMessages.length > 1 ? (
1435
+ <div className="bg-muted/20">
1436
+ {action.threadMessages.map((msg, idx) => {
1437
+ const isExpMsg = expandedMessageId === msg.id
1438
+ const isReplyingTo = !replyingToMessageId
1439
+ ? idx === action.threadMessages!.length - 1
1440
+ : replyingToMessageId === msg.id
1441
+ const isLast = idx === action.threadMessages!.length - 1
1442
+
1443
+ return (
1444
+ <div key={msg.id}>
1445
+ <div
1446
+ className="px-5 py-3 hover:bg-muted/30 cursor-pointer transition-colors group relative"
1447
+ onClick={() => setExpandedMessageId(isExpMsg ? null : msg.id)}
1448
+ >
1449
+ <div className="flex justify-between items-start mb-1">
1450
+ <div className="flex items-center gap-2">
1451
+ <span className="text-xs font-semibold text-foreground">{msg.from}</span>
1452
+ {isReplyingTo && (
1453
+ <span className="text-xs text-muted-foreground flex items-center gap-1">
1454
+ <ArrowLeft className="w-3 h-3" />
1455
+ Replying to this
1456
+ </span>
1457
+ )}
1458
+ </div>
1459
+ <div className="flex items-center gap-2">
1460
+ <span className="text-xs text-muted-foreground">{msg.time}</span>
1461
+ {!isReplyingTo && !isExpMsg && (
1462
+ <button
1463
+ onClick={(e) => { e.stopPropagation(); setReplyingToMessageId(msg.id) }}
1464
+ className="text-xs text-muted-foreground hover:text-foreground underline opacity-0 group-hover:opacity-100 transition-opacity"
1465
+ >
1466
+ Reply to this
1467
+ </button>
1468
+ )}
1469
+ </div>
1470
+ </div>
1471
+ <div className={`text-sm text-muted-foreground leading-relaxed ${isExpMsg ? "" : "line-clamp-2"}`}>
1472
+ {isExpMsg ? msg.content : msg.preview}
1473
+ </div>
1474
+ {isExpMsg && (
1475
+ <button
1476
+ onClick={(e) => { e.stopPropagation(); setExpandedMessageId(null) }}
1477
+ className="text-xs text-muted-foreground hover:text-foreground underline mt-2"
1478
+ >
1479
+ Collapse
1480
+ </button>
1481
+ )}
1482
+ </div>
1483
+ {!isLast && <div className="h-px bg-border/40 mx-5" />}
1484
+ </div>
1485
+ )
1486
+ })}
1487
+ </div>
1488
+ ) : (
1489
+ <div className="px-5 py-3 bg-muted/30">
1490
+ <div className="flex justify-between items-start mb-2">
1491
+ <span className="text-xs font-semibold text-foreground">{action.replyTo.from} to Me</span>
1492
+ <span className="text-xs text-muted-foreground">{action.replyTo.time}</span>
1493
+ </div>
1494
+ <div className="text-sm text-muted-foreground leading-relaxed line-clamp-3">{action.replyTo.content}</div>
1495
+ </div>
1496
+ )}
1497
+ </div>
1498
+ )}
1499
+
1500
+ {/* Reply Context (Slack) */}
1501
+ {action.type === "slack" && action.replyTo && (
1502
+ <div className="border-b border-border/40">
1503
+ <div className="px-5 py-3 bg-muted/30">
1504
+ <div className="flex justify-between items-start mb-2">
1505
+ <span className="text-xs font-semibold text-foreground">{action.replyTo.from} to Me</span>
1506
+ <span className="text-xs text-muted-foreground">{action.replyTo.time}</span>
1507
+ </div>
1508
+ <div className="text-sm text-muted-foreground leading-relaxed line-clamp-3">{action.replyTo.content}</div>
1509
+ </div>
1510
+ </div>
1511
+ )}
1512
+
1513
+ {/* EmailHeader */}
1514
+ {action.type === "email" && action.emailMeta && (
1515
+ <div>
1516
+ {isThreadReply && (
1517
+ <div className="mx-4 mt-3 flex items-center gap-1.5 rounded-md border border-border/60 bg-muted/[0.16] px-3 py-1.5 text-xs text-muted-foreground">
1518
+ <Undo2 className="h-3 w-3" />
1519
+ Replying in existing thread
1520
+ </div>
1521
+ )}
1522
+ <EmailHeader
1523
+ fromName={action.emailMeta.from}
1524
+ fromEmail={action.emailMeta.fromEmail}
1525
+ toContacts={toContacts}
1526
+ ccContacts={ccContacts}
1527
+ initialSubject={action.emailMeta.subject}
1528
+ accountContacts={accountContacts}
1529
+ bccContacts={bccContacts}
1530
+ onAddToContact={(c) => setToContacts((prev) => addUniqueContact(prev, c))}
1531
+ onRemoveToContact={(index) => setToContacts((prev) => prev.filter((_, idx) => idx !== index))}
1532
+ onConfirmToContact={(index) =>
1533
+ setToContacts((prev) =>
1534
+ prev.map((contact, idx) => (idx === index ? { ...contact, confirmed: true } : contact))
1535
+ )
1536
+ }
1537
+ onUpdateToContact={(index, updatedContact) =>
1538
+ setToContacts((prev) =>
1539
+ prev.map((contact, idx) => (idx === index ? updatedContact : contact))
1540
+ )
1541
+ }
1542
+ onCcAdd={(c) =>
1543
+ setCcContacts((prev) => addUniqueContact(prev, c))
1544
+ }
1545
+ onCcRemove={(i) => setCcContacts((prev) => prev.filter((_, idx) => idx !== i))}
1546
+ onBccAdd={(c) =>
1547
+ setBccContacts((prev) => addUniqueContact(prev, c))
1548
+ }
1549
+ onBccRemove={(i) => setBccContacts((prev) => prev.filter((_, idx) => idx !== i))}
1550
+ onOpenAccountDetails={onOpenAccountDetails}
1551
+ onOpenRecentActivity={onOpenRecentActivity}
1552
+ iconMap={iconMap}
1553
+ showSubject={isNewEmail}
1554
+ accountDetailsLabel={accountDetailsLabel}
1555
+ />
1556
+ </div>
1557
+ )}
1558
+
1559
+ {/* Call contact card */}
1560
+ {isCall && (
1561
+ <div className="mx-4 mt-3 rounded-md border border-border/60 bg-muted/[0.16] px-3 py-2.5">
1562
+ <div className="flex items-start gap-3">
1563
+ <span className="text-xs text-muted-foreground w-10 shrink-0 mt-1.5">To</span>
1564
+ <div className="flex-1 min-w-0">
1565
+ {callContact ? (
1566
+ <div className="flex flex-wrap items-center gap-1.5">
1567
+ <div
1568
+ className={`inline-flex max-w-[300px] items-start gap-2 rounded-md border px-2 py-1 text-xs ${
1569
+ callContact.confirmed
1570
+ ? "border-border/80 bg-background text-foreground"
1571
+ : "border-amber-300 bg-amber-50 text-amber-800"
1572
+ }`}
1573
+ >
1574
+ <div className="min-w-0">
1575
+ <div className="flex items-center gap-1">
1576
+ <span className="font-medium leading-none">{callContact.name}</span>
1577
+ {callContact.confirmed && <Check className="h-3 w-3 text-emerald-600" />}
1578
+ </div>
1579
+ {callContact.role && (
1580
+ <div className="mt-0.5 truncate text-muted-foreground">{callContact.role}</div>
1581
+ )}
1582
+ {(callContact.phone || callContact.phones?.[0]) && (
1583
+ <div className="mt-0.5 truncate text-muted-foreground">{callContact.phone ?? callContact.phones?.[0]}</div>
1584
+ )}
1585
+ </div>
1586
+ <div className="ml-auto flex items-center gap-1">
1587
+ {!callContact.confirmed && (
1588
+ <button
1589
+ type="button"
1590
+ onClick={(e) => {
1591
+ e.stopPropagation()
1592
+ setCallContact({ ...callContact, confirmed: true })
1593
+ }}
1594
+ className="h-6 rounded px-1.5 text-[10px] font-semibold hover:bg-amber-100"
1595
+ >
1596
+ Confirm
1597
+ </button>
1598
+ )}
1599
+ <button
1600
+ type="button"
1601
+ onClick={(e) => {
1602
+ e.stopPropagation()
1603
+ setCallContact(undefined)
1604
+ }}
1605
+ className="rounded p-0.5 text-muted-foreground hover:text-foreground hover:bg-muted/40"
1606
+ aria-label="Remove recipient"
1607
+ >
1608
+ <X className="h-3 w-3" />
1609
+ </button>
1610
+ </div>
1611
+ </div>
1612
+ </div>
1613
+ ) : (
1614
+ <div className="text-sm text-muted-foreground">No contact selected.</div>
1615
+ )}
1616
+ <div className="flex items-center flex-wrap gap-1.5 mt-1.5">
1617
+ {accountContacts && accountContacts.length > 0 && (
1618
+ <AccountContactsPopover
1619
+ contacts={accountContacts}
1620
+ onSelect={(c) => setCallContact({ ...c, confirmed: true })}
1621
+ onViewAll={onOpenAccountDetails}
1622
+ onOpenRecentActivity={onOpenRecentActivity}
1623
+ iconMap={iconMap}
1624
+ trigger={
1625
+ <button className="h-7 rounded-md border border-border bg-background px-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors">
1626
+ Contacts
1627
+ </button>
1628
+ }
1629
+ />
1630
+ )}
1631
+ <button
1632
+ type="button"
1633
+ onClick={onOpenAccountDetails}
1634
+ className="h-7 rounded-md border border-border bg-background px-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors"
1635
+ >
1636
+ {accountDetailsLabel ?? "Account details"}
1637
+ </button>
1638
+ </div>
1639
+ </div>
1640
+ </div>
1641
+ </div>
1642
+ )}
1643
+
1644
+
1645
+
1646
+ {/* Content Area */}
1647
+ <div>
1648
+ {action.type === "ticket" ? (
1649
+ <div className="p-5 space-y-4">
1650
+ <div className="grid grid-cols-2 gap-4">
1651
+ <div className="space-y-1.5">
1652
+ <label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Priority</label>
1653
+ <select
1654
+ className="w-full px-3 py-2 text-sm bg-background border border-border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-primary/20"
1655
+ value={ticketPriority}
1656
+ onChange={(e) => setTicketPriority(e.target.value)}
1657
+ >
1658
+ <option>High</option>
1659
+ <option>Medium</option>
1660
+ <option>Low</option>
1661
+ </select>
1662
+ </div>
1663
+ <div className="space-y-1.5">
1664
+ <label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Type</label>
1665
+ <select
1666
+ className="w-full px-3 py-2 text-sm bg-background border border-border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-primary/20"
1667
+ value={ticketType}
1668
+ onChange={(e) => setTicketType(e.target.value)}
1669
+ >
1670
+ <option>Churn Risk</option>
1671
+ <option>Support Request</option>
1672
+ <option>Feature Request</option>
1673
+ </select>
1674
+ </div>
1675
+ </div>
1676
+ <div className="space-y-1.5">
1677
+ <label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Subject</label>
1678
+ <input
1679
+ className="w-full px-3 py-2 text-sm bg-background border border-border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-primary/20"
1680
+ value={ticketSubject}
1681
+ onChange={(e) => setTicketSubject(e.target.value)}
1682
+ />
1683
+ </div>
1684
+ <div className="space-y-1.5">
1685
+ <label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Description</label>
1686
+ <textarea
1687
+ className="w-full min-h-[100px] px-3 py-2 text-sm bg-background border border-border rounded-md shadow-sm resize-none focus:outline-none focus:ring-1 focus:ring-primary/20"
1688
+ value={ticketDescription}
1689
+ onChange={(e) => setTicketDescription(e.target.value)}
1690
+ />
1691
+ </div>
1692
+ </div>
1693
+ ) : isCall ? (
1694
+ <div className="flex flex-col">
1695
+ <div className="px-4 pt-3 pb-3">
1696
+ <div className="rounded-md border border-border/60 bg-muted/[0.16] overflow-hidden">
1697
+ <div className="px-3 pt-2 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/70">
1698
+ Talk track
1699
+ </div>
1700
+ <div className="px-3 pb-3">
1701
+ <textarea
1702
+ className="w-full min-h-[280px] text-sm leading-relaxed bg-transparent border-none resize-none focus:ring-0 focus:outline-none placeholder:text-muted-foreground/50 p-0 overflow-y-auto"
1703
+ value={content}
1704
+ onChange={(e) => setContent(e.target.value)}
1705
+ placeholder="Edit talk track..."
1706
+ />
1707
+ </div>
1708
+ </div>
1709
+ </div>
1710
+ <EditorToolbar />
1711
+ </div>
1712
+ ) : action.type === "manual" ? (
1713
+ <div className="p-5 space-y-4">
1714
+ <div className="rounded-md border border-border/60 bg-muted/[0.16] overflow-hidden">
1715
+ <div className="px-3 pt-2 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/70">
1716
+ Task description
1717
+ </div>
1718
+ <div className="px-3 pb-3">
1719
+ <p className="text-sm leading-relaxed text-foreground whitespace-pre-wrap">
1720
+ {action.manualMeta?.taskDescription ?? action.content ?? ""}
1721
+ </p>
1722
+ </div>
1723
+ </div>
1724
+ </div>
1725
+ ) : action.type === "browser" ? (
1726
+ <div className="p-5 space-y-4">
1727
+ <div className="rounded-md border border-border/60 bg-muted/[0.16] overflow-hidden">
1728
+ <div className="px-3 pt-2 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/70">
1729
+ Browser automation
1730
+ </div>
1731
+ <div className="px-3 pb-3 space-y-2">
1732
+ {action.browserMeta?.url && (
1733
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
1734
+ <Globe className="w-3.5 h-3.5 shrink-0" />
1735
+ <a href={action.browserMeta.url} target="_blank" rel="noopener noreferrer" className="text-primary underline underline-offset-2 truncate">
1736
+ {action.browserMeta.url}
1737
+ </a>
1738
+ </div>
1739
+ )}
1740
+ <p className="text-sm leading-relaxed text-foreground whitespace-pre-wrap">
1741
+ {action.browserMeta?.actionDescription ?? action.content ?? ""}
1742
+ </p>
1743
+ </div>
1744
+ </div>
1745
+ </div>
1746
+ ) : (
1747
+ <div className="flex flex-col">
1748
+ <div className="px-4 pt-3 pb-3">
1749
+ <div className="rounded-md border border-border/60 bg-muted/[0.16] overflow-hidden">
1750
+ <div className="px-3 pt-2 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/70">
1751
+ Draft
1752
+ </div>
1753
+ <div className="px-3 pb-3">
1754
+ <textarea
1755
+ className="w-full min-h-[280px] text-sm leading-relaxed bg-transparent border-none resize-none focus:ring-0 focus:outline-none placeholder:text-muted-foreground/50 p-0 overflow-y-auto"
1756
+ value={content}
1757
+ onChange={(e) => setContent(e.target.value)}
1758
+ placeholder="Write your message..."
1759
+ />
1760
+ </div>
1761
+ {action.type === "email" && signature && (
1762
+ <SignatureBlock signature={signature} enabled={signatureEnabled} onToggle={() => setSignatureEnabled(!signatureEnabled)} />
1763
+ )}
1764
+ </div>
1765
+ </div>
1766
+ <EditorToolbar />
1767
+ </div>
1768
+ )}
1769
+ </div>
1770
+
1771
+ {/* Footer */}
1772
+ <div className="bg-muted/10 border-t border-border/60">
1773
+ <div className="px-5 py-4">
1774
+ {showAiEdit && <AiEditPanel onApply={(pills, desc) => console.log("AI Edit:", pills, desc)} />}
1775
+
1776
+ {action.followUp && (
1777
+ <div className="mb-3">
1778
+ <FollowUpToggle checked={followUpEnabled} onCheckedChange={setFollowUpEnabled} days={action.followUp.days} />
1779
+ </div>
1780
+ )}
1781
+
1782
+ <div className="flex items-center justify-between">
1783
+ <div className="flex items-center gap-2 overflow-x-auto no-scrollbar">
1784
+ <Button
1785
+ variant="ghost"
1786
+ size="sm"
1787
+ className={`h-9 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 ${showAiEdit ? "bg-muted/50 text-foreground" : ""}`}
1788
+ onClick={() => setShowAiEdit(!showAiEdit)}
1789
+ >
1790
+ AI Edit
1791
+ </Button>
1792
+ {!isCall && action.type !== "email" && (
1793
+ <Button
1794
+ variant="ghost"
1795
+ size="sm"
1796
+ className="h-9 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50"
1797
+ onClick={() => onSaveDraft?.(action.id)}
1798
+ >
1799
+ Draft
1800
+ </Button>
1801
+ )}
1802
+ {!isCall && (
1803
+ <Button variant="ghost" size="sm" className="h-9 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 whitespace-nowrap">
1804
+ {openInLabel}
1805
+ </Button>
1806
+ )}
1807
+ <Button
1808
+ variant="ghost"
1809
+ size="sm"
1810
+ className="h-9 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50"
1811
+ onClick={() => onDismiss?.(action.id)}
1812
+ >
1813
+ {dismissLabel}
1814
+ </Button>
1815
+ {action.type === "email" && onDuplicate && (
1816
+ <Button
1817
+ variant="ghost"
1818
+ size="sm"
1819
+ className="h-9 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 whitespace-nowrap"
1820
+ onClick={() => onDuplicate(action.id)}
1821
+ >
1822
+ <Copy className="w-3 h-3 mr-1.5" />
1823
+ Copy
1824
+ </Button>
1825
+ )}
1826
+ </div>
1827
+
1828
+ <div className="flex items-center gap-2 pl-2 shrink-0">
1829
+ <div className="w-px h-4 bg-border/60" />
1830
+ {isCall ? (
1831
+ <>
1832
+ {action.callMeta?.allowDispatchAgent && (
1833
+ <Button
1834
+ variant="outline"
1835
+ size="sm"
1836
+ className="h-9 text-xs font-medium shadow-none"
1837
+ onClick={() => onDispatchAgent?.(action.id, content)}
1838
+ disabled={!canSend}
1839
+ >
1840
+ Dispatch agent
1841
+ </Button>
1842
+ )}
1843
+ <Button
1844
+ size="sm"
1845
+ className={`px-5 h-9 text-xs font-semibold shadow-sm ${
1846
+ canSend
1847
+ ? "bg-foreground text-background hover:bg-foreground/90"
1848
+ : "bg-muted text-muted-foreground cursor-not-allowed"
1849
+ }`}
1850
+ onClick={() => canSend && onMarkComplete?.(action.id)}
1851
+ disabled={!canSend}
1852
+ >
1853
+ <Check className="w-3.5 h-3.5 mr-1.5" />
1854
+ Mark complete
1855
+ </Button>
1856
+ </>
1857
+ ) : action.type === "manual" ? (
1858
+ <Button
1859
+ size="sm"
1860
+ className="px-5 h-9 text-xs font-semibold shadow-sm bg-foreground text-background hover:bg-foreground/90"
1861
+ onClick={() => onMarkComplete?.(action.id)}
1862
+ >
1863
+ <Check className="w-3.5 h-3.5 mr-1.5" />
1864
+ Mark complete
1865
+ </Button>
1866
+ ) : action.type === "browser" ? (
1867
+ <Button
1868
+ size="sm"
1869
+ className="px-5 h-9 text-xs font-semibold shadow-sm bg-foreground text-background hover:bg-foreground/90"
1870
+ onClick={() => onSend?.(action.id)}
1871
+ >
1872
+ <Globe className="w-3.5 h-3.5 mr-1.5" />
1873
+ {sendLabel ?? "Run"}
1874
+ </Button>
1875
+ ) : (
1876
+ <Button
1877
+ size="sm"
1878
+ className={`px-6 h-9 text-xs font-semibold shadow-sm ${
1879
+ canSend
1880
+ ? "bg-foreground text-background hover:bg-foreground/90"
1881
+ : "bg-muted text-muted-foreground cursor-not-allowed"
1882
+ }`}
1883
+ onClick={() => canSend && onSend?.(action.id)}
1884
+ disabled={!canSend}
1885
+ >
1886
+ {sendLabel ?? "Send"}
1887
+ </Button>
1888
+ )}
1889
+ </div>
1890
+ </div>
1891
+ </div>
1892
+ </div>
1893
+ </div>
1894
+ )
1895
+ }
1896
+
1897
+ // ---------------------------------------------------------------------------
1898
+ // SuggestedActions (public API)
1899
+ // ---------------------------------------------------------------------------
1900
+
1901
+ export interface SuggestedActionsProps {
1902
+ actions: SuggestedAction[]
1903
+ title?: string
1904
+ onDismiss?: (id: number | string) => void
1905
+ onSend?: (id: number | string) => void
1906
+ onSaveDraft?: (id: number | string) => void
1907
+ accountContacts?: SuggestedContact[]
1908
+ signature?: string | React.ReactNode
1909
+ onDuplicate?: (id: number | string) => void
1910
+ onOpenAccountDetails?: () => void
1911
+ onOpenRecentActivity?: () => void
1912
+ onMarkComplete?: (id: number | string) => void
1913
+ onDispatchAgent?: (id: number | string, editedContent?: string, settings?: { aiDisclosureEnabled?: boolean; maxDurationMinutes?: string; callRecordingEnabled?: boolean; recordingNoticeEnabled?: boolean }) => void
1914
+ iconMap?: SuggestedActionsIconMap
1915
+ sendLabel?: string
1916
+ accountDetailsLabel?: string
1917
+ dismissLabel?: string
1918
+ }
1919
+
1920
+ export function SuggestedActions({
1921
+ actions,
1922
+ title = "Suggested Actions",
1923
+ onDismiss,
1924
+ onSend,
1925
+ onSaveDraft,
1926
+ accountContacts,
1927
+ signature,
1928
+ onDuplicate,
1929
+ onOpenAccountDetails,
1930
+ onOpenRecentActivity,
1931
+ onMarkComplete,
1932
+ onDispatchAgent,
1933
+ iconMap,
1934
+ sendLabel,
1935
+ accountDetailsLabel,
1936
+ dismissLabel,
1937
+ }: SuggestedActionsProps) {
1938
+ const [dismissedIds, setDismissedIds] = React.useState<Set<number | string>>(new Set())
1939
+
1940
+ const handleDismiss = React.useCallback(
1941
+ (id: number | string) => {
1942
+ setDismissedIds((prev) => new Set([...prev, id]))
1943
+ onDismiss?.(id)
1944
+ },
1945
+ [onDismiss]
1946
+ )
1947
+
1948
+ const visibleActions = actions.filter((a) => a.status !== "dismissed" && !dismissedIds.has(a.id))
1949
+
1950
+ return (
1951
+ <div className="py-6 border-t border-border">
1952
+ <div className="flex items-center justify-between mb-4">
1953
+ <div className="text-[11px] font-bold text-muted-foreground/70 uppercase tracking-wider">{title}</div>
1954
+ <span className="text-[11px] text-muted-foreground">{visibleActions.length} actions</span>
1955
+ </div>
1956
+
1957
+ <div className="space-y-4">
1958
+ {visibleActions.map((action) => (
1959
+ <div
1960
+ key={action.id}
1961
+ className="group bg-background border border-border rounded-md overflow-hidden shadow-sm hover:shadow-md transition-all duration-200"
1962
+ >
1963
+ <SuggestedActionCard
1964
+ action={action}
1965
+ onDismiss={handleDismiss}
1966
+ onSend={onSend}
1967
+ onSaveDraft={onSaveDraft}
1968
+ accountContacts={accountContacts}
1969
+ signature={signature}
1970
+ onDuplicate={onDuplicate}
1971
+ onOpenAccountDetails={onOpenAccountDetails}
1972
+ onOpenRecentActivity={onOpenRecentActivity}
1973
+ onMarkComplete={onMarkComplete}
1974
+ onDispatchAgent={onDispatchAgent}
1975
+ iconMap={iconMap}
1976
+ sendLabel={sendLabel}
1977
+ accountDetailsLabel={accountDetailsLabel}
1978
+ dismissLabel={dismissLabel}
1979
+ />
1980
+ </div>
1981
+ ))}
1982
+ </div>
1983
+ </div>
1984
+ )
1985
+ }