cue-console 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.
@@ -0,0 +1,220 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ } from "@/components/ui/dialog";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Input } from "@/components/ui/input";
14
+ import { ScrollArea } from "@/components/ui/scroll-area";
15
+ import { fetchAllAgents, fetchConversationList, createNewGroup } from "@/lib/actions";
16
+ import { getAgentEmoji, cn } from "@/lib/utils";
17
+ import { Check } from "lucide-react";
18
+
19
+ interface CreateGroupDialogProps {
20
+ open: boolean;
21
+ onOpenChange: (open: boolean) => void;
22
+ onCreated: (groupId: string, groupName: string) => void;
23
+ }
24
+
25
+ export function CreateGroupDialog({
26
+ open,
27
+ onOpenChange,
28
+ onCreated,
29
+ }: CreateGroupDialogProps) {
30
+ const [mode, setMode] = useState<"quick" | "select">("quick");
31
+ const [name, setName] = useState("");
32
+ const [agents, setAgents] = useState<string[]>([]);
33
+ const [selected, setSelected] = useState<Set<string>>(new Set());
34
+ const [recentAgents, setRecentAgents] = useState<string[]>([]);
35
+ const [loading, setLoading] = useState(false);
36
+
37
+ useEffect(() => {
38
+ if (open) {
39
+ fetchAllAgents().then(setAgents);
40
+ fetchConversationList()
41
+ .then((items) => {
42
+ const recent = items
43
+ .filter((i) => i.type === "agent")
44
+ .slice()
45
+ .sort((a, b) => {
46
+ const ta = a.lastTime ? new Date(a.lastTime + "Z").getTime() : 0;
47
+ const tb = b.lastTime ? new Date(b.lastTime + "Z").getTime() : 0;
48
+ return tb - ta;
49
+ })
50
+ .map((i) => i.name)
51
+ .slice(0, 5);
52
+ setRecentAgents(recent);
53
+ })
54
+ .catch(() => {
55
+ setRecentAgents([]);
56
+ });
57
+ setName("");
58
+ setSelected(new Set());
59
+ setMode("quick");
60
+ }
61
+ }, [open]);
62
+
63
+ const toggleAgent = (agent: string) => {
64
+ setSelected((prev) => {
65
+ const next = new Set(prev);
66
+ if (next.has(agent)) {
67
+ next.delete(agent);
68
+ } else {
69
+ next.add(agent);
70
+ }
71
+ return next;
72
+ });
73
+ };
74
+
75
+ const membersForMode = () => {
76
+ if (mode === "quick") return recentAgents;
77
+ return Array.from(selected);
78
+ };
79
+
80
+ const handleCreate = async () => {
81
+ const groupName = name.trim() || (mode === "quick" ? "Recent" : "New group");
82
+ const members = membersForMode();
83
+ if (members.length === 0) return;
84
+
85
+ setLoading(true);
86
+ try {
87
+ const result = await createNewGroup(groupName, members);
88
+ if (!result.success) {
89
+ alert(result.error || "Create failed");
90
+ return;
91
+ }
92
+ onCreated(result.id, result.name);
93
+ onOpenChange(false);
94
+ } finally {
95
+ setLoading(false);
96
+ }
97
+ };
98
+
99
+ return (
100
+ <Dialog open={open} onOpenChange={onOpenChange}>
101
+ <DialogContent className="sm:max-w-md glass-surface-opaque glass-noise">
102
+ <DialogHeader>
103
+ <DialogTitle>Create group chat</DialogTitle>
104
+ <DialogDescription className="sr-only">
105
+ Create a group chat and choose members to join
106
+ </DialogDescription>
107
+ </DialogHeader>
108
+
109
+ <div className="space-y-4 py-4">
110
+ <div className="flex items-center gap-2">
111
+ <button
112
+ type="button"
113
+ className={cn(
114
+ "flex-1 rounded-xl border px-3 py-2 text-sm transition",
115
+ mode === "quick"
116
+ ? "bg-primary text-primary-foreground border-primary/20"
117
+ : "bg-white/60 hover:bg-white/75"
118
+ )}
119
+ onClick={() => setMode("quick")}
120
+ disabled={loading}
121
+ >
122
+ Quick create (recent 5)
123
+ </button>
124
+ <button
125
+ type="button"
126
+ className={cn(
127
+ "flex-1 rounded-xl border px-3 py-2 text-sm transition",
128
+ mode === "select"
129
+ ? "bg-primary text-primary-foreground border-primary/20"
130
+ : "bg-white/60 hover:bg-white/75"
131
+ )}
132
+ onClick={() => setMode("select")}
133
+ disabled={loading}
134
+ >
135
+ Select members
136
+ </button>
137
+ </div>
138
+
139
+ <div>
140
+ <label className="mb-2 block text-sm font-medium">Group name</label>
141
+ <Input
142
+ placeholder="Enter group name"
143
+ value={name}
144
+ onChange={(e) => setName(e.target.value)}
145
+ className="bg-white/80"
146
+ />
147
+ </div>
148
+
149
+ {mode === "quick" ? (
150
+ <div>
151
+ <label className="mb-2 block text-sm font-medium">Members to add</label>
152
+ <div className="rounded-xl border bg-white/60 p-3">
153
+ {recentAgents.length === 0 ? (
154
+ <p className="text-sm text-muted-foreground">No recent agents</p>
155
+ ) : (
156
+ <div className="flex flex-wrap gap-2">
157
+ {recentAgents.map((a) => (
158
+ <span
159
+ key={a}
160
+ className="inline-flex items-center gap-2 rounded-full border bg-white/70 px-2.5 py-1 text-sm"
161
+ title={a}
162
+ >
163
+ <span className="text-base">{getAgentEmoji(a)}</span>
164
+ <span className="max-w-40 truncate">{a}</span>
165
+ </span>
166
+ ))}
167
+ </div>
168
+ )}
169
+ </div>
170
+ </div>
171
+ ) : (
172
+ <div>
173
+ <label className="mb-2 block text-sm font-medium">
174
+ Select members ({selected.size} selected)
175
+ </label>
176
+ <ScrollArea className="h-60 rounded-md border bg-white/55 p-2">
177
+ {agents.length === 0 ? (
178
+ <p className="py-8 text-center text-sm text-muted-foreground">
179
+ No agents available
180
+ </p>
181
+ ) : (
182
+ <div className="space-y-1">
183
+ {agents.map((agent) => (
184
+ <button
185
+ key={agent}
186
+ className={cn(
187
+ "flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors",
188
+ selected.has(agent)
189
+ ? "bg-primary text-primary-foreground"
190
+ : "hover:bg-accent"
191
+ )}
192
+ onClick={() => toggleAgent(agent)}
193
+ >
194
+ <span className="text-xl">{getAgentEmoji(agent)}</span>
195
+ <span className="flex-1 font-medium">{agent}</span>
196
+ {selected.has(agent) && <Check className="h-4 w-4" />}
197
+ </button>
198
+ ))}
199
+ </div>
200
+ )}
201
+ </ScrollArea>
202
+ </div>
203
+ )}
204
+ </div>
205
+
206
+ <DialogFooter>
207
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
208
+ Cancel
209
+ </Button>
210
+ <Button
211
+ onClick={handleCreate}
212
+ disabled={membersForMode().length === 0 || loading}
213
+ >
214
+ {loading ? "Creating..." : "Create"}
215
+ </Button>
216
+ </DialogFooter>
217
+ </DialogContent>
218
+ </Dialog>
219
+ );
220
+ }
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { type ReactNode, useMemo } from "react";
4
+ import ReactMarkdown, { type Components } from "react-markdown";
5
+ import rehypeHighlight from "rehype-highlight";
6
+ import remarkGfm from "remark-gfm";
7
+
8
+ export function MarkdownRenderer({ children }: { children: string }) {
9
+ const components = useMemo<Partial<Components>>(
10
+ () => ({
11
+ p: ({ children }: { children?: ReactNode }) => <p>{children}</p>,
12
+ a: ({ children, href }: { children?: ReactNode; href?: string }) => (
13
+ <a
14
+ href={href}
15
+ target="_blank"
16
+ rel="noreferrer"
17
+ className="underline underline-offset-2 break-all"
18
+ >
19
+ {children}
20
+ </a>
21
+ ),
22
+ code: ({ children }: { children?: ReactNode }) => (
23
+ <code className="rounded bg-muted/40 px-1 py-0.5 text-[0.9em] wrap-anywhere">
24
+ {children}
25
+ </code>
26
+ ),
27
+ pre: ({ children }: { children?: ReactNode }) => (
28
+ <pre className="mt-2 max-w-full overflow-auto rounded-lg border bg-muted/30 p-2 text-xs text-muted-foreground">
29
+ {children}
30
+ </pre>
31
+ ),
32
+ img: (props) => {
33
+ const safeSrc = typeof props.src === "string" ? props.src : undefined;
34
+ const safeAlt = typeof props.alt === "string" ? props.alt : "";
35
+ return <img {...props} src={safeSrc} alt={safeAlt} className="max-w-full h-auto" />;
36
+ },
37
+ table: ({ children }: { children?: ReactNode }) => (
38
+ <div className="mt-2 max-w-full overflow-auto">
39
+ <table className="w-full text-left text-sm">{children}</table>
40
+ </div>
41
+ ),
42
+ }),
43
+ []
44
+ );
45
+
46
+ return (
47
+ <div className="md-flow">
48
+ <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight]} components={components}>
49
+ {children || ""}
50
+ </ReactMarkdown>
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,275 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ export function PayloadCard({
9
+ raw,
10
+ disabled,
11
+ onPasteChoice,
12
+ selectedLines,
13
+ }: {
14
+ raw?: string | null;
15
+ disabled?: boolean;
16
+ onPasteChoice?: (text: string, mode?: "replace" | "append") => void;
17
+ selectedLines?: Set<string>;
18
+ }) {
19
+ type ParsedChoice = { id?: string; label?: string } | string;
20
+ type ParsedField = { id?: string; label?: string; kind?: string } | string;
21
+ type ParsedViewModel =
22
+ | { kind: "raw"; raw: string }
23
+ | { kind: "unknown"; pretty: string }
24
+ | { kind: "choice"; allowMultiple: boolean; options: ParsedChoice[] }
25
+ | { kind: "confirm"; text: string; confirmLabel: string; cancelLabel: string }
26
+ | { kind: "form"; fields: ParsedField[] };
27
+
28
+ const vm = useMemo<ParsedViewModel | null>(() => {
29
+ if (!raw) return null;
30
+ try {
31
+ const parsed = JSON.parse(raw) as unknown;
32
+ if (!parsed || typeof parsed !== "object") {
33
+ return { kind: "raw", raw: String(raw) };
34
+ }
35
+
36
+ const obj = parsed as Record<string, unknown>;
37
+ const type = typeof obj.type === "string" ? obj.type : "unknown";
38
+
39
+ if (type === "choice") {
40
+ return {
41
+ kind: "choice",
42
+ allowMultiple: Boolean(obj.allow_multiple),
43
+ options: Array.isArray(obj.options) ? (obj.options as ParsedChoice[]) : [],
44
+ };
45
+ }
46
+
47
+ if (type === "confirm") {
48
+ return {
49
+ kind: "confirm",
50
+ text: typeof obj.text === "string" ? obj.text : "",
51
+ confirmLabel:
52
+ typeof obj.confirm_label === "string" ? obj.confirm_label : "Confirm",
53
+ cancelLabel:
54
+ typeof obj.cancel_label === "string" ? obj.cancel_label : "Cancel",
55
+ };
56
+ }
57
+
58
+ if (type === "form") {
59
+ return {
60
+ kind: "form",
61
+ fields: Array.isArray(obj.fields) ? (obj.fields as ParsedField[]) : [],
62
+ };
63
+ }
64
+
65
+ return { kind: "unknown", pretty: JSON.stringify(parsed, null, 2) };
66
+ } catch {
67
+ return { kind: "raw", raw };
68
+ }
69
+ }, [raw]);
70
+
71
+ if (!vm) return null;
72
+
73
+ if (vm.kind === "raw") {
74
+ return (
75
+ <pre className="mt-2 max-w-full overflow-auto rounded-lg border bg-muted/30 p-2 text-xs text-muted-foreground">
76
+ {vm.raw}
77
+ </pre>
78
+ );
79
+ }
80
+
81
+ if (vm.kind === "unknown") {
82
+ return (
83
+ <pre className="mt-2 max-w-full overflow-auto rounded-lg border bg-muted/30 p-2 text-xs text-muted-foreground">
84
+ {vm.pretty}
85
+ </pre>
86
+ );
87
+ }
88
+
89
+ if (vm.kind === "choice") {
90
+ const selected = selectedLines ?? new Set<string>();
91
+ return (
92
+ <div className="mt-2 rounded-xl border bg-linear-to-b from-background to-muted/20 p-2.5 text-xs shadow-sm">
93
+ <div className="mb-2 flex items-center justify-between gap-2">
94
+ <div className="flex items-center gap-2">
95
+ <Badge variant="secondary" className="text-[11px]">
96
+ Choice
97
+ </Badge>
98
+ {vm.allowMultiple && (
99
+ <span className="text-[11px] text-muted-foreground">Multiple allowed</span>
100
+ )}
101
+ </div>
102
+ <span className="text-[11px] text-muted-foreground">Click a button to fill the input</span>
103
+ </div>
104
+ <div className="grid grid-cols-1 gap-2">
105
+ {vm.options.length > 0 ? (
106
+ vm.options.map((opt, idx) => {
107
+ if (opt && typeof opt === "object") {
108
+ const o = opt as Record<string, unknown>;
109
+ const id = typeof o.id === "string" ? o.id : "";
110
+ const label = typeof o.label === "string" ? o.label : "";
111
+ const text = id && label ? `${id}: ${label}` : id || label || "<empty>";
112
+ const pasteText = id || label || "";
113
+ const cleaned = pasteText.trim();
114
+ const isSelected = vm.allowMultiple && !!cleaned && selected.has(cleaned);
115
+
116
+ return (
117
+ <Button
118
+ key={`${id || "opt"}-${idx}`}
119
+ type="button"
120
+ variant={isSelected ? "secondary" : "outline"}
121
+ size="sm"
122
+ className={cn(
123
+ "h-auto min-h-9 justify-start gap-2 px-3 py-2 text-left text-xs",
124
+ "rounded-xl",
125
+ isSelected && "cursor-not-allowed opacity-80"
126
+ )}
127
+ disabled={disabled || !onPasteChoice || !pasteText || isSelected}
128
+ onClick={() =>
129
+ onPasteChoice?.(pasteText, vm.allowMultiple ? "append" : "replace")
130
+ }
131
+ title={pasteText ? `Click to paste: ${pasteText}` : undefined}
132
+ >
133
+ {id && (
134
+ <span className="inline-flex h-5 items-center rounded-md bg-muted px-1.5 font-mono text-[11px] text-muted-foreground">
135
+ {id}
136
+ </span>
137
+ )}
138
+ <span className="min-w-0 flex-1 truncate">{id && label ? label : text}</span>
139
+ </Button>
140
+ );
141
+ }
142
+
143
+ const asText = String(opt);
144
+ return (
145
+ <Button
146
+ key={`opt-${idx}`}
147
+ type="button"
148
+ variant="outline"
149
+ size="sm"
150
+ className="h-auto min-h-9 justify-start rounded-xl px-3 py-2 text-left text-xs"
151
+ disabled={disabled || !onPasteChoice}
152
+ onClick={() =>
153
+ onPasteChoice?.(asText, vm.allowMultiple ? "append" : "replace")
154
+ }
155
+ title={`Click to paste: ${asText}`}
156
+ >
157
+ {asText}
158
+ </Button>
159
+ );
160
+ })
161
+ ) : (
162
+ <div className="text-muted-foreground">No options</div>
163
+ )}
164
+ </div>
165
+ </div>
166
+ );
167
+ }
168
+
169
+ if (vm.kind === "confirm") {
170
+ return (
171
+ <div className="mt-2 rounded-xl border bg-linear-to-b from-background to-muted/20 p-2.5 text-xs shadow-sm">
172
+ <div className="mb-2 flex items-center justify-between gap-2">
173
+ <Badge variant="secondary" className="text-[11px]">
174
+ Confirm
175
+ </Badge>
176
+ <span className="text-[11px] text-muted-foreground">Click a button to fill the input</span>
177
+ </div>
178
+ {vm.text && <div className="mb-2 whitespace-pre-wrap leading-normal">{vm.text}</div>}
179
+ <div className="flex flex-col gap-2">
180
+ <Button
181
+ type="button"
182
+ variant="default"
183
+ size="sm"
184
+ className="h-9 w-full rounded-xl px-3 text-xs"
185
+ disabled={disabled || !onPasteChoice}
186
+ onClick={() => onPasteChoice?.(vm.confirmLabel)}
187
+ title={`Click to paste: ${vm.confirmLabel}`}
188
+ >
189
+ {vm.confirmLabel}
190
+ </Button>
191
+ <Button
192
+ type="button"
193
+ variant="outline"
194
+ size="sm"
195
+ className="h-9 w-full rounded-xl px-3 text-xs"
196
+ disabled={disabled || !onPasteChoice}
197
+ onClick={() => onPasteChoice?.(vm.cancelLabel)}
198
+ title={`Click to paste: ${vm.cancelLabel}`}
199
+ >
200
+ {vm.cancelLabel}
201
+ </Button>
202
+ </div>
203
+ </div>
204
+ );
205
+ }
206
+
207
+ // vm.kind === "form"
208
+ return (
209
+ <div className="mt-2 rounded-xl border bg-linear-to-b from-background to-muted/20 p-2.5 text-xs shadow-sm">
210
+ <div className="mb-2 flex items-center justify-between gap-2">
211
+ <Badge variant="secondary" className="text-[11px]">
212
+ Form
213
+ </Badge>
214
+ <span className="text-[11px] text-muted-foreground">Fill by field (click to insert)</span>
215
+ </div>
216
+ <div className="space-y-2">
217
+ {vm.fields.length > 0 ? (
218
+ vm.fields.map((f, idx) => {
219
+ if (f && typeof f === "object") {
220
+ const fo = f as Record<string, unknown>;
221
+ const id = typeof fo.id === "string" ? fo.id : "";
222
+ const label = typeof fo.label === "string" ? fo.label : "";
223
+ const kind = typeof fo.kind === "string" ? fo.kind : "";
224
+ const name = label || id || "Field";
225
+ const pasteText = id || label || "";
226
+
227
+ return (
228
+ <button
229
+ key={`${id || "field"}-${idx}`}
230
+ type="button"
231
+ className={cn(
232
+ "w-full rounded-xl border bg-background/60 px-3 py-2 text-left",
233
+ "hover:bg-background/80 hover:shadow-sm transition",
234
+ "disabled:opacity-60"
235
+ )}
236
+ disabled={disabled || !onPasteChoice || !pasteText}
237
+ onClick={() => onPasteChoice?.(pasteText)}
238
+ title={pasteText ? `Click to paste: ${pasteText}` : undefined}
239
+ >
240
+ <div className="flex items-center gap-2">
241
+ <span className="min-w-0 flex-1 truncate text-[13px]">{name}</span>
242
+ {kind && (
243
+ <span className="shrink-0 rounded-md bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground">
244
+ {kind}
245
+ </span>
246
+ )}
247
+ </div>
248
+ {id && label && (
249
+ <div className="mt-1 text-[11px] text-muted-foreground">{id}</div>
250
+ )}
251
+ </button>
252
+ );
253
+ }
254
+
255
+ const asText = String(f);
256
+ return (
257
+ <button
258
+ key={`field-${idx}`}
259
+ type="button"
260
+ className="w-full rounded-xl border bg-background/60 px-3 py-2 text-left text-[13px] hover:bg-background/80 hover:shadow-sm transition disabled:opacity-60"
261
+ disabled={disabled || !onPasteChoice}
262
+ onClick={() => onPasteChoice?.(asText)}
263
+ title={`Click to paste: ${asText}`}
264
+ >
265
+ {asText}
266
+ </button>
267
+ );
268
+ })
269
+ ) : (
270
+ <div className="text-muted-foreground">No fields</div>
271
+ )}
272
+ </div>
273
+ </div>
274
+ );
275
+ }
@@ -0,0 +1,53 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn(
16
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function AvatarImage({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28
+ return (
29
+ <AvatarPrimitive.Image
30
+ data-slot="avatar-image"
31
+ className={cn("aspect-square size-full", className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AvatarFallback({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41
+ return (
42
+ <AvatarPrimitive.Fallback
43
+ data-slot="avatar-fallback"
44
+ className={cn(
45
+ "bg-muted flex size-full items-center justify-center rounded-full",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback }
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden backdrop-blur-md",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-white/20 bg-primary text-primary-foreground shadow-sm ring-1 ring-white/15 [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-white/35 bg-white/55 text-foreground shadow-sm ring-1 ring-white/25 dark:border-white/12 dark:bg-white/8 dark:text-foreground [a&]:hover:bg-white/70 dark:[a&]:hover:bg-white/12",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "border-white/40 bg-white/35 text-foreground ring-1 ring-white/20 dark:border-white/12 dark:bg-white/6 [a&]:hover:bg-white/50 dark:[a&]:hover:bg-white/10",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Badge, badgeVariants }