agent-state-machine 2.2.1 → 2.2.2
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/bin/cli.js +30 -2
- package/lib/runtime/agent.js +6 -2
- package/lib/runtime/interaction.js +2 -1
- package/lib/runtime/prompt.js +37 -1
- package/lib/runtime/runtime.js +67 -5
- package/package.json +1 -1
- package/templates/project-builder/agents/code-fixer.md +50 -0
- package/templates/project-builder/agents/code-writer.md +3 -0
- package/templates/project-builder/agents/sanity-checker.md +6 -0
- package/templates/project-builder/agents/test-planner.md +3 -1
- package/templates/project-builder/config.js +4 -4
- package/templates/project-builder/scripts/workflow-helpers.js +104 -2
- package/templates/project-builder/workflow.js +151 -14
- package/templates/starter/config.js +1 -1
- package/vercel-server/api/submit/[token].js +0 -11
- package/vercel-server/local-server.js +0 -19
- package/vercel-server/public/remote/assets/index-BsJsLDKc.css +1 -0
- package/vercel-server/public/remote/assets/index-CmtT6ADh.js +168 -0
- package/vercel-server/public/remote/index.html +2 -2
- package/vercel-server/ui/src/App.jsx +69 -19
- package/vercel-server/ui/src/components/ChoiceInteraction.jsx +69 -18
- package/vercel-server/ui/src/components/ConfirmInteraction.jsx +7 -7
- package/vercel-server/ui/src/components/ContentCard.jsx +600 -104
- package/vercel-server/ui/src/components/EventsLog.jsx +20 -13
- package/vercel-server/ui/src/components/Footer.jsx +9 -4
- package/vercel-server/ui/src/components/Header.jsx +12 -3
- package/vercel-server/ui/src/components/SendingCard.jsx +33 -0
- package/vercel-server/ui/src/components/TextInteraction.jsx +8 -8
- package/vercel-server/ui/src/index.css +82 -10
- package/vercel-server/public/remote/assets/index-CbgeVnKw.js +0 -148
- package/vercel-server/public/remote/assets/index-DHL_iHQW.css +0 -1
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
6
6
|
<title>{{WORKFLOW_NAME}}</title>
|
|
7
|
-
<script type="module" crossorigin src="/remote/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/remote/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/remote/assets/index-CmtT6ADh.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/remote/assets/index-BsJsLDKc.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -5,6 +5,8 @@ import EventsLog from "./components/EventsLog.jsx";
|
|
|
5
5
|
import Footer from "./components/Footer.jsx";
|
|
6
6
|
import Header from "./components/Header.jsx";
|
|
7
7
|
import InteractionForm from "./components/InteractionForm.jsx";
|
|
8
|
+
import SendingCard from "./components/SendingCard.jsx";
|
|
9
|
+
import { Search } from "lucide-react";
|
|
8
10
|
|
|
9
11
|
export default function App() {
|
|
10
12
|
const [history, setHistory] = useState([]);
|
|
@@ -15,6 +17,8 @@ export default function App() {
|
|
|
15
17
|
const [viewMode, setViewMode] = useState("present");
|
|
16
18
|
const [pendingInteraction, setPendingInteraction] = useState(null);
|
|
17
19
|
const [hasNew, setHasNew] = useState(false);
|
|
20
|
+
const [sendingState, setSendingState] = useState(null);
|
|
21
|
+
const [promptSearchRequestId, setPromptSearchRequestId] = useState(0);
|
|
18
22
|
|
|
19
23
|
useEffect(() => {
|
|
20
24
|
const savedTheme = localStorage.getItem("rf_theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
@@ -76,6 +80,32 @@ export default function App() {
|
|
|
76
80
|
prevLen.current = history.length;
|
|
77
81
|
}, [history.length, pageIndex]);
|
|
78
82
|
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!sendingState || history.length === 0) return;
|
|
85
|
+
const latest = history[history.length - 1];
|
|
86
|
+
const hasNewEvent =
|
|
87
|
+
history.length !== sendingState.historyLength ||
|
|
88
|
+
(latest?.timestamp && latest.timestamp !== sendingState.lastEventTimestamp);
|
|
89
|
+
|
|
90
|
+
if (!hasNewEvent) return;
|
|
91
|
+
|
|
92
|
+
if (latest?.event === "INTERACTION_SUBMITTED") {
|
|
93
|
+
setSendingState((prev) =>
|
|
94
|
+
prev
|
|
95
|
+
? {
|
|
96
|
+
...prev,
|
|
97
|
+
historyLength: history.length,
|
|
98
|
+
lastEventTimestamp: latest?.timestamp || prev.lastEventTimestamp,
|
|
99
|
+
}
|
|
100
|
+
: prev
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setSendingState(null);
|
|
106
|
+
setPageIndex(history.length - 1);
|
|
107
|
+
}, [history, sendingState]);
|
|
108
|
+
|
|
79
109
|
useEffect(() => {
|
|
80
110
|
if (pageIndex === history.length - 1) {
|
|
81
111
|
setHasNew(false);
|
|
@@ -110,8 +140,10 @@ export default function App() {
|
|
|
110
140
|
}, [history.length]);
|
|
111
141
|
|
|
112
142
|
const currentItem = history[pageIndex];
|
|
143
|
+
const isAgentStarted = currentItem?.event === "AGENT_STARTED";
|
|
113
144
|
const isRequestEvent = currentItem && (currentItem.event === "INTERACTION_REQUESTED" || currentItem.event === "PROMPT_REQUESTED");
|
|
114
145
|
const isRequest = pendingInteraction && isRequestEvent && currentItem.slug === pendingInteraction.slug;
|
|
146
|
+
const isSending = Boolean(sendingState);
|
|
115
147
|
|
|
116
148
|
return (
|
|
117
149
|
<div className="w-full h-[100dvh] flex flex-col relative overflow-hidden bg-bg">
|
|
@@ -144,34 +176,40 @@ export default function App() {
|
|
|
144
176
|
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
|
145
177
|
className="w-full h-full"
|
|
146
178
|
>
|
|
147
|
-
{
|
|
179
|
+
{isSending ? (
|
|
180
|
+
<SendingCard submission={sendingState} />
|
|
181
|
+
) : isRequest ? (
|
|
148
182
|
<div className="content-width h-full">
|
|
149
183
|
<InteractionForm
|
|
150
184
|
interaction={pendingInteraction}
|
|
151
185
|
onSubmit={async (slug, targetKey, response) => {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
answer: responsePreview,
|
|
159
|
-
response
|
|
160
|
-
}
|
|
161
|
-
]);
|
|
162
|
-
setPageIndex((prev) => prev + 1);
|
|
163
|
-
await fetch(submitUrl, {
|
|
164
|
-
method: "POST",
|
|
165
|
-
headers: { "Content-Type": "application/json" },
|
|
166
|
-
body: JSON.stringify({ slug, targetKey, response })
|
|
186
|
+
const lastEvent = history[history.length - 1];
|
|
187
|
+
setSendingState({
|
|
188
|
+
slug,
|
|
189
|
+
targetKey,
|
|
190
|
+
historyLength: history.length,
|
|
191
|
+
lastEventTimestamp: lastEvent?.timestamp || null,
|
|
167
192
|
});
|
|
168
|
-
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetch(submitUrl, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: { "Content-Type": "application/json" },
|
|
197
|
+
body: JSON.stringify({ slug, targetKey, response })
|
|
198
|
+
});
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
throw new Error(`Submit failed with ${res.status}`);
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
setSendingState(null);
|
|
204
|
+
} finally {
|
|
205
|
+
setTimeout(fetchData, 1000);
|
|
206
|
+
}
|
|
169
207
|
}}
|
|
170
|
-
disabled={status === "disconnected"}
|
|
208
|
+
disabled={status === "disconnected" || isSending}
|
|
171
209
|
/>
|
|
172
210
|
</div>
|
|
173
211
|
) : (
|
|
174
|
-
<ContentCard item={currentItem} />
|
|
212
|
+
<ContentCard item={currentItem} promptSearchRequestId={promptSearchRequestId} />
|
|
175
213
|
)}
|
|
176
214
|
</motion.div>
|
|
177
215
|
</AnimatePresence>
|
|
@@ -187,6 +225,18 @@ export default function App() {
|
|
|
187
225
|
hasNew={hasNew}
|
|
188
226
|
onJumpToLatest={() => setPageIndex(history.length - 1)}
|
|
189
227
|
className={viewMode === "log" ? "opacity-0 pointer-events-none" : "opacity-100"}
|
|
228
|
+
leftSlot={
|
|
229
|
+
isAgentStarted ? (
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
onClick={() => setPromptSearchRequestId((prev) => prev + 1)}
|
|
233
|
+
className="w-12 h-12 rounded-full bg-white text-black dark:bg-black dark:text-white border border-black/10 dark:border-white/10 flex items-center justify-center shadow-2xl shadow-black/20 dark:shadow-white/10 hover:scale-[1.02] transition-transform"
|
|
234
|
+
aria-label="Search prompt sections"
|
|
235
|
+
>
|
|
236
|
+
<Search className="w-5 h-5" />
|
|
237
|
+
</button>
|
|
238
|
+
) : null
|
|
239
|
+
}
|
|
190
240
|
/>
|
|
191
241
|
</div>
|
|
192
242
|
);
|
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
import { useMemo, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { Bot, Check } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
|
|
5
|
-
const { prompt, question, options = [], multiSelect, allowCustom } = interaction;
|
|
5
|
+
const { prompt, question, options = [], multiSelect, allowCustom, fullAuto, autoSelectDelay = 20, timestamp } = interaction;
|
|
6
6
|
const [selected, setSelected] = useState(multiSelect ? [] : null);
|
|
7
7
|
const [customText, setCustomText] = useState("");
|
|
8
8
|
const [showCustom, setShowCustom] = useState(false);
|
|
9
|
+
const [tick, setTick] = useState(0);
|
|
9
10
|
|
|
10
11
|
const list = useMemo(() => options || [], [options]);
|
|
11
12
|
const title = prompt || question || "Choose an option.";
|
|
12
13
|
|
|
14
|
+
// Calculate countdown based on event timestamp
|
|
15
|
+
const countdown = useMemo(() => {
|
|
16
|
+
if (!fullAuto || !timestamp) return null;
|
|
17
|
+
const eventTime = new Date(timestamp).getTime();
|
|
18
|
+
const elapsed = Math.floor((Date.now() - eventTime) / 1000);
|
|
19
|
+
return autoSelectDelay - elapsed;
|
|
20
|
+
}, [fullAuto, timestamp, autoSelectDelay, tick]);
|
|
21
|
+
|
|
22
|
+
// Tick every second to update countdown
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!fullAuto) return;
|
|
25
|
+
const timer = setInterval(() => setTick(t => t + 1), 1000);
|
|
26
|
+
return () => clearInterval(timer);
|
|
27
|
+
}, [fullAuto]);
|
|
28
|
+
|
|
13
29
|
const handleSelect = (key) => {
|
|
14
30
|
if (multiSelect) {
|
|
15
31
|
setSelected((prev) => (
|
|
@@ -44,19 +60,43 @@ export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
|
|
|
44
60
|
<div className="w-full h-full flex flex-col items-stretch overflow-hidden">
|
|
45
61
|
<div className="flex-1 overflow-y-auto custom-scroll px-6 py-12 space-y-8 flex flex-col items-center">
|
|
46
62
|
<div className="space-y-4 shrink-0">
|
|
47
|
-
<div className="w-16 h-16 rounded-3xl bg-
|
|
63
|
+
<div className="w-16 h-16 rounded-3xl bg-black text-white dark:bg-white dark:text-black flex items-center justify-center mx-auto shadow-2xl shadow-black/20 dark:shadow-white/10">
|
|
48
64
|
<Bot className="w-8 h-8" />
|
|
49
65
|
</div>
|
|
50
|
-
<h3 className="text-4xl font-extrabold tracking-tight text-fg pt-4 text-center">Choose an option.</h3>
|
|
66
|
+
<h3 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-fg pt-4 text-center">Choose an option.</h3>
|
|
51
67
|
</div>
|
|
52
68
|
|
|
53
|
-
<div className="text-xl font-medium text-fg/70 text-center max-w-2xl whitespace-pre-wrap">
|
|
69
|
+
<div className="text-lg sm:text-xl font-medium text-fg/70 text-center max-w-2xl whitespace-pre-wrap break-words">
|
|
54
70
|
{title}
|
|
55
71
|
</div>
|
|
56
72
|
|
|
73
|
+
{fullAuto && countdown !== null && (
|
|
74
|
+
<div className="text-center">
|
|
75
|
+
<div className={`inline-flex items-center gap-2 px-4 py-2 rounded-full ${
|
|
76
|
+
countdown > 0
|
|
77
|
+
? "bg-yellow-500 text-black animate-pulse"
|
|
78
|
+
: "bg-black text-white dark:bg-white dark:text-black"
|
|
79
|
+
}`}>
|
|
80
|
+
<span className="text-xl">⚡</span>
|
|
81
|
+
<span className="text-sm font-bold">
|
|
82
|
+
{countdown > 0
|
|
83
|
+
? `Agent deciding in ${countdown}s...`
|
|
84
|
+
: "Auto-selecting recommended option..."}
|
|
85
|
+
</span>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
57
90
|
<div className="w-full max-w-2xl space-y-3">
|
|
58
|
-
{list.map((opt) => {
|
|
91
|
+
{list.map((opt, index) => {
|
|
59
92
|
const isSelected = multiSelect ? selected.includes(opt.key) : selected === opt.key;
|
|
93
|
+
const labelClass = isSelected
|
|
94
|
+
? "text-white dark:text-black"
|
|
95
|
+
: "text-black dark:text-white";
|
|
96
|
+
const descriptionClass = isSelected
|
|
97
|
+
? "text-white/70 dark:text-black/70"
|
|
98
|
+
: "text-black/50 dark:text-white/50";
|
|
99
|
+
|
|
60
100
|
return (
|
|
61
101
|
<button
|
|
62
102
|
key={opt.key}
|
|
@@ -65,21 +105,32 @@ export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
|
|
|
65
105
|
type="button"
|
|
66
106
|
className={`w-full p-6 rounded-2xl border-2 transition-all text-left ${
|
|
67
107
|
isSelected
|
|
68
|
-
? "border-
|
|
69
|
-
: "border-white/10 hover:border-white/
|
|
108
|
+
? "border-black bg-black text-white dark:border-white dark:bg-white dark:text-black"
|
|
109
|
+
: "border-black/10 dark:border-white/10 hover:border-black/30 dark:hover:border-white/30 bg-black/[0.02] dark:bg-white/[0.03]"
|
|
70
110
|
}`}
|
|
71
111
|
>
|
|
72
112
|
<div className="flex items-center gap-4">
|
|
73
113
|
<div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center ${
|
|
74
114
|
isSelected
|
|
75
|
-
? "border-
|
|
76
|
-
: "border-white/
|
|
115
|
+
? "border-black bg-black text-white dark:border-white dark:bg-white dark:text-black"
|
|
116
|
+
: "border-black/30 dark:border-white/30"
|
|
77
117
|
}`}>
|
|
78
118
|
{isSelected && <Check className="w-4 h-4" />}
|
|
79
119
|
</div>
|
|
80
120
|
<div className="flex-1">
|
|
81
|
-
<div className=
|
|
82
|
-
|
|
121
|
+
<div className={`font-bold text-lg break-words flex flex-wrap items-center gap-2 ${labelClass}`}>
|
|
122
|
+
<span className="break-words">{opt.label || opt.key}</span>
|
|
123
|
+
{index === 0 && (
|
|
124
|
+
<span className={`ml-2 text-xs font-medium px-2 py-0.5 rounded-full ${
|
|
125
|
+
isSelected
|
|
126
|
+
? 'bg-white/20 dark:bg-black/20'
|
|
127
|
+
: 'bg-black/10 dark:bg-white/10'
|
|
128
|
+
}`}>
|
|
129
|
+
Recommended
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
{opt.description && <div className={`text-sm mt-1 break-words ${descriptionClass}`}>{opt.description}</div>}
|
|
83
134
|
</div>
|
|
84
135
|
</div>
|
|
85
136
|
</button>
|
|
@@ -93,12 +144,12 @@ export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
|
|
|
93
144
|
type="button"
|
|
94
145
|
className={`w-full p-6 rounded-2xl border-2 transition-all text-left ${
|
|
95
146
|
showCustom
|
|
96
|
-
? "border-
|
|
97
|
-
: "border-white/10 hover:border-white/
|
|
147
|
+
? "border-black bg-black text-white dark:border-white dark:bg-white dark:text-black"
|
|
148
|
+
: "border-black/10 dark:border-white/10 hover:border-black/30 dark:hover:border-white/30 bg-black/[0.02] dark:bg-white/[0.03]"
|
|
98
149
|
}`}
|
|
99
150
|
>
|
|
100
|
-
<div className=
|
|
101
|
-
<div className=
|
|
151
|
+
<div className={`font-bold text-lg break-words ${showCustom ? "text-white dark:text-black" : "text-black dark:text-white"}`}>Other</div>
|
|
152
|
+
<div className={`text-sm mt-1 break-words ${showCustom ? "text-white/70 dark:text-black/70" : "text-black/50 dark:text-white/50"}`}>Provide a custom response</div>
|
|
102
153
|
</button>
|
|
103
154
|
)}
|
|
104
155
|
|
|
@@ -107,13 +158,13 @@ export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
|
|
|
107
158
|
value={customText}
|
|
108
159
|
onChange={(event) => setCustomText(event.target.value)}
|
|
109
160
|
placeholder="Type your response..."
|
|
110
|
-
className="w-full h-32 p-6 rounded-2xl bg-black/[0.
|
|
161
|
+
className="w-full h-32 p-6 rounded-2xl bg-black/[0.02] dark:bg-white/[0.03] border-2 border-black/20 dark:border-white/20 focus:border-black dark:focus:border-white focus:outline-none text-lg"
|
|
111
162
|
/>
|
|
112
163
|
)}
|
|
113
164
|
</div>
|
|
114
165
|
</div>
|
|
115
166
|
|
|
116
|
-
<div className="p-4 flex justify-center bg-gradient-to-t from-bg via-bg to-transparent shrink-0 border-t border-white/
|
|
167
|
+
<div className="p-4 flex justify-center bg-gradient-to-t from-bg via-bg to-transparent shrink-0 border-t border-black/10 dark:border-white/10">
|
|
117
168
|
<button
|
|
118
169
|
onClick={handleSubmit}
|
|
119
170
|
disabled={disabled || !isValid}
|
|
@@ -13,35 +13,35 @@ export default function ConfirmInteraction({ interaction, onSubmit, disabled })
|
|
|
13
13
|
<div className="w-full h-full flex flex-col items-stretch overflow-hidden">
|
|
14
14
|
<div className="flex-1 overflow-y-auto custom-scroll px-6 py-12 space-y-8 flex flex-col items-center justify-center">
|
|
15
15
|
<div className="space-y-4">
|
|
16
|
-
<div className="w-16 h-16 rounded-3xl bg-
|
|
16
|
+
<div className="w-16 h-16 rounded-3xl bg-black text-white dark:bg-white dark:text-black flex items-center justify-center mx-auto shadow-2xl shadow-black/20 dark:shadow-white/10">
|
|
17
17
|
<Bot className="w-8 h-8" />
|
|
18
18
|
</div>
|
|
19
|
-
<h3 className="text-4xl font-extrabold tracking-tight text-fg pt-4 text-center">Confirm action.</h3>
|
|
19
|
+
<h3 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-fg pt-4 text-center">Confirm action.</h3>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
|
-
<div className="text-xl font-medium text-fg/70 text-center max-w-2xl whitespace-pre-wrap">
|
|
22
|
+
<div className="text-lg sm:text-xl font-medium text-fg/70 text-center max-w-2xl whitespace-pre-wrap break-words">
|
|
23
23
|
{prompt || question || "Please confirm."}
|
|
24
24
|
</div>
|
|
25
25
|
|
|
26
26
|
{context?.documentPath && (
|
|
27
27
|
<div className="text-sm text-fg/40 text-center">
|
|
28
|
-
Review: <code className="bg-white/10 px-2 py-1 rounded">{context.documentPath}</code>
|
|
28
|
+
Review: <code className="bg-black/10 dark:bg-white/10 px-2 py-1 rounded">{context.documentPath}</code>
|
|
29
29
|
</div>
|
|
30
30
|
)}
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
|
-
<div className="p-4 flex justify-center gap-4 bg-gradient-to-t from-bg via-bg to-transparent shrink-0 border-t border-white/
|
|
33
|
+
<div className="p-4 flex justify-center gap-4 bg-gradient-to-t from-bg via-bg to-transparent shrink-0 border-t border-black/10 dark:border-white/10">
|
|
34
34
|
<button
|
|
35
35
|
onClick={() => onSubmit({ confirmed: false, raw: cancelLabel })}
|
|
36
36
|
disabled={disabled}
|
|
37
|
-
className="px-12 py-6
|
|
37
|
+
className="px-12 py-6 border border-black/20 dark:border-white/20 text-fg rounded-full font-bold text-xl hover:bg-black/5 dark:hover:bg-white/10 transition-all disabled:opacity-30"
|
|
38
38
|
>
|
|
39
39
|
{cancelLabel}
|
|
40
40
|
</button>
|
|
41
41
|
<button
|
|
42
42
|
onClick={() => onSubmit({ confirmed: true, raw: confirmLabel })}
|
|
43
43
|
disabled={disabled}
|
|
44
|
-
className="px-12 py-6 bg-fg text-bg rounded-full font-bold text-xl hover:scale-105 active:scale-95 transition-all disabled:opacity-30 shadow-2xl"
|
|
44
|
+
className="px-12 py-6 bg-fg text-bg rounded-full font-bold text-xl hover:scale-105 active:scale-95 transition-all disabled:opacity-30 shadow-2xl shadow-black/20 dark:shadow-white/10"
|
|
45
45
|
>
|
|
46
46
|
{confirmLabel}
|
|
47
47
|
</button>
|