@chrysb/alphaclaw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/bin/alphaclaw.js +338 -0
  2. package/lib/public/icons/chevron-down.svg +9 -0
  3. package/lib/public/js/app.js +325 -0
  4. package/lib/public/js/components/badge.js +16 -0
  5. package/lib/public/js/components/channels.js +36 -0
  6. package/lib/public/js/components/credentials-modal.js +336 -0
  7. package/lib/public/js/components/device-pairings.js +72 -0
  8. package/lib/public/js/components/envars.js +354 -0
  9. package/lib/public/js/components/gateway.js +163 -0
  10. package/lib/public/js/components/google.js +223 -0
  11. package/lib/public/js/components/icons.js +23 -0
  12. package/lib/public/js/components/models.js +461 -0
  13. package/lib/public/js/components/pairings.js +74 -0
  14. package/lib/public/js/components/scope-picker.js +106 -0
  15. package/lib/public/js/components/toast.js +31 -0
  16. package/lib/public/js/components/welcome.js +541 -0
  17. package/lib/public/js/hooks/usePolling.js +29 -0
  18. package/lib/public/js/lib/api.js +196 -0
  19. package/lib/public/js/lib/model-config.js +88 -0
  20. package/lib/public/login.html +90 -0
  21. package/lib/public/setup.html +33 -0
  22. package/lib/scripts/systemctl +56 -0
  23. package/lib/server/auth-profiles.js +101 -0
  24. package/lib/server/commands.js +84 -0
  25. package/lib/server/constants.js +282 -0
  26. package/lib/server/env.js +78 -0
  27. package/lib/server/gateway.js +262 -0
  28. package/lib/server/helpers.js +192 -0
  29. package/lib/server/login-throttle.js +86 -0
  30. package/lib/server/onboarding/cron.js +51 -0
  31. package/lib/server/onboarding/github.js +49 -0
  32. package/lib/server/onboarding/index.js +127 -0
  33. package/lib/server/onboarding/openclaw.js +171 -0
  34. package/lib/server/onboarding/validation.js +107 -0
  35. package/lib/server/onboarding/workspace.js +52 -0
  36. package/lib/server/openclaw-version.js +179 -0
  37. package/lib/server/routes/auth.js +80 -0
  38. package/lib/server/routes/codex.js +204 -0
  39. package/lib/server/routes/google.js +390 -0
  40. package/lib/server/routes/models.js +68 -0
  41. package/lib/server/routes/onboarding.js +116 -0
  42. package/lib/server/routes/pages.js +21 -0
  43. package/lib/server/routes/pairings.js +134 -0
  44. package/lib/server/routes/proxy.js +29 -0
  45. package/lib/server/routes/system.js +213 -0
  46. package/lib/server.js +161 -0
  47. package/lib/setup/core-prompts/AGENTS.md +22 -0
  48. package/lib/setup/core-prompts/TOOLS.md +18 -0
  49. package/lib/setup/env.template +19 -0
  50. package/lib/setup/gitignore +12 -0
  51. package/lib/setup/hourly-git-sync.sh +86 -0
  52. package/lib/setup/skills/control-ui/SKILL.md +70 -0
  53. package/package.json +34 -0
@@ -0,0 +1,354 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useState, useEffect, useCallback } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { fetchEnvVars, saveEnvVars, restartGateway } from "../lib/api.js";
5
+ import { showToast } from "./toast.js";
6
+ const html = htm.bind(h);
7
+
8
+ const kGroupLabels = {
9
+ github: "GitHub",
10
+ channels: "Channels",
11
+ tools: "Tools",
12
+ custom: "Custom",
13
+ };
14
+
15
+ const kGroupOrder = ["github", "channels", "tools", "custom"];
16
+
17
+ const kHintByKey = {
18
+ ANTHROPIC_API_KEY: html`From <a href="https://console.anthropic.com" target="_blank" class="text-blue-400 hover:underline">console.anthropic.com</a>`,
19
+ ANTHROPIC_TOKEN: html`From <code class="text-xs bg-black/30 px-1 rounded">claude setup-token</code>`,
20
+ OPENAI_API_KEY: html`From <a href="https://platform.openai.com" target="_blank" class="text-blue-400 hover:underline">platform.openai.com</a>`,
21
+ GEMINI_API_KEY: html`From <a href="https://aistudio.google.com" target="_blank" class="text-blue-400 hover:underline">aistudio.google.com</a>`,
22
+ GITHUB_TOKEN: html`Use a <strong>classic PAT</strong> with <code class="text-xs bg-black/30 px-1 rounded">repo</code> scope from <a href="https://github.com/settings/tokens" target="_blank" class="text-blue-400 hover:underline">github.com/settings/tokens</a>. Fine-grained tokens can fail unless they include equivalent repo write permissions.`,
23
+ GITHUB_WORKSPACE_REPO: html`Use <code class="text-xs bg-black/30 px-1 rounded">owner/repo</code> or <code class="text-xs bg-black/30 px-1 rounded">https://github.com/owner/repo</code>`,
24
+ TELEGRAM_BOT_TOKEN: html`From <a href="https://t.me/BotFather" target="_blank" class="text-blue-400 hover:underline">@BotFather</a> · <a href="https://docs.openclaw.ai/channels/telegram" target="_blank" class="text-blue-400 hover:underline">full guide</a>`,
25
+ DISCORD_BOT_TOKEN: html`From <a href="https://discord.com/developers/applications" target="_blank" class="text-blue-400 hover:underline">Developer Portal</a> · <a href="https://docs.openclaw.ai/channels/discord" target="_blank" class="text-blue-400 hover:underline">full guide</a>`,
26
+ BRAVE_API_KEY: html`From <a href="https://brave.com/search/api/" target="_blank" class="text-blue-400 hover:underline">brave.com/search/api</a> — free tier available`,
27
+ };
28
+
29
+ const getHintContent = (envVar) => kHintByKey[envVar.key] || envVar.hint || "";
30
+
31
+ const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
32
+ const [visible, setVisible] = useState(false);
33
+ const isSecret = !!envVar.value;
34
+
35
+ return html`
36
+ <div class="flex items-center gap-2">
37
+ <div class="flex-1 min-w-0">
38
+ <div class="flex items-center gap-2 mb-1">
39
+ <span
40
+ class="inline-block w-2 h-2 rounded-full ${envVar.value
41
+ ? "bg-green-500"
42
+ : "bg-gray-600"}"
43
+ />
44
+ <label class="text-xs font-medium text-gray-400">
45
+ ${envVar.label || envVar.key}
46
+ </label>
47
+ </div>
48
+ <div class="flex items-center gap-1">
49
+ <input
50
+ type=${isSecret && !visible ? "password" : "text"}
51
+ value=${envVar.value}
52
+ placeholder=${envVar.value ? "" : "not set"}
53
+ onInput=${(e) => onChange(envVar.key, e.target.value)}
54
+ class="w-full bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
55
+ disabled=${disabled}
56
+ />
57
+ ${isSecret
58
+ ? html`<button
59
+ onclick=${() => setVisible(!visible)}
60
+ class="text-gray-500 hover:text-gray-300 px-1 text-xs shrink-0"
61
+ title=${visible ? "Hide" : "Show"}
62
+ >
63
+ ${visible ? "Hide" : "Show"}
64
+ </button>`
65
+ : null}
66
+ ${envVar.group === "custom"
67
+ ? html`<button
68
+ onclick=${() => onDelete(envVar.key)}
69
+ class="text-gray-600 hover:text-red-400 px-1 text-xs shrink-0"
70
+ title="Delete"
71
+ >
72
+
73
+ </button>`
74
+ : null}
75
+ </div>
76
+ ${getHintContent(envVar)
77
+ ? html`<p class="text-xs text-gray-600 mt-1">${getHintContent(envVar)}</p>`
78
+ : null}
79
+ </div>
80
+ </div>
81
+ `;
82
+ };
83
+
84
+ export const Envars = () => {
85
+ const [vars, setVars] = useState([]);
86
+ const [dirty, setDirty] = useState(false);
87
+ const [saving, setSaving] = useState(false);
88
+ const [restartingGateway, setRestartingGateway] = useState(false);
89
+ const [restartRequired, setRestartRequired] = useState(false);
90
+ const [newKey, setNewKey] = useState("");
91
+
92
+ const load = useCallback(async () => {
93
+ try {
94
+ const data = await fetchEnvVars();
95
+ setVars(data.vars || []);
96
+ setDirty(false);
97
+ setRestartRequired(!!data.restartRequired);
98
+ } catch (err) {
99
+ console.error("Failed to load env vars:", err);
100
+ }
101
+ }, []);
102
+
103
+ useEffect(() => {
104
+ load();
105
+ }, [load]);
106
+
107
+ const handleChange = (key, value) => {
108
+ setVars((prev) => prev.map((v) => (v.key === key ? { ...v, value } : v)));
109
+ setDirty(true);
110
+ };
111
+
112
+ const handleDelete = (key) => {
113
+ setVars((prev) => prev.filter((v) => v.key !== key));
114
+ setDirty(true);
115
+ };
116
+
117
+ const handleSave = async () => {
118
+ if (saving) return;
119
+ setSaving(true);
120
+ try {
121
+ const toSave = vars.filter((v) => v.editable).map((v) => ({ key: v.key, value: v.value }));
122
+ const result = await saveEnvVars(toSave);
123
+ const needsRestart = !!result?.restartRequired;
124
+ setRestartRequired(needsRestart);
125
+ showToast(
126
+ needsRestart
127
+ ? "Environment variables saved. Restart gateway to apply."
128
+ : "Environment variables saved",
129
+ "success",
130
+ );
131
+ setDirty(false);
132
+ } catch (err) {
133
+ showToast("Failed to save: " + err.message, "error");
134
+ } finally {
135
+ setSaving(false);
136
+ }
137
+ };
138
+
139
+ const handleRestartGateway = async () => {
140
+ if (restartingGateway) return;
141
+ setRestartingGateway(true);
142
+ try {
143
+ await restartGateway();
144
+ setRestartRequired(false);
145
+ showToast("Gateway restarted", "success");
146
+ } catch (err) {
147
+ showToast("Restart failed: " + err.message, "error");
148
+ } finally {
149
+ setRestartingGateway(false);
150
+ }
151
+ };
152
+
153
+ const [newVal, setNewVal] = useState("");
154
+
155
+ const parsePaste = (input) => {
156
+ const lines = input.split("\n").map((l) => l.trim()).filter(Boolean).filter((l) => !l.startsWith("#"));
157
+ const pairs = [];
158
+ for (const line of lines) {
159
+ const eqIdx = line.indexOf("=");
160
+ if (eqIdx > 0) pairs.push({ key: line.slice(0, eqIdx).trim(), value: line.slice(eqIdx + 1).trim() });
161
+ }
162
+ return pairs;
163
+ };
164
+
165
+ const addVars = (pairs) => {
166
+ let added = 0;
167
+ setVars((prev) => {
168
+ const next = [...prev];
169
+ for (const { key: rawKey, value } of pairs) {
170
+ const key = rawKey.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
171
+ if (!key) continue;
172
+ const existing = next.find((v) => v.key === key);
173
+ if (existing) {
174
+ existing.value = value;
175
+ } else {
176
+ next.push({ key, value, label: key, group: "custom", hint: "", source: "env_file", editable: true });
177
+ }
178
+ added++;
179
+ }
180
+ return next;
181
+ });
182
+ if (added) setDirty(true);
183
+ return added;
184
+ };
185
+
186
+ const handlePaste = (e, fallbackField) => {
187
+ const text = (e.clipboardData || window.clipboardData).getData("text");
188
+ const pairs = parsePaste(text);
189
+ if (pairs.length > 1) {
190
+ e.preventDefault();
191
+ const added = addVars(pairs);
192
+ setNewKey("");
193
+ setNewVal("");
194
+ showToast(`Added ${added} variable${added !== 1 ? "s" : ""}`, "success");
195
+ return;
196
+ }
197
+ if (pairs.length === 1) {
198
+ e.preventDefault();
199
+ setNewKey(pairs[0].key);
200
+ setNewVal(pairs[0].value);
201
+ return;
202
+ }
203
+ };
204
+
205
+ const handleKeyInput = (raw) => {
206
+ const pairs = parsePaste(raw);
207
+ if (pairs.length === 1) {
208
+ setNewKey(pairs[0].key);
209
+ setNewVal(pairs[0].value);
210
+ return;
211
+ }
212
+ setNewKey(raw);
213
+ };
214
+
215
+ const handleValInput = (raw) => {
216
+ const pairs = parsePaste(raw);
217
+ if (pairs.length === 1) {
218
+ setNewKey(pairs[0].key);
219
+ setNewVal(pairs[0].value);
220
+ return;
221
+ }
222
+ setNewVal(raw);
223
+ };
224
+
225
+ const handleAddVar = () => {
226
+ const key = newKey.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
227
+ if (!key) return;
228
+ addVars([{ key, value: newVal }]);
229
+ setNewKey("");
230
+ setNewVal("");
231
+ };
232
+
233
+ // Group vars
234
+ const grouped = {};
235
+ for (const v of vars) {
236
+ const g = v.group || "custom";
237
+ if (!grouped[g]) grouped[g] = [];
238
+ grouped[g].push(v);
239
+ }
240
+
241
+ return html`
242
+ <div class="space-y-4">
243
+ ${kGroupOrder
244
+ .filter((g) => grouped[g]?.length)
245
+ .map(
246
+ (g) => html`
247
+ <div
248
+ class="bg-surface border border-border rounded-xl p-4 space-y-3"
249
+ >
250
+ <h3 class="text-sm font-medium text-gray-400">
251
+ ${kGroupLabels[g] || g}
252
+ </h3>
253
+ ${grouped[g].map(
254
+ (v) =>
255
+ html`<${EnvRow}
256
+ envVar=${v}
257
+ onChange=${handleChange}
258
+ onDelete=${handleDelete}
259
+ disabled=${saving}
260
+ />`
261
+ )}
262
+ </div>
263
+ `
264
+ )}
265
+
266
+ <div class="bg-surface border border-border rounded-xl p-4 space-y-3">
267
+ <div class="flex items-center justify-between">
268
+ <h3 class="text-sm font-medium text-gray-400">Add Variable</h3>
269
+ <span class="text-xs text-gray-600">Paste KEY=VALUE or multiple lines</span>
270
+ </div>
271
+ <input
272
+ type="text"
273
+ value=${newKey}
274
+ placeholder="VARIABLE_NAME"
275
+ onInput=${(e) => handleKeyInput(e.target.value)}
276
+ onPaste=${(e) => handlePaste(e, "key")}
277
+ onKeyDown=${(e) => e.key === "Enter" && handleAddVar()}
278
+ class="w-full bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono uppercase"
279
+ />
280
+ <div class="flex gap-2">
281
+ <input
282
+ type="text"
283
+ value=${newVal}
284
+ placeholder="value"
285
+ onInput=${(e) => handleValInput(e.target.value)}
286
+ onPaste=${(e) => handlePaste(e, "val")}
287
+ onKeyDown=${(e) => e.key === "Enter" && handleAddVar()}
288
+ class="flex-1 bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
289
+ />
290
+ <button
291
+ onclick=${handleAddVar}
292
+ class="text-sm px-3 py-1.5 rounded-lg border border-border text-gray-400 hover:text-gray-200 hover:border-gray-500 shrink-0"
293
+ >
294
+ + Add
295
+ </button>
296
+ </div>
297
+ </div>
298
+
299
+ ${restartRequired
300
+ ? html`<div
301
+ class="bg-yellow-500/10 border border-yellow-500/30 rounded-xl p-4 flex items-center justify-between gap-3"
302
+ >
303
+ <p class="text-sm text-yellow-200">
304
+ Gateway restart required to apply env changes.
305
+ </p>
306
+ <button
307
+ onclick=${handleRestartGateway}
308
+ disabled=${restartingGateway}
309
+ class="text-xs px-2.5 py-1 rounded-lg border border-yellow-500/40 text-yellow-200 hover:border-yellow-400 hover:text-yellow-100 transition-colors shrink-0 ${restartingGateway
310
+ ? "opacity-60 cursor-not-allowed"
311
+ : ""}"
312
+ >
313
+ ${restartingGateway ? "Restarting..." : "Restart Gateway"}
314
+ </button>
315
+ </div>`
316
+ : null}
317
+
318
+ <button
319
+ onclick=${handleSave}
320
+ disabled=${!dirty || saving || restartingGateway}
321
+ class="w-full text-sm font-medium px-4 py-2.5 rounded-xl transition-all ${dirty &&
322
+ !saving &&
323
+ !restartingGateway
324
+ ? "bg-white text-black hover:opacity-85"
325
+ : "bg-gray-800 text-gray-500 cursor-not-allowed"}"
326
+ >
327
+ ${saving
328
+ ? html`<span class="flex items-center justify-center gap-2">
329
+ <svg
330
+ class="animate-spin h-4 w-4"
331
+ viewBox="0 0 24 24"
332
+ fill="none"
333
+ >
334
+ <circle
335
+ class="opacity-25"
336
+ cx="12"
337
+ cy="12"
338
+ r="10"
339
+ stroke="currentColor"
340
+ stroke-width="4"
341
+ />
342
+ <path
343
+ class="opacity-75"
344
+ fill="currentColor"
345
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
346
+ />
347
+ </svg>
348
+ Saving...
349
+ </span>`
350
+ : "Save Changes"}
351
+ </button>
352
+ </div>
353
+ `;
354
+ };
@@ -0,0 +1,163 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useEffect, useState } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import {
5
+ fetchOpenclawVersion,
6
+ restartGateway,
7
+ updateOpenclaw,
8
+ } from "../lib/api.js";
9
+ import { showToast } from "./toast.js";
10
+ const html = htm.bind(h);
11
+
12
+ export function Gateway({ status, openclawVersion }) {
13
+ const [restarting, setRestarting] = useState(false);
14
+ const [checkingUpdate, setCheckingUpdate] = useState(false);
15
+ const [currentVersion, setCurrentVersion] = useState(openclawVersion || null);
16
+ const [latestVersion, setLatestVersion] = useState(null);
17
+ const [hasUpdate, setHasUpdate] = useState(false);
18
+ const [updateError, setUpdateError] = useState("");
19
+ const isRunning = status === "running" && !restarting;
20
+ const dotClass = isRunning
21
+ ? "w-2 h-2 rounded-full bg-green-500"
22
+ : "w-2 h-2 rounded-full bg-yellow-500 animate-pulse";
23
+
24
+ useEffect(() => {
25
+ setCurrentVersion(openclawVersion || null);
26
+ }, [openclawVersion]);
27
+
28
+ useEffect(() => {
29
+ let active = true;
30
+ const loadLatest = async () => {
31
+ try {
32
+ const data = await fetchOpenclawVersion(false);
33
+ if (!active) return;
34
+ setCurrentVersion(data.currentVersion || openclawVersion || null);
35
+ setLatestVersion(data.latestVersion || null);
36
+ setHasUpdate(!!data.hasUpdate);
37
+ setUpdateError(data.ok ? "" : data.error || "");
38
+ } catch (err) {
39
+ if (!active) return;
40
+ setUpdateError(err.message || "Could not check updates");
41
+ }
42
+ };
43
+ loadLatest();
44
+ return () => {
45
+ active = false;
46
+ };
47
+ }, []);
48
+
49
+ const handleRestart = async () => {
50
+ if (restarting) return;
51
+ setRestarting(true);
52
+ try {
53
+ await restartGateway();
54
+ showToast("Gateway restarted", "success");
55
+ } catch (err) {
56
+ showToast("Restart failed: " + err.message, "error");
57
+ }
58
+ setRestarting(false);
59
+ };
60
+
61
+ const handleUpdate = async () => {
62
+ if (checkingUpdate) return;
63
+ setCheckingUpdate(true);
64
+ setUpdateError("");
65
+ try {
66
+ const data = hasUpdate
67
+ ? await updateOpenclaw()
68
+ : await fetchOpenclawVersion(true);
69
+ setCurrentVersion(data.currentVersion || currentVersion);
70
+ setLatestVersion(data.latestVersion || null);
71
+ setHasUpdate(!!data.hasUpdate);
72
+ setUpdateError(data.ok ? "" : data.error || "");
73
+ if (hasUpdate) {
74
+ if (!data.ok) {
75
+ showToast(data.error || "OpenClaw update failed", "error");
76
+ } else if (data.updated) {
77
+ showToast(
78
+ data.restarted
79
+ ? `Updated to ${data.currentVersion} and restarted gateway`
80
+ : `Updated to ${data.currentVersion}`,
81
+ "success",
82
+ );
83
+ } else {
84
+ showToast("Already at latest OpenClaw version", "success");
85
+ }
86
+ } else if (data.hasUpdate && data.latestVersion) {
87
+ showToast(`Update available: ${data.latestVersion}`, "warning");
88
+ } else {
89
+ showToast("OpenClaw is up to date", "success");
90
+ }
91
+ } catch (err) {
92
+ setUpdateError(
93
+ err.message ||
94
+ (hasUpdate ? "Could not update OpenClaw" : "Could not check updates"),
95
+ );
96
+ showToast(
97
+ hasUpdate ? "Could not update OpenClaw" : "Could not check updates",
98
+ "error",
99
+ );
100
+ }
101
+ setCheckingUpdate(false);
102
+ };
103
+
104
+ return html` <div class="bg-surface border border-border rounded-xl p-4">
105
+ <div class="flex items-start justify-between gap-3">
106
+ <div class="min-w-0">
107
+ <div class="flex items-center gap-2">
108
+ <span class=${dotClass}></span>
109
+ <span class="font-semibold">Gateway:</span>
110
+ <span class="text-gray-400"
111
+ >${restarting ? "restarting..." : status || "checking..."}</span
112
+ >
113
+ </div>
114
+ </div>
115
+ <button
116
+ onclick=${handleRestart}
117
+ disabled=${restarting || !status}
118
+ class="text-xs px-2.5 py-1 rounded-lg border border-border text-gray-500 hover:text-gray-300 hover:border-gray-500 transition-colors ${restarting ||
119
+ !status
120
+ ? "opacity-50 cursor-not-allowed"
121
+ : ""}"
122
+ >
123
+ Restart
124
+ </button>
125
+ </div>
126
+ <div class="mt-3 pt-3 border-t border-border">
127
+ <div class="flex items-center justify-between gap-3">
128
+ <div class="min-w-0">
129
+ <p class="text-sm text-gray-300 truncate">
130
+ v${currentVersion || openclawVersion || "unknown"}
131
+ </p>
132
+ ${updateError &&
133
+ html`<p class="text-xs text-yellow-500 mt-1">${updateError}</p>`}
134
+ </div>
135
+ <div class="flex items-center gap-2 shrink-0">
136
+ ${hasUpdate &&
137
+ latestVersion &&
138
+ html`<a
139
+ href="https://github.com/openclaw/openclaw/tags"
140
+ target="_blank"
141
+ class="text-xs text-yellow-500 hover:text-yellow-300 transition-colors"
142
+ >${latestVersion} available</a
143
+ >`}
144
+ <button
145
+ onclick=${handleUpdate}
146
+ disabled=${checkingUpdate}
147
+ class="text-xs px-2.5 py-1 rounded-lg border border-border text-gray-500 hover:text-gray-300 hover:border-gray-500 transition-colors ${checkingUpdate
148
+ ? "opacity-50 cursor-not-allowed"
149
+ : ""}"
150
+ >
151
+ ${checkingUpdate
152
+ ? hasUpdate
153
+ ? "Updating..."
154
+ : "Checking..."
155
+ : hasUpdate
156
+ ? "Update"
157
+ : "Check updates"}
158
+ </button>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>`;
163
+ }