pxengine 0.1.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 (127) hide show
  1. package/README.md +175 -0
  2. package/config/tailwind-preset.js +106 -0
  3. package/dist/index.d.mts +1259 -0
  4. package/dist/index.d.ts +1259 -0
  5. package/dist/index.js +5175 -0
  6. package/dist/index.mjs +4929 -0
  7. package/package.json +94 -0
  8. package/src/atoms/AccordionAtom.tsx +44 -0
  9. package/src/atoms/AlertAtom.tsx +46 -0
  10. package/src/atoms/AlertDialogAtom.tsx +66 -0
  11. package/src/atoms/AspectRatioAtom.tsx +27 -0
  12. package/src/atoms/AvatarAtom.tsx +20 -0
  13. package/src/atoms/BadgeAtom.tsx +25 -0
  14. package/src/atoms/BreadcrumbAtom.tsx +36 -0
  15. package/src/atoms/ButtonAtom.tsx +63 -0
  16. package/src/atoms/CalendarAtom.tsx +24 -0
  17. package/src/atoms/CardAtom.tsx +64 -0
  18. package/src/atoms/CarouselAtom.tsx +40 -0
  19. package/src/atoms/CollapsibleAtom.tsx +44 -0
  20. package/src/atoms/CommandAtom.tsx +46 -0
  21. package/src/atoms/DialogAtom.tsx +68 -0
  22. package/src/atoms/InputAtom.tsx +162 -0
  23. package/src/atoms/LayoutAtom.tsx +43 -0
  24. package/src/atoms/PaginationAtom.tsx +49 -0
  25. package/src/atoms/PopoverAtom.tsx +40 -0
  26. package/src/atoms/ProgressAtom.tsx +15 -0
  27. package/src/atoms/ScrollAreaAtom.tsx +31 -0
  28. package/src/atoms/SeparatorAtom.tsx +16 -0
  29. package/src/atoms/SheetAtom.tsx +72 -0
  30. package/src/atoms/SkeletonAtom.tsx +22 -0
  31. package/src/atoms/SpinnerAtom.tsx +26 -0
  32. package/src/atoms/TableAtom.tsx +58 -0
  33. package/src/atoms/TabsAtom.tsx +40 -0
  34. package/src/atoms/TextAtom.tsx +35 -0
  35. package/src/atoms/TooltipAtom.tsx +39 -0
  36. package/src/atoms/index.ts +28 -0
  37. package/src/components/index.ts +178 -0
  38. package/src/components/ui/accordion.tsx +56 -0
  39. package/src/components/ui/alert-dialog.tsx +139 -0
  40. package/src/components/ui/alert.tsx +59 -0
  41. package/src/components/ui/aspect-ratio.tsx +5 -0
  42. package/src/components/ui/avatar.tsx +50 -0
  43. package/src/components/ui/badge.tsx +36 -0
  44. package/src/components/ui/breadcrumb.tsx +115 -0
  45. package/src/components/ui/button-group.tsx +83 -0
  46. package/src/components/ui/button.tsx +56 -0
  47. package/src/components/ui/calendar.tsx +213 -0
  48. package/src/components/ui/card.tsx +79 -0
  49. package/src/components/ui/carousel.tsx +260 -0
  50. package/src/components/ui/chart.tsx +367 -0
  51. package/src/components/ui/checkbox.tsx +28 -0
  52. package/src/components/ui/collapsible.tsx +11 -0
  53. package/src/components/ui/command.tsx +153 -0
  54. package/src/components/ui/context-menu.tsx +198 -0
  55. package/src/components/ui/dialog.tsx +122 -0
  56. package/src/components/ui/drawer.tsx +116 -0
  57. package/src/components/ui/dropdown-menu.tsx +200 -0
  58. package/src/components/ui/empty.tsx +104 -0
  59. package/src/components/ui/field.tsx +244 -0
  60. package/src/components/ui/form.tsx +176 -0
  61. package/src/components/ui/hover-card.tsx +27 -0
  62. package/src/components/ui/input-group.tsx +168 -0
  63. package/src/components/ui/input-otp.tsx +69 -0
  64. package/src/components/ui/input.tsx +22 -0
  65. package/src/components/ui/item.tsx +193 -0
  66. package/src/components/ui/kbd.tsx +28 -0
  67. package/src/components/ui/label.tsx +26 -0
  68. package/src/components/ui/menubar.tsx +254 -0
  69. package/src/components/ui/navigation-menu.tsx +128 -0
  70. package/src/components/ui/pagination.tsx +117 -0
  71. package/src/components/ui/popover.tsx +29 -0
  72. package/src/components/ui/progress.tsx +28 -0
  73. package/src/components/ui/radio-group.tsx +42 -0
  74. package/src/components/ui/resizable.tsx +45 -0
  75. package/src/components/ui/scroll-area.tsx +46 -0
  76. package/src/components/ui/select.tsx +160 -0
  77. package/src/components/ui/separator.tsx +29 -0
  78. package/src/components/ui/sheet.tsx +140 -0
  79. package/src/components/ui/sidebar.tsx +771 -0
  80. package/src/components/ui/skeleton.tsx +15 -0
  81. package/src/components/ui/slider.tsx +26 -0
  82. package/src/components/ui/sonner.tsx +45 -0
  83. package/src/components/ui/spinner.tsx +16 -0
  84. package/src/components/ui/switch.tsx +27 -0
  85. package/src/components/ui/table.tsx +117 -0
  86. package/src/components/ui/tabs.tsx +53 -0
  87. package/src/components/ui/textarea.tsx +22 -0
  88. package/src/components/ui/toggle-group.tsx +61 -0
  89. package/src/components/ui/toggle.tsx +43 -0
  90. package/src/components/ui/tooltip.tsx +30 -0
  91. package/src/hooks/use-mobile.tsx +19 -0
  92. package/src/index.ts +24 -0
  93. package/src/lib/countries.ts +203 -0
  94. package/src/lib/index.ts +2 -0
  95. package/src/lib/utils.ts +15 -0
  96. package/src/lib/validators/index.ts +1 -0
  97. package/src/lib/validators/theme.ts +148 -0
  98. package/src/molecules/creator-discovery/CampaignSeedCard/CampaignSeedCard.tsx +123 -0
  99. package/src/molecules/creator-discovery/CampaignSeedCard/CampaignSeedCard.types.ts +13 -0
  100. package/src/molecules/creator-discovery/CampaignSeedCard/index.ts +2 -0
  101. package/src/molecules/creator-discovery/MCQCard/MCQCard.tsx +165 -0
  102. package/src/molecules/creator-discovery/MCQCard/MCQCard.types.ts +71 -0
  103. package/src/molecules/creator-discovery/MCQCard/index.ts +2 -0
  104. package/src/molecules/creator-discovery/SearchSpecCard/CustomFieldRenderers.tsx +334 -0
  105. package/src/molecules/creator-discovery/SearchSpecCard/SearchSpecCard.tsx +111 -0
  106. package/src/molecules/creator-discovery/SearchSpecCard/SearchSpecCard.types.ts +18 -0
  107. package/src/molecules/creator-discovery/SearchSpecCard/index.ts +3 -0
  108. package/src/molecules/creator-discovery/index.ts +3 -0
  109. package/src/molecules/generic/ActionButton/ActionButton.tsx +137 -0
  110. package/src/molecules/generic/ActionButton/ActionButton.types.ts +68 -0
  111. package/src/molecules/generic/ActionButton/index.ts +2 -0
  112. package/src/molecules/generic/EditableField/EditableField.tsx +229 -0
  113. package/src/molecules/generic/EditableField/EditableField.types.ts +73 -0
  114. package/src/molecules/generic/EditableField/index.ts +2 -0
  115. package/src/molecules/generic/FormCard/FormCard.tsx +136 -0
  116. package/src/molecules/generic/FormCard/FormCard.types.ts +93 -0
  117. package/src/molecules/generic/FormCard/index.ts +2 -0
  118. package/src/molecules/generic/index.ts +3 -0
  119. package/src/molecules/index.ts +2 -0
  120. package/src/render/PXEngineRenderer.tsx +272 -0
  121. package/src/render/index.ts +1 -0
  122. package/src/styles/globals.css +146 -0
  123. package/src/types/atoms.ts +294 -0
  124. package/src/types/common.ts +116 -0
  125. package/src/types/index.ts +3 -0
  126. package/src/types/molecules.ts +54 -0
  127. package/src/types/schema.ts +12 -0
@@ -0,0 +1,71 @@
1
+ export interface MCQOption {
2
+ key: string;
3
+ label: string;
4
+ }
5
+
6
+ export interface MCQCardProps {
7
+ /**
8
+ * The question being asked
9
+ */
10
+ question: string;
11
+
12
+ /**
13
+ * The options to choose from
14
+ */
15
+ options: Record<string, string>;
16
+
17
+ /**
18
+ * The key of the recommended option
19
+ */
20
+ recommended?: string;
21
+
22
+ /**
23
+ * The currently selected option key
24
+ */
25
+ selectedOption?: string;
26
+
27
+ /**
28
+ * Triggered when an option is selected
29
+ */
30
+ onSelect?: (key: string) => void;
31
+
32
+ /**
33
+ * Triggered when the user clicks continue
34
+ */
35
+ onProceed?: (key: string) => void;
36
+
37
+ /**
38
+ * Whether the message is the latest
39
+ */
40
+ isLatestMessage?: boolean;
41
+
42
+ /**
43
+ * Countdown in seconds
44
+ */
45
+ countdown?: number;
46
+
47
+ /**
48
+ * Whether the countdown is paused
49
+ */
50
+ isPaused?: boolean;
51
+
52
+ /**
53
+ * Pause/Resume handler
54
+ */
55
+ onPause?: () => void;
56
+
57
+ /**
58
+ * Loading state during submission
59
+ */
60
+ isLoading?: boolean;
61
+
62
+ /**
63
+ * Custom className
64
+ */
65
+ className?: string;
66
+
67
+ /**
68
+ * Who made the final selection (for historical view)
69
+ */
70
+ selectionStatus?: "user" | "agent";
71
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./MCQCard";
2
+ export * from "./MCQCard.types";
@@ -0,0 +1,334 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { ChevronDown, Check } from "lucide-react";
3
+ import { countries } from "@/lib/countries";
4
+ import { Badge } from "@/components";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ // ============================================================================
8
+ // COUNTRY SELECT FIELD
9
+ // ============================================================================
10
+
11
+ export const CountrySelectEdit = ({
12
+ value,
13
+ onChange,
14
+ }: {
15
+ value: any;
16
+ onChange: (value: any) => void;
17
+ }) => {
18
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
19
+ const [searchTerm, setSearchTerm] = useState("");
20
+ const dropdownRef = useRef<HTMLDivElement>(null);
21
+
22
+ useEffect(() => {
23
+ const handleClickOutside = (event: MouseEvent) => {
24
+ if (
25
+ dropdownRef.current &&
26
+ !dropdownRef.current.contains(event.target as Node)
27
+ ) {
28
+ setIsDropdownOpen(false);
29
+ }
30
+ };
31
+ document.addEventListener("mousedown", handleClickOutside);
32
+ return () => document.removeEventListener("mousedown", handleClickOutside);
33
+ }, []);
34
+
35
+ const inputValue = Array.isArray(value) ? value : [];
36
+
37
+ return (
38
+ <div className="space-y-3">
39
+ <div className="relative" ref={dropdownRef}>
40
+ <div
41
+ className="flex-1 bg-white border border-gray200 rounded-md px-3 py-2 text-sm cursor-pointer flex items-center justify-between font-medium hover:border-purple500 transition-colors"
42
+ onClick={() => setIsDropdownOpen(!isDropdownOpen)}
43
+ >
44
+ <span className="text-gray-700">
45
+ {inputValue.length > 0
46
+ ? `${inputValue.length} ${inputValue.length === 1 ? "country" : "countries"} selected`
47
+ : "Select countries..."}
48
+ </span>
49
+ <ChevronDown
50
+ className={cn(
51
+ "h-4 w-4 text-gray-400 transition-transform",
52
+ isDropdownOpen && "rotate-180",
53
+ )}
54
+ />
55
+ </div>
56
+
57
+ {isDropdownOpen && (
58
+ <div className="absolute top-full left-0 right-0 mt-1 bg-white border border-gray200 rounded-lg shadow-xl z-50 max-h-60 overflow-hidden animate-in fade-in slide-in-from-top-1">
59
+ <div className="p-2 border-b border-gray-100">
60
+ <input
61
+ type="text"
62
+ placeholder="Search countries..."
63
+ value={searchTerm}
64
+ onChange={(e) => setSearchTerm(e.target.value)}
65
+ className="w-full px-3 py-1.5 text-sm border border-gray-100 rounded-md bg-gray-50 focus:outline-none focus:ring-2 focus:ring-purple500/20 focus:border-purple500"
66
+ onClick={(e) => e.stopPropagation()}
67
+ />
68
+ </div>
69
+
70
+ <div className="max-h-40 overflow-y-auto p-1">
71
+ {countries
72
+ .filter(
73
+ (country) =>
74
+ country.name
75
+ .toLowerCase()
76
+ .includes(searchTerm.toLowerCase()) ||
77
+ country.code
78
+ .toLowerCase()
79
+ .includes(searchTerm.toLowerCase()),
80
+ )
81
+ .map((country) => (
82
+ <div
83
+ key={country.code}
84
+ className={cn(
85
+ "flex items-center justify-between px-3 py-2 rounded-md hover:bg-purple50 cursor-pointer transition-colors",
86
+ inputValue.includes(country.code) && "bg-purple50/50",
87
+ )}
88
+ onClick={(e) => {
89
+ e.stopPropagation();
90
+ const currentGeography = inputValue;
91
+ if (currentGeography.includes(country.code)) {
92
+ onChange(
93
+ currentGeography.filter(
94
+ (c: string) => c !== country.code,
95
+ ),
96
+ );
97
+ } else {
98
+ onChange([...currentGeography, country.code]);
99
+ }
100
+ }}
101
+ >
102
+ <div className="flex items-center gap-2">
103
+ <span className="text-sm text-gray-700">
104
+ {country.name}
105
+ </span>
106
+ <span className="text-xs text-gray-400 font-mono">
107
+ {country.code}
108
+ </span>
109
+ </div>
110
+ {inputValue.includes(country.code) && (
111
+ <Check className="h-4 w-4 text-purple500" />
112
+ )}
113
+ </div>
114
+ ))}
115
+ </div>
116
+ </div>
117
+ )}
118
+ </div>
119
+
120
+ {inputValue.length > 0 && (
121
+ <div className="flex flex-wrap gap-1.5">
122
+ {inputValue.map((countryCode: string) => (
123
+ <Badge
124
+ key={countryCode}
125
+ variant="secondary"
126
+ className="bg-purple50 text-purple700 border-purple100 hover:bg-purple100 cursor-default flex items-center gap-1 pr-1"
127
+ >
128
+ {countryCode}
129
+ <button
130
+ onClick={() =>
131
+ onChange(inputValue.filter((c: string) => c !== countryCode))
132
+ }
133
+ className="hover:bg-purple200 rounded-full p-0.5 transition-colors"
134
+ >
135
+ <ChevronDown className="h-3 w-3 rotate-45" />{" "}
136
+ {/* Close icon substitute */}
137
+ </button>
138
+ </Badge>
139
+ ))}
140
+ </div>
141
+ )}
142
+ </div>
143
+ );
144
+ };
145
+
146
+ export const CountrySelectDisplay = ({ value }: { value: any }) => {
147
+ if (!value || !Array.isArray(value) || value.length === 0) {
148
+ return <span className="text-muted-foreground italic">Not specified</span>;
149
+ }
150
+
151
+ return (
152
+ <div className="flex flex-wrap gap-1.5">
153
+ {value.map((countryCode: string) => (
154
+ <Badge
155
+ key={countryCode}
156
+ variant="outline"
157
+ className="bg-gray-50 text-gray-700 border-gray-200"
158
+ >
159
+ {countryCode}
160
+ </Badge>
161
+ ))}
162
+ </div>
163
+ );
164
+ };
165
+
166
+ // ============================================================================
167
+ // KEYWORD BUNDLES FIELD
168
+ // ============================================================================
169
+
170
+ export const KeywordBundlesEdit = ({
171
+ value,
172
+ onChange,
173
+ }: {
174
+ value: any;
175
+ onChange: (value: any) => void;
176
+ }) => {
177
+ const bundles = Array.isArray(value) ? value : [];
178
+
179
+ const groups: { [priority: number]: { bundle: any; index: number }[] } = {};
180
+ bundles.forEach((b: any, idx: number) => {
181
+ const p = Number(b?.priority) || 1;
182
+ if (!groups[p]) groups[p] = [];
183
+ groups[p].push({ bundle: b, index: idx });
184
+ });
185
+
186
+ const sortedPriorities = Object.keys(groups)
187
+ .map((n) => parseInt(n))
188
+ .sort((a, b) => a - b);
189
+
190
+ return (
191
+ <div className="space-y-6 pt-2">
192
+ {sortedPriorities.map((priority) => (
193
+ <div key={priority} className="space-y-3">
194
+ <div className="flex items-center gap-2">
195
+ <Badge className="bg-purple500 hover:bg-purple500">
196
+ Priority {priority}
197
+ </Badge>
198
+ <div className="h-px flex-1 bg-gray-100" />
199
+ </div>
200
+ {groups[priority].map(({ bundle, index: bundleIndex }) => (
201
+ <div
202
+ key={bundleIndex}
203
+ className="bg-gray-50/50 border border-gray-100 rounded-xl p-4 transition-all hover:bg-gray-50 hover:border-gray-200 shadow-sm"
204
+ >
205
+ <div className="flex flex-col gap-3">
206
+ <label className="text-[10px] font-bold text-gray-400 uppercase tracking-widest pl-1">
207
+ Keywords
208
+ </label>
209
+ <div className="space-y-3">
210
+ <div className="flex flex-wrap gap-1.5">
211
+ {Array.isArray(bundle.keywords) &&
212
+ bundle.keywords.map((keyword: string, kIndex: number) => (
213
+ <Badge
214
+ key={kIndex}
215
+ className="bg-white border-gray-200 text-gray-700 hover:bg-red-50 hover:text-red-600 hover:border-red-100 transition-all cursor-pointer group"
216
+ onClick={() => {
217
+ const updatedBundles = [...bundles];
218
+ updatedBundles[bundleIndex] = {
219
+ ...bundle,
220
+ keywords: bundle.keywords.filter(
221
+ (_: string, i: number) => i !== kIndex,
222
+ ),
223
+ };
224
+ onChange(updatedBundles);
225
+ }}
226
+ >
227
+ {keyword}
228
+ <span className="ml-1 opacity-0 group-hover:opacity-100 transition-opacity">
229
+ ×
230
+ </span>
231
+ </Badge>
232
+ ))}
233
+ </div>
234
+ <div className="flex gap-2">
235
+ <input
236
+ type="text"
237
+ placeholder="Add keyword..."
238
+ className="flex-1 px-3 py-1.5 text-sm border border-gray-200 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500"
239
+ onKeyPress={(e) => {
240
+ if (e.key === "Enter") {
241
+ const input = e.target as HTMLInputElement;
242
+ const val = input.value.trim();
243
+ if (val) {
244
+ const updatedBundles = [...bundles];
245
+ updatedBundles[bundleIndex] = {
246
+ ...bundle,
247
+ keywords: [...(bundle.keywords || []), val],
248
+ };
249
+ onChange(updatedBundles);
250
+ input.value = "";
251
+ }
252
+ }
253
+ }}
254
+ />
255
+ </div>
256
+ </div>
257
+ <div className="flex flex-col gap-1.5 mt-1">
258
+ <label className="text-[10px] font-bold text-gray-400 uppercase tracking-widest pl-1">
259
+ Priority Level (1-5)
260
+ </label>
261
+ <input
262
+ type="number"
263
+ min="1"
264
+ max="5"
265
+ value={bundle.priority || 1}
266
+ onChange={(e) => {
267
+ const updatedBundles = [...bundles];
268
+ updatedBundles[bundleIndex] = {
269
+ ...bundle,
270
+ priority: parseInt(e.target.value) || 1,
271
+ };
272
+ onChange(updatedBundles);
273
+ }}
274
+ className="w-20 px-3 py-1.5 text-sm border border-gray-200 rounded-md bg-white focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 outline-none"
275
+ />
276
+ </div>
277
+ </div>
278
+ </div>
279
+ ))}
280
+ </div>
281
+ ))}
282
+ </div>
283
+ );
284
+ };
285
+
286
+ export const KeywordBundlesDisplay = ({ value }: { value: any }) => {
287
+ const bundles = Array.isArray(value) ? value : [];
288
+ if (bundles.length === 0)
289
+ return <span className="text-muted-foreground italic">Not specified</span>;
290
+
291
+ const groups: { [priority: number]: string[] } = {};
292
+ bundles.forEach((b: any) => {
293
+ const p = Number(b?.priority) || 1;
294
+ const keywords = Array.isArray(b?.keywords) ? b.keywords : [];
295
+ if (!groups[p]) groups[p] = [];
296
+ groups[p].push(...keywords);
297
+ });
298
+
299
+ const sortedPriorities = Object.keys(groups)
300
+ .map((n) => parseInt(n))
301
+ .sort((a, b) => a - b);
302
+
303
+ return (
304
+ <div className="space-y-4 pt-1">
305
+ {sortedPriorities.map((priority) => {
306
+ const deduped = Array.from(new Set(groups[priority]));
307
+ return (
308
+ <div key={priority} className="space-y-2">
309
+ <div className="flex items-center gap-2">
310
+ <Badge
311
+ variant="outline"
312
+ className="text-[10px] uppercase tracking-wider text-purple600 border-purple100 bg-purple50/30"
313
+ >
314
+ Priority {priority}
315
+ </Badge>
316
+ <div className="h-px flex-1 bg-gray-100/50" />
317
+ </div>
318
+ <div className="flex flex-wrap gap-1.5">
319
+ {deduped.map((keyword: string) => (
320
+ <Badge
321
+ key={keyword}
322
+ variant="secondary"
323
+ className="bg-white border-gray-200 text-gray-700 font-medium"
324
+ >
325
+ {keyword}
326
+ </Badge>
327
+ ))}
328
+ </div>
329
+ </div>
330
+ );
331
+ })}
332
+ </div>
333
+ );
334
+ };
@@ -0,0 +1,111 @@
1
+ import React from "react";
2
+ import { SearchSpecCardProps } from "./SearchSpecCard.types";
3
+ import { FormCard } from "../../generic/FormCard";
4
+ import { FieldConfig } from "@/types/common";
5
+ import {
6
+ CountrySelectEdit,
7
+ CountrySelectDisplay,
8
+ KeywordBundlesEdit,
9
+ KeywordBundlesDisplay,
10
+ } from "./CustomFieldRenderers";
11
+ import { CheckCircle2 } from "lucide-react";
12
+
13
+ /**
14
+ * Default field configuration for Search Specification
15
+ */
16
+ export const SEARCH_SPEC_FIELDS: FieldConfig[] = [
17
+ {
18
+ key: "platforms",
19
+ label: "Platforms",
20
+ type: "select", // Changed to select for simplicity in form, or custom if multi-select needed
21
+ placeholder: "Select platforms",
22
+ options: ["Instagram", "YouTube", "TikTok"],
23
+ },
24
+ {
25
+ key: "follower_range",
26
+ label: "Follower Range",
27
+ type: "slider",
28
+ placeholder: "Not specified",
29
+ sliderConfig: {
30
+ min: 0,
31
+ max: 5000000,
32
+ step: 10000,
33
+ formatValue: (value: any) => {
34
+ if (!value || typeof value !== "object") return "0 - 2.5M followers";
35
+ const formatNum = (n: number) => {
36
+ if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
37
+ if (n >= 1000) return `${(n / 1000).toFixed(0)}K`;
38
+ return n.toLocaleString();
39
+ };
40
+ return `${formatNum(value.min || 0)} - ${formatNum(value.max || 2500000)} followers`;
41
+ },
42
+ },
43
+ },
44
+ {
45
+ key: "geography",
46
+ label: "Geography",
47
+ type: "custom",
48
+ renderEdit: (value, onChange) => (
49
+ <CountrySelectEdit value={value} onChange={onChange} />
50
+ ),
51
+ renderDisplay: (value) => <CountrySelectDisplay value={value} />,
52
+ },
53
+ {
54
+ key: "keyword_bundles",
55
+ label: "Keyword Bundles",
56
+ type: "custom",
57
+ renderEdit: (value, onChange) => (
58
+ <KeywordBundlesEdit value={value} onChange={onChange} />
59
+ ),
60
+ renderDisplay: (value) => <KeywordBundlesDisplay value={value} />,
61
+ },
62
+ {
63
+ key: "desired_candidate_count",
64
+ label: "Initial Creators",
65
+ type: "number",
66
+ numberConfig: {
67
+ min: 1,
68
+ formatValue: (value: any) => `${value || 0} creators`,
69
+ },
70
+ },
71
+ ];
72
+
73
+ /**
74
+ * SearchSpecCard
75
+ *
76
+ * A domain-specific molecule for the Creator Discovery workflow.
77
+ * Encapsulates search settings like platforms, ranges, and custom keyword bundles.
78
+ */
79
+ export const SearchSpecCard = React.memo<SearchSpecCardProps>(
80
+ ({
81
+ selectionStatus,
82
+ isLatestMessage = true,
83
+ className,
84
+ ...formCardProps
85
+ }) => {
86
+ return (
87
+ <FormCard
88
+ {...formCardProps}
89
+ title={formCardProps.title || "Creator Search Settings"}
90
+ fields={SEARCH_SPEC_FIELDS}
91
+ className={className}
92
+ footer={
93
+ !isLatestMessage && selectionStatus ? (
94
+ <div className="flex justify-end items-center gap-1.5 text-green-600 text-xs font-semibold py-1">
95
+ <CheckCircle2 className="h-4 w-4" />
96
+ <span>
97
+ {selectionStatus === "agent"
98
+ ? "Selected by Agent"
99
+ : "Selected by User"}
100
+ </span>
101
+ </div>
102
+ ) : (
103
+ formCardProps.footer
104
+ )
105
+ }
106
+ />
107
+ );
108
+ },
109
+ );
110
+
111
+ SearchSpecCard.displayName = "SearchSpecCard";
@@ -0,0 +1,18 @@
1
+ import { FormCardProps } from "../../generic/FormCard";
2
+
3
+ export interface SearchSpecCardProps extends Omit<FormCardProps, "fields"> {
4
+ /**
5
+ * Version of the search specification
6
+ */
7
+ version?: number;
8
+
9
+ /**
10
+ * Status of the selection
11
+ */
12
+ selectionStatus?: "user" | "agent";
13
+
14
+ /**
15
+ * Whether the message is the latest
16
+ */
17
+ isLatestMessage?: boolean;
18
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./SearchSpecCard";
2
+ export * from "./SearchSpecCard.types";
3
+ export * from "./CustomFieldRenderers";
@@ -0,0 +1,3 @@
1
+ export * from "./CampaignSeedCard";
2
+ export * from "./SearchSpecCard";
3
+ export * from "./MCQCard";
@@ -0,0 +1,137 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { ActionButtonProps } from "./ActionButton.types";
3
+ import { Button } from "@/components";
4
+ import { cn } from "@/lib/utils";
5
+ import { Pause, Play, ArrowRight, Loader2 } from "lucide-react";
6
+
7
+ /**
8
+ * ActionButton
9
+ *
10
+ * A powerful button molecule that supports:
11
+ * - Auto-countdown for agent workflows
12
+ * - Pause/Resume toggle
13
+ * - Loading and Disabled states
14
+ * - Custom themes
15
+ */
16
+ export const ActionButton = React.memo<ActionButtonProps>(
17
+ ({
18
+ label,
19
+ secondaryLabel,
20
+ countdown: countdownProp,
21
+ isPaused = false,
22
+ onPause,
23
+ onProceed,
24
+ variant = "default",
25
+ size = "default",
26
+ disabled = false,
27
+ isLoading = false,
28
+ className,
29
+ showCountdown = true,
30
+ }) => {
31
+ const [timeLeft, setTimeLeft] = useState(countdownProp || 0);
32
+
33
+ // Sync with prop
34
+ useEffect(() => {
35
+ if (countdownProp !== undefined) {
36
+ setTimeLeft(countdownProp);
37
+ }
38
+ }, [countdownProp]);
39
+
40
+ // Countdown logic
41
+ useEffect(() => {
42
+ if (
43
+ countdownProp === undefined ||
44
+ countdownProp <= 0 ||
45
+ isPaused ||
46
+ isLoading ||
47
+ disabled
48
+ ) {
49
+ return;
50
+ }
51
+
52
+ const timer = setInterval(() => {
53
+ setTimeLeft((prev) => {
54
+ if (prev <= 1) {
55
+ clearInterval(timer);
56
+ onProceed();
57
+ return 0;
58
+ }
59
+ return prev - 1;
60
+ });
61
+ }, 1000);
62
+
63
+ return () => clearInterval(timer);
64
+ }, [countdownProp, isPaused, isLoading, disabled, onProceed]);
65
+
66
+ const progress = countdownProp ? (timeLeft / countdownProp) * 100 : 0;
67
+
68
+ return (
69
+ <div className={cn("inline-flex items-center gap-2", className)}>
70
+ <Button
71
+ variant={variant}
72
+ size={size}
73
+ disabled={disabled || isLoading}
74
+ onClick={onProceed}
75
+ className={cn(
76
+ "relative min-w-[140px] overflow-hidden group transition-all duration-300",
77
+ "bg-purple500 hover:bg-purple600 text-white font-semibold rounded-full px-6 py-2.5 shadow-md hover:shadow-lg",
78
+ variant === "outline" &&
79
+ "bg-transparent border-purple500 text-purple500 hover:bg-purple50",
80
+ isLoading && "opacity-80",
81
+ )}
82
+ >
83
+ <div className="relative z-10 flex items-center justify-center gap-2">
84
+ {isLoading ? (
85
+ <Loader2 className="h-4 w-4 animate-spin" />
86
+ ) : (
87
+ <>
88
+ <span>{label}</span>
89
+ {showCountdown &&
90
+ countdownProp &&
91
+ countdownProp > 0 &&
92
+ !isLoading && (
93
+ <span className="opacity-70 text-xs bg-white/20 px-1.5 py-0.5 rounded-md tabular-nums min-w-[24px]">
94
+ {timeLeft}s
95
+ </span>
96
+ )}
97
+ <ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" />
98
+ </>
99
+ )}
100
+ </div>
101
+
102
+ {/* Progress Background Overlay (Optional) */}
103
+ {!isPaused && countdownProp && countdownProp > 0 && (
104
+ <div
105
+ className="absolute bottom-0 left-0 h-0.5 bg-white/30 transition-all duration-1000 ease-linear"
106
+ style={{ width: `${progress}%` }}
107
+ />
108
+ )}
109
+ </Button>
110
+
111
+ {countdownProp !== undefined && countdownProp > 0 && onPause && (
112
+ <Button
113
+ variant="ghost"
114
+ size="icon"
115
+ onClick={onPause}
116
+ className="h-10 w-10 rounded-full hover:bg-gray-100 text-gray-500"
117
+ title={isPaused ? "Resume auto-proceed" : "Pause auto-proceed"}
118
+ >
119
+ {isPaused ? (
120
+ <Play className="h-4 w-4 fill-current" />
121
+ ) : (
122
+ <Pause className="h-4 w-4 fill-current" />
123
+ )}
124
+ </Button>
125
+ )}
126
+
127
+ {secondaryLabel && (
128
+ <span className="text-sm text-gray-500 font-medium italic">
129
+ {secondaryLabel}
130
+ </span>
131
+ )}
132
+ </div>
133
+ );
134
+ },
135
+ );
136
+
137
+ ActionButton.displayName = "ActionButton";