@wealthx/shadcn 1.5.28 → 1.5.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +104 -104
- package/CHANGELOG.md +6 -0
- package/dist/{chunk-CE2WONIY.mjs → chunk-AE4JKISB.mjs} +27 -31
- package/dist/chunk-BZWQU52U.mjs +1025 -0
- package/dist/components/ui/ai-builder/index.js +993 -12
- package/dist/components/ui/ai-builder/index.mjs +27 -3
- package/dist/components/ui/ai-conversations/index.js +27 -31
- package/dist/components/ui/ai-conversations/index.mjs +1 -1
- package/dist/index.js +4976 -4972
- package/dist/index.mjs +2 -2
- package/dist/styles.css +1 -1
- package/package.json +4 -1
- package/src/components/index.tsx +0 -2
- package/src/components/ui/ai-builder/agent-card.tsx +7 -5
- package/src/components/ui/ai-builder/agent-settings.tsx +709 -0
- package/src/components/ui/ai-builder/index.tsx +27 -2
- package/src/components/ui/ai-builder/service-config-modal.tsx +11 -11
- package/src/components/ui/ai-builder/types.ts +27 -15
- package/src/components/ui/ai-conversations/thread.tsx +9 -11
- package/src/styles/styles-css.ts +1 -1
- package/dist/chunk-T5PGVLMR.mjs +0 -479
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
closestCenter,
|
|
4
|
+
DndContext,
|
|
5
|
+
KeyboardSensor,
|
|
6
|
+
PointerSensor,
|
|
7
|
+
useSensor,
|
|
8
|
+
useSensors,
|
|
9
|
+
type DragEndEvent,
|
|
10
|
+
} from "@dnd-kit/core";
|
|
11
|
+
import {
|
|
12
|
+
SortableContext,
|
|
13
|
+
sortableKeyboardCoordinates,
|
|
14
|
+
useSortable,
|
|
15
|
+
verticalListSortingStrategy,
|
|
16
|
+
} from "@dnd-kit/sortable";
|
|
17
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
18
|
+
import {
|
|
19
|
+
Download,
|
|
20
|
+
GripVertical,
|
|
21
|
+
Info,
|
|
22
|
+
Pencil,
|
|
23
|
+
Plus,
|
|
24
|
+
Trash2,
|
|
25
|
+
Upload,
|
|
26
|
+
} from "lucide-react";
|
|
27
|
+
import { Badge } from "@/components/ui/badge";
|
|
28
|
+
import { Button } from "@/components/ui/button";
|
|
29
|
+
import {
|
|
30
|
+
Dialog,
|
|
31
|
+
DialogContent,
|
|
32
|
+
DialogFooter,
|
|
33
|
+
DialogHeader,
|
|
34
|
+
DialogTitle,
|
|
35
|
+
} from "@/components/ui/dialog";
|
|
36
|
+
import { Input } from "@/components/ui/input";
|
|
37
|
+
import { Label } from "@/components/ui/label";
|
|
38
|
+
import {
|
|
39
|
+
Select,
|
|
40
|
+
SelectContent,
|
|
41
|
+
SelectItem,
|
|
42
|
+
SelectTrigger,
|
|
43
|
+
SelectValue,
|
|
44
|
+
} from "@/components/ui/select";
|
|
45
|
+
import { Separator } from "@/components/ui/separator";
|
|
46
|
+
import { Switch } from "@/components/ui/switch";
|
|
47
|
+
import {
|
|
48
|
+
Table,
|
|
49
|
+
TableBody,
|
|
50
|
+
TableCell,
|
|
51
|
+
TableHead,
|
|
52
|
+
TableHeader,
|
|
53
|
+
TableRow,
|
|
54
|
+
} from "@/components/ui/table";
|
|
55
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
56
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
57
|
+
import {
|
|
58
|
+
Tooltip,
|
|
59
|
+
TooltipContent,
|
|
60
|
+
TooltipProvider,
|
|
61
|
+
TooltipTrigger,
|
|
62
|
+
} from "@/components/ui/tooltip";
|
|
63
|
+
import { cn } from "@/lib/utils";
|
|
64
|
+
import type { AiBuilderAgentConfig, AiBuilderRuleItem } from "./types";
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// SectionHeader
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export type SectionHeaderProps = {
|
|
71
|
+
title: string;
|
|
72
|
+
description: string;
|
|
73
|
+
className?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export function SectionHeader({
|
|
77
|
+
title,
|
|
78
|
+
description,
|
|
79
|
+
className,
|
|
80
|
+
}: SectionHeaderProps) {
|
|
81
|
+
return (
|
|
82
|
+
<div className={cn("flex flex-col gap-0.5", className)}>
|
|
83
|
+
<h2 className="text-h5">{title}</h2>
|
|
84
|
+
<p className="text-body-small text-muted-foreground">{description}</p>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// SettingRow
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export type SettingRowProps = {
|
|
94
|
+
icon: React.ReactNode;
|
|
95
|
+
label: string;
|
|
96
|
+
description: string;
|
|
97
|
+
control: React.ReactNode;
|
|
98
|
+
className?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function SettingRow({
|
|
102
|
+
icon,
|
|
103
|
+
label,
|
|
104
|
+
description,
|
|
105
|
+
control,
|
|
106
|
+
className,
|
|
107
|
+
}: SettingRowProps) {
|
|
108
|
+
return (
|
|
109
|
+
<div className={cn("flex items-center gap-4 py-4", className)}>
|
|
110
|
+
<div className="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
111
|
+
{icon}
|
|
112
|
+
</div>
|
|
113
|
+
<div className="flex flex-1 flex-col gap-0.5">
|
|
114
|
+
<span className="text-sm font-semibold">{label}</span>
|
|
115
|
+
<span className="text-caption text-muted-foreground">
|
|
116
|
+
{description}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="shrink-0">{control}</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// SettingCard — wraps SettingRows with a bordered card + dividers
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
export type SettingCardProps = {
|
|
129
|
+
rows: SettingRowProps[];
|
|
130
|
+
className?: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function SettingCard({ rows, className }: SettingCardProps) {
|
|
134
|
+
return (
|
|
135
|
+
<div className={cn("rounded-none border px-4", className)}>
|
|
136
|
+
{rows.map((row, i) => (
|
|
137
|
+
<React.Fragment key={row.label}>
|
|
138
|
+
<SettingRow {...row} />
|
|
139
|
+
{i < rows.length - 1 && <Separator />}
|
|
140
|
+
</React.Fragment>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// AgentConfigForm
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
export type AgentConfigFormProps = {
|
|
151
|
+
config: AiBuilderAgentConfig;
|
|
152
|
+
onChange: (config: AiBuilderAgentConfig) => void;
|
|
153
|
+
className?: string;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export function AgentConfigForm({
|
|
157
|
+
config,
|
|
158
|
+
onChange,
|
|
159
|
+
className,
|
|
160
|
+
}: AgentConfigFormProps) {
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
className={cn("flex flex-col gap-4 rounded-none border p-6", className)}
|
|
164
|
+
>
|
|
165
|
+
<div className="flex flex-col gap-1.5">
|
|
166
|
+
<Label htmlFor="agent-name">Agent Name</Label>
|
|
167
|
+
<Input
|
|
168
|
+
id="agent-name"
|
|
169
|
+
value={config.name}
|
|
170
|
+
onChange={(e) => onChange({ ...config, name: e.target.value })}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="flex flex-col gap-1.5">
|
|
174
|
+
<Label htmlFor="agent-desc">Business Description</Label>
|
|
175
|
+
<Textarea
|
|
176
|
+
id="agent-desc"
|
|
177
|
+
rows={4}
|
|
178
|
+
placeholder="What business is this agent for? What industry?"
|
|
179
|
+
value={config.businessDescription}
|
|
180
|
+
onChange={(e) =>
|
|
181
|
+
onChange({ ...config, businessDescription: e.target.value })
|
|
182
|
+
}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// ResponseTemplateEditModal
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
export type ResponseTemplateEditModalProps = {
|
|
194
|
+
open: boolean;
|
|
195
|
+
onOpenChange: (open: boolean) => void;
|
|
196
|
+
channel: "email" | "chat";
|
|
197
|
+
content: string;
|
|
198
|
+
onConfirm: (content: string) => void;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export function ResponseTemplateEditModal({
|
|
202
|
+
open,
|
|
203
|
+
onOpenChange,
|
|
204
|
+
channel,
|
|
205
|
+
content,
|
|
206
|
+
onConfirm,
|
|
207
|
+
}: ResponseTemplateEditModalProps) {
|
|
208
|
+
const [draft, setDraft] = React.useState(content);
|
|
209
|
+
|
|
210
|
+
React.useEffect(() => {
|
|
211
|
+
if (open) setDraft(content);
|
|
212
|
+
}, [open, content]);
|
|
213
|
+
|
|
214
|
+
const isEmail = channel === "email";
|
|
215
|
+
const channelLabel = isEmail ? "Email" : "Chat";
|
|
216
|
+
const helperText = isEmail
|
|
217
|
+
? "Use {customer} to insert the customer name and {answer} to insert the response"
|
|
218
|
+
: "Use {answer} to insert the response";
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
222
|
+
<DialogContent className="max-w-lg">
|
|
223
|
+
<DialogHeader>
|
|
224
|
+
<DialogTitle>Edit {channelLabel} response template</DialogTitle>
|
|
225
|
+
</DialogHeader>
|
|
226
|
+
|
|
227
|
+
<div className="flex flex-col gap-1.5 py-2">
|
|
228
|
+
<Label htmlFor="template-content">
|
|
229
|
+
{channelLabel} template content{" "}
|
|
230
|
+
<span className="text-destructive">*</span>
|
|
231
|
+
</Label>
|
|
232
|
+
<Textarea
|
|
233
|
+
id="template-content"
|
|
234
|
+
rows={6}
|
|
235
|
+
placeholder={`Enter content for ${channelLabel} template`}
|
|
236
|
+
value={draft}
|
|
237
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
238
|
+
/>
|
|
239
|
+
<p className="text-caption text-muted-foreground">{helperText}</p>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<DialogFooter>
|
|
243
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
244
|
+
Cancel
|
|
245
|
+
</Button>
|
|
246
|
+
<Button
|
|
247
|
+
onClick={() => {
|
|
248
|
+
onConfirm(draft);
|
|
249
|
+
onOpenChange(false);
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
Confirm
|
|
253
|
+
</Button>
|
|
254
|
+
</DialogFooter>
|
|
255
|
+
</DialogContent>
|
|
256
|
+
</Dialog>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// RuleOrderBadge
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
export type RuleOrderBadgeProps = {
|
|
265
|
+
order: number;
|
|
266
|
+
className?: string;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export function RuleOrderBadge({ order, className }: RuleOrderBadgeProps) {
|
|
270
|
+
return (
|
|
271
|
+
<Badge variant="secondary" className={className}>
|
|
272
|
+
#{order}
|
|
273
|
+
</Badge>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// RuleSetSection
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
export type RuleSetSectionProps = {
|
|
282
|
+
standardRules: AiBuilderRuleItem[];
|
|
283
|
+
handoffRules: AiBuilderRuleItem[];
|
|
284
|
+
onAddRule: (defaultType: "standard" | "handoff") => void;
|
|
285
|
+
onEditRule: (id: string) => void;
|
|
286
|
+
onDeleteRule: (id: string) => void;
|
|
287
|
+
onToggleRule: (id: string, enabled: boolean) => void;
|
|
288
|
+
/** Called when the user drag-reorders a rule. activeId dropped over overId. */
|
|
289
|
+
onReorderRules?: (activeId: string, overId: string) => void;
|
|
290
|
+
onExport: () => void;
|
|
291
|
+
onImport: () => void;
|
|
292
|
+
rowsPerPage?: number;
|
|
293
|
+
className?: string;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// SortableRuleRow — individual draggable row
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
function SortableRuleRow({
|
|
301
|
+
rule,
|
|
302
|
+
index,
|
|
303
|
+
onEdit,
|
|
304
|
+
onDelete,
|
|
305
|
+
onToggle,
|
|
306
|
+
}: {
|
|
307
|
+
rule: AiBuilderRuleItem;
|
|
308
|
+
index: number;
|
|
309
|
+
onEdit: (id: string) => void;
|
|
310
|
+
onDelete: (id: string) => void;
|
|
311
|
+
onToggle: (id: string, enabled: boolean) => void;
|
|
312
|
+
}) {
|
|
313
|
+
const {
|
|
314
|
+
attributes,
|
|
315
|
+
listeners,
|
|
316
|
+
setNodeRef,
|
|
317
|
+
transform,
|
|
318
|
+
transition,
|
|
319
|
+
isDragging,
|
|
320
|
+
} = useSortable({ id: rule.id });
|
|
321
|
+
|
|
322
|
+
const style = {
|
|
323
|
+
transform: CSS.Transform.toString(transform),
|
|
324
|
+
transition,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<TableRow
|
|
329
|
+
ref={setNodeRef}
|
|
330
|
+
style={style}
|
|
331
|
+
className={cn(isDragging && "opacity-50 bg-muted/40")}
|
|
332
|
+
>
|
|
333
|
+
{/* Order: drag handle + badge */}
|
|
334
|
+
<TableCell>
|
|
335
|
+
<div className="flex items-center gap-2">
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
className="cursor-grab touch-none text-muted-foreground hover:text-foreground active:cursor-grabbing"
|
|
339
|
+
aria-label={`Drag to reorder rule ${index + 1}`}
|
|
340
|
+
{...attributes}
|
|
341
|
+
{...listeners}
|
|
342
|
+
>
|
|
343
|
+
<GripVertical className="h-4 w-4" />
|
|
344
|
+
</button>
|
|
345
|
+
<RuleOrderBadge order={index + 1} />
|
|
346
|
+
</div>
|
|
347
|
+
</TableCell>
|
|
348
|
+
|
|
349
|
+
{/* Rule text */}
|
|
350
|
+
<TableCell className="text-body-small overflow-hidden">
|
|
351
|
+
<div className="break-words whitespace-normal">{rule.text}</div>
|
|
352
|
+
</TableCell>
|
|
353
|
+
|
|
354
|
+
{/* Actions */}
|
|
355
|
+
<TableCell>
|
|
356
|
+
<div className="flex items-center justify-end gap-1">
|
|
357
|
+
<Switch
|
|
358
|
+
checked={rule.isEnabled}
|
|
359
|
+
onCheckedChange={(checked) => onToggle(rule.id, checked)}
|
|
360
|
+
aria-label={`Toggle rule ${index + 1}`}
|
|
361
|
+
/>
|
|
362
|
+
<Button
|
|
363
|
+
variant="ghost"
|
|
364
|
+
size="icon"
|
|
365
|
+
className="h-8 w-8"
|
|
366
|
+
onClick={() => onEdit(rule.id)}
|
|
367
|
+
aria-label={`Edit rule ${index + 1}`}
|
|
368
|
+
>
|
|
369
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
370
|
+
</Button>
|
|
371
|
+
<Button
|
|
372
|
+
variant="ghost"
|
|
373
|
+
size="icon"
|
|
374
|
+
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
375
|
+
onClick={() => onDelete(rule.id)}
|
|
376
|
+
aria-label={`Delete rule ${index + 1}`}
|
|
377
|
+
>
|
|
378
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
379
|
+
</Button>
|
|
380
|
+
</div>
|
|
381
|
+
</TableCell>
|
|
382
|
+
</TableRow>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// RuleTable — sortable list with DndContext
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
function RuleTable({
|
|
391
|
+
rules,
|
|
392
|
+
onEdit,
|
|
393
|
+
onDelete,
|
|
394
|
+
onToggle,
|
|
395
|
+
onReorder,
|
|
396
|
+
}: {
|
|
397
|
+
rules: AiBuilderRuleItem[];
|
|
398
|
+
onEdit: (id: string) => void;
|
|
399
|
+
onDelete: (id: string) => void;
|
|
400
|
+
onToggle: (id: string, enabled: boolean) => void;
|
|
401
|
+
onReorder?: (activeId: string, overId: string) => void;
|
|
402
|
+
}) {
|
|
403
|
+
const sensors = useSensors(
|
|
404
|
+
useSensor(PointerSensor),
|
|
405
|
+
useSensor(KeyboardSensor, {
|
|
406
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
function handleDragEnd(event: DragEndEvent) {
|
|
411
|
+
const { active, over } = event;
|
|
412
|
+
if (over && active.id !== over.id && onReorder) {
|
|
413
|
+
onReorder(String(active.id), String(over.id));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (rules.length === 0) {
|
|
418
|
+
return (
|
|
419
|
+
<div className="flex h-24 items-center justify-center text-body-small text-muted-foreground">
|
|
420
|
+
No rules added yet.
|
|
421
|
+
</div>
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<DndContext
|
|
427
|
+
sensors={sensors}
|
|
428
|
+
collisionDetection={closestCenter}
|
|
429
|
+
onDragEnd={handleDragEnd}
|
|
430
|
+
>
|
|
431
|
+
<SortableContext
|
|
432
|
+
items={rules.map((r) => r.id)}
|
|
433
|
+
strategy={verticalListSortingStrategy}
|
|
434
|
+
>
|
|
435
|
+
<Table className="table-fixed">
|
|
436
|
+
<TableHeader>
|
|
437
|
+
<TableRow>
|
|
438
|
+
<TableHead className="w-28">
|
|
439
|
+
<span className="flex items-center gap-1.5">
|
|
440
|
+
Order
|
|
441
|
+
<TooltipProvider>
|
|
442
|
+
<Tooltip>
|
|
443
|
+
<TooltipTrigger asChild>
|
|
444
|
+
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-default" />
|
|
445
|
+
</TooltipTrigger>
|
|
446
|
+
<TooltipContent>
|
|
447
|
+
Drag rows to reorder rules
|
|
448
|
+
</TooltipContent>
|
|
449
|
+
</Tooltip>
|
|
450
|
+
</TooltipProvider>
|
|
451
|
+
</span>
|
|
452
|
+
</TableHead>
|
|
453
|
+
<TableHead>Rule Set</TableHead>
|
|
454
|
+
<TableHead className="w-36 text-right">Actions</TableHead>
|
|
455
|
+
</TableRow>
|
|
456
|
+
</TableHeader>
|
|
457
|
+
<TableBody>
|
|
458
|
+
{rules.map((rule, index) => (
|
|
459
|
+
<SortableRuleRow
|
|
460
|
+
key={rule.id}
|
|
461
|
+
rule={rule}
|
|
462
|
+
index={index}
|
|
463
|
+
onEdit={onEdit}
|
|
464
|
+
onDelete={onDelete}
|
|
465
|
+
onToggle={onToggle}
|
|
466
|
+
/>
|
|
467
|
+
))}
|
|
468
|
+
</TableBody>
|
|
469
|
+
</Table>
|
|
470
|
+
</SortableContext>
|
|
471
|
+
</DndContext>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function RuleTabContent({
|
|
476
|
+
rules,
|
|
477
|
+
description,
|
|
478
|
+
onEditRule,
|
|
479
|
+
onDeleteRule,
|
|
480
|
+
onToggleRule,
|
|
481
|
+
onReorderRules,
|
|
482
|
+
rowsPerPage,
|
|
483
|
+
}: {
|
|
484
|
+
rules: AiBuilderRuleItem[];
|
|
485
|
+
description: string;
|
|
486
|
+
onEditRule: (id: string) => void;
|
|
487
|
+
onDeleteRule: (id: string) => void;
|
|
488
|
+
onToggleRule: (id: string, enabled: boolean) => void;
|
|
489
|
+
onReorderRules?: (activeId: string, overId: string) => void;
|
|
490
|
+
rowsPerPage: number;
|
|
491
|
+
}) {
|
|
492
|
+
return (
|
|
493
|
+
<div className="mt-4 flex flex-col gap-3">
|
|
494
|
+
<div className="flex items-start gap-2 rounded-none border border-blue-200 bg-blue-50 p-3">
|
|
495
|
+
<Info className="mt-0.5 h-4 w-4 shrink-0 text-blue-600" />
|
|
496
|
+
<p className="text-caption text-blue-700">{description}</p>
|
|
497
|
+
</div>
|
|
498
|
+
<RuleTable
|
|
499
|
+
rules={rules}
|
|
500
|
+
onEdit={onEditRule}
|
|
501
|
+
onDelete={onDeleteRule}
|
|
502
|
+
onToggle={onToggleRule}
|
|
503
|
+
onReorder={onReorderRules}
|
|
504
|
+
/>
|
|
505
|
+
<div className="flex items-center justify-end gap-2 pt-2 text-body-small text-muted-foreground">
|
|
506
|
+
<span>Rows per page:</span>
|
|
507
|
+
<Select defaultValue={String(rowsPerPage)}>
|
|
508
|
+
<SelectTrigger className="h-8 w-20">
|
|
509
|
+
<SelectValue />
|
|
510
|
+
</SelectTrigger>
|
|
511
|
+
<SelectContent>
|
|
512
|
+
<SelectItem value="5">5</SelectItem>
|
|
513
|
+
<SelectItem value="10">10</SelectItem>
|
|
514
|
+
<SelectItem value="20">20</SelectItem>
|
|
515
|
+
<SelectItem value="50">50</SelectItem>
|
|
516
|
+
</SelectContent>
|
|
517
|
+
</Select>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function RuleSetSection({
|
|
524
|
+
standardRules,
|
|
525
|
+
handoffRules,
|
|
526
|
+
onAddRule,
|
|
527
|
+
onEditRule,
|
|
528
|
+
onDeleteRule,
|
|
529
|
+
onToggleRule,
|
|
530
|
+
onReorderRules,
|
|
531
|
+
onExport,
|
|
532
|
+
onImport,
|
|
533
|
+
rowsPerPage = 10,
|
|
534
|
+
className,
|
|
535
|
+
}: RuleSetSectionProps) {
|
|
536
|
+
const [activeTab, setActiveTab] = React.useState<"standard" | "handoff">(
|
|
537
|
+
"standard"
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
return (
|
|
541
|
+
<div className={cn("flex flex-col gap-4", className)}>
|
|
542
|
+
{/* Toolbar */}
|
|
543
|
+
<div className="flex items-start justify-between gap-4">
|
|
544
|
+
<SectionHeader
|
|
545
|
+
title="Rule Set"
|
|
546
|
+
description="You can adjust the behavior and response method of the Agent through rules."
|
|
547
|
+
/>
|
|
548
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
549
|
+
<Button variant="outline" size="sm" onClick={onExport}>
|
|
550
|
+
<Download className="mr-1.5 h-3.5 w-3.5" />
|
|
551
|
+
Export
|
|
552
|
+
</Button>
|
|
553
|
+
<Button variant="outline" size="sm" onClick={onImport}>
|
|
554
|
+
<Upload className="mr-1.5 h-3.5 w-3.5" />
|
|
555
|
+
Import
|
|
556
|
+
</Button>
|
|
557
|
+
<Button size="sm" onClick={() => onAddRule(activeTab)}>
|
|
558
|
+
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
|
559
|
+
Add rule
|
|
560
|
+
</Button>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
{/* Tabs + content */}
|
|
565
|
+
<Tabs
|
|
566
|
+
defaultValue="standard"
|
|
567
|
+
onValueChange={(v) => setActiveTab(v as "standard" | "handoff")}
|
|
568
|
+
>
|
|
569
|
+
<TabsList className="w-full">
|
|
570
|
+
<TabsTrigger value="standard" className="flex-1">
|
|
571
|
+
Standard Rules
|
|
572
|
+
</TabsTrigger>
|
|
573
|
+
<TabsTrigger value="handoff" className="flex-1">
|
|
574
|
+
Hand-off Rules
|
|
575
|
+
</TabsTrigger>
|
|
576
|
+
</TabsList>
|
|
577
|
+
|
|
578
|
+
<TabsContent value="standard">
|
|
579
|
+
<RuleTabContent
|
|
580
|
+
rules={standardRules}
|
|
581
|
+
description="Standard rules guide AI behavior during response generation. These rules help the AI understand how to respond appropriately."
|
|
582
|
+
onEditRule={onEditRule}
|
|
583
|
+
onDeleteRule={onDeleteRule}
|
|
584
|
+
onToggleRule={onToggleRule}
|
|
585
|
+
onReorderRules={onReorderRules}
|
|
586
|
+
rowsPerPage={rowsPerPage}
|
|
587
|
+
/>
|
|
588
|
+
</TabsContent>
|
|
589
|
+
|
|
590
|
+
<TabsContent value="handoff">
|
|
591
|
+
<RuleTabContent
|
|
592
|
+
rules={handoffRules}
|
|
593
|
+
description="Hand-off rules define when the AI should transfer a conversation to a human agent."
|
|
594
|
+
onEditRule={onEditRule}
|
|
595
|
+
onDeleteRule={onDeleteRule}
|
|
596
|
+
onToggleRule={onToggleRule}
|
|
597
|
+
onReorderRules={onReorderRules}
|
|
598
|
+
rowsPerPage={rowsPerPage}
|
|
599
|
+
/>
|
|
600
|
+
</TabsContent>
|
|
601
|
+
</Tabs>
|
|
602
|
+
</div>
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
// AddEditRuleModal
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
|
|
610
|
+
export type AddEditRuleModalProps = {
|
|
611
|
+
open: boolean;
|
|
612
|
+
onOpenChange: (open: boolean) => void;
|
|
613
|
+
/** Pass a rule to edit; omit or pass null/undefined for "Add" mode */
|
|
614
|
+
rule?: AiBuilderRuleItem | null;
|
|
615
|
+
/** Pre-select the rule type in Add mode (defaults to "standard") */
|
|
616
|
+
defaultType?: "standard" | "handoff";
|
|
617
|
+
onConfirm: (text: string, type: "standard" | "handoff") => void;
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export function AddEditRuleModal({
|
|
621
|
+
open,
|
|
622
|
+
onOpenChange,
|
|
623
|
+
rule,
|
|
624
|
+
defaultType = "standard",
|
|
625
|
+
onConfirm,
|
|
626
|
+
}: AddEditRuleModalProps) {
|
|
627
|
+
// Map between internal type key and display label (pattern: value === label)
|
|
628
|
+
const RULE_TYPE_LABELS: Record<"standard" | "handoff", string> = {
|
|
629
|
+
standard: "Standard Rule",
|
|
630
|
+
handoff: "Hand-off Rule",
|
|
631
|
+
};
|
|
632
|
+
const LABEL_TO_TYPE: Record<string, "standard" | "handoff"> = {
|
|
633
|
+
"Standard Rule": "standard",
|
|
634
|
+
"Hand-off Rule": "handoff",
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const isEdit = !!rule;
|
|
638
|
+
const [draft, setDraft] = React.useState(rule?.text ?? "");
|
|
639
|
+
const [ruleType, setRuleType] = React.useState<"standard" | "handoff">(
|
|
640
|
+
defaultType
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
React.useEffect(() => {
|
|
644
|
+
if (open) {
|
|
645
|
+
setDraft(rule?.text ?? "");
|
|
646
|
+
setRuleType(defaultType);
|
|
647
|
+
}
|
|
648
|
+
}, [open, rule, defaultType]);
|
|
649
|
+
|
|
650
|
+
return (
|
|
651
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
652
|
+
<DialogContent className="max-w-lg">
|
|
653
|
+
<DialogHeader>
|
|
654
|
+
<DialogTitle>{isEdit ? "Edit Rule" : "Add Rule"}</DialogTitle>
|
|
655
|
+
</DialogHeader>
|
|
656
|
+
|
|
657
|
+
<div className="flex flex-col gap-3 py-2">
|
|
658
|
+
{!isEdit && (
|
|
659
|
+
<div className="flex flex-col gap-1.5">
|
|
660
|
+
<Label htmlFor="rule-type">
|
|
661
|
+
Rule Type <span className="text-destructive">*</span>
|
|
662
|
+
</Label>
|
|
663
|
+
<Select
|
|
664
|
+
value={RULE_TYPE_LABELS[ruleType]}
|
|
665
|
+
onValueChange={(v) => setRuleType(LABEL_TO_TYPE[v])}
|
|
666
|
+
>
|
|
667
|
+
<SelectTrigger id="rule-type" className="w-full">
|
|
668
|
+
<SelectValue />
|
|
669
|
+
</SelectTrigger>
|
|
670
|
+
<SelectContent>
|
|
671
|
+
<SelectItem value="Standard Rule">Standard Rule</SelectItem>
|
|
672
|
+
<SelectItem value="Hand-off Rule">Hand-off Rule</SelectItem>
|
|
673
|
+
</SelectContent>
|
|
674
|
+
</Select>
|
|
675
|
+
</div>
|
|
676
|
+
)}
|
|
677
|
+
|
|
678
|
+
<div className="flex flex-col gap-1.5">
|
|
679
|
+
<Label htmlFor="rule-draft">
|
|
680
|
+
Rule <span className="text-destructive">*</span>
|
|
681
|
+
</Label>
|
|
682
|
+
<Textarea
|
|
683
|
+
id="rule-draft"
|
|
684
|
+
rows={4}
|
|
685
|
+
placeholder="Describe the rule the agent should follow…"
|
|
686
|
+
value={draft}
|
|
687
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
688
|
+
/>
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<DialogFooter>
|
|
693
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
694
|
+
Cancel
|
|
695
|
+
</Button>
|
|
696
|
+
<Button
|
|
697
|
+
disabled={!draft.trim()}
|
|
698
|
+
onClick={() => {
|
|
699
|
+
onConfirm(draft.trim(), ruleType);
|
|
700
|
+
onOpenChange(false);
|
|
701
|
+
}}
|
|
702
|
+
>
|
|
703
|
+
{isEdit ? "Save" : "Add"}
|
|
704
|
+
</Button>
|
|
705
|
+
</DialogFooter>
|
|
706
|
+
</DialogContent>
|
|
707
|
+
</Dialog>
|
|
708
|
+
);
|
|
709
|
+
}
|