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.
- package/LICENSE +201 -0
- package/README.md +33 -0
- package/bin/cue-console.js +82 -0
- package/components.json +22 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +11 -0
- package/package.json +61 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +304 -0
- package/src/app/layout.tsx +36 -0
- package/src/app/page.tsx +109 -0
- package/src/components/chat-composer.tsx +493 -0
- package/src/components/chat-view.tsx +1463 -0
- package/src/components/conversation-list.tsx +525 -0
- package/src/components/create-group-dialog.tsx +220 -0
- package/src/components/markdown-renderer.tsx +53 -0
- package/src/components/payload-card.tsx +275 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +63 -0
- package/src/components/ui/collapsible.tsx +46 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/scroll-area.tsx +59 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/lib/actions.ts +300 -0
- package/src/lib/db.ts +581 -0
- package/src/lib/types.ts +89 -0
- package/src/lib/utils.ts +135 -0
- package/tsconfig.json +34 -0
|
@@ -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 }
|