@biolab/talk-to-figma 0.3.3 → 0.4.1

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,834 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Talk to Figma MCP</title>
6
+ <style>
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
9
+ Helvetica, Arial, sans-serif;
10
+ margin: 0;
11
+ padding: 20px;
12
+ color: #e0e0e0;
13
+ background-color: #1e1e1e;
14
+ }
15
+
16
+ .container {
17
+ display: flex;
18
+ flex-direction: column;
19
+ height: 100%;
20
+ }
21
+
22
+ h1 {
23
+ font-size: 16px;
24
+ font-weight: 600;
25
+ margin-bottom: 10px;
26
+ color: #ffffff;
27
+ }
28
+
29
+ h2 {
30
+ font-size: 14px;
31
+ font-weight: 600;
32
+ margin-top: 20px;
33
+ margin-bottom: 8px;
34
+ color: #ffffff;
35
+ }
36
+
37
+ button {
38
+ background-color: #18a0fb;
39
+ border: none;
40
+ color: white;
41
+ padding: 8px 12px;
42
+ border-radius: 6px;
43
+ margin-top: 8px;
44
+ margin-bottom: 8px;
45
+ cursor: pointer;
46
+ font-size: 14px;
47
+ transition: background-color 0.2s;
48
+ }
49
+
50
+ button:hover {
51
+ background-color: #0d8ee0;
52
+ }
53
+
54
+ button.secondary {
55
+ background-color: #3d3d3d;
56
+ color: #e0e0e0;
57
+ }
58
+
59
+ button.secondary:hover {
60
+ background-color: #4d4d4d;
61
+ }
62
+
63
+ button:disabled {
64
+ background-color: #333333;
65
+ color: #666666;
66
+ cursor: not-allowed;
67
+ }
68
+
69
+ input {
70
+ border: 1px solid #444444;
71
+ border-radius: 4px;
72
+ padding: 8px;
73
+ margin-bottom: 12px;
74
+ font-size: 14px;
75
+ width: 100%;
76
+ box-sizing: border-box;
77
+ background-color: #2d2d2d;
78
+ color: #e0e0e0;
79
+ }
80
+
81
+ label {
82
+ display: block;
83
+ margin-bottom: 4px;
84
+ font-size: 12px;
85
+ font-weight: 500;
86
+ color: #cccccc;
87
+ }
88
+
89
+ .status {
90
+ margin-top: 16px;
91
+ padding: 12px;
92
+ border-radius: 6px;
93
+ font-size: 14px;
94
+ }
95
+
96
+ .status.connected {
97
+ background-color: #1a472a;
98
+ color: #4ade80;
99
+ }
100
+
101
+ .status.disconnected {
102
+ background-color: #471a1a;
103
+ color: #ff9999;
104
+ }
105
+
106
+ .status.info {
107
+ background-color: #1a3147;
108
+ color: #66b3ff;
109
+ }
110
+
111
+ .toggle-container {
112
+ display: flex;
113
+ align-items: center;
114
+ margin-bottom: 12px;
115
+ }
116
+
117
+ .toggle-switch {
118
+ position: relative;
119
+ display: inline-block;
120
+ width: 40px;
121
+ height: 20px;
122
+ margin-right: 8px;
123
+ }
124
+
125
+ .toggle-switch input {
126
+ opacity: 0;
127
+ width: 0;
128
+ height: 0;
129
+ margin: 0;
130
+ }
131
+
132
+ .toggle-slider {
133
+ position: absolute;
134
+ cursor: pointer;
135
+ top: 0;
136
+ left: 0;
137
+ right: 0;
138
+ bottom: 0;
139
+ background-color: #444444;
140
+ transition: 0.4s;
141
+ border-radius: 20px;
142
+ }
143
+
144
+ .toggle-slider:before {
145
+ position: absolute;
146
+ content: "";
147
+ height: 16px;
148
+ width: 16px;
149
+ left: 2px;
150
+ bottom: 2px;
151
+ background-color: white;
152
+ transition: 0.4s;
153
+ border-radius: 50%;
154
+ }
155
+
156
+ input:checked + .toggle-slider {
157
+ background-color: #18a0fb;
158
+ }
159
+
160
+ input:checked + .toggle-slider:before {
161
+ transform: translateX(20px);
162
+ }
163
+
164
+ .section {
165
+ margin-bottom: 24px;
166
+ }
167
+
168
+ .hidden {
169
+ display: none;
170
+ }
171
+
172
+ .logo {
173
+ width: 50px;
174
+ height: 50px;
175
+ }
176
+
177
+ .header {
178
+ display: flex;
179
+ align-items: center;
180
+ margin-bottom: 16px;
181
+ }
182
+
183
+ .header-text {
184
+ margin-left: 12px;
185
+ }
186
+
187
+ .header-text h1 {
188
+ margin: 0;
189
+ font-size: 16px;
190
+ }
191
+
192
+ .header-text p {
193
+ margin: 4px 0 0 0;
194
+ font-size: 12px;
195
+ color: #999999;
196
+ }
197
+
198
+ .tabs {
199
+ display: flex;
200
+ border-bottom: 1px solid #444444;
201
+ margin-bottom: 16px;
202
+ }
203
+
204
+ .tab {
205
+ padding: 8px 16px;
206
+ cursor: pointer;
207
+ font-size: 14px;
208
+ font-weight: 500;
209
+ color: #999999;
210
+ }
211
+
212
+ .tab.active {
213
+ border-bottom: 2px solid #18a0fb;
214
+ color: #18a0fb;
215
+ }
216
+
217
+ .tab-content {
218
+ display: none;
219
+ }
220
+
221
+ .tab-content.active {
222
+ display: block;
223
+ }
224
+
225
+ .link {
226
+ color: #18a0fb;
227
+ text-decoration: none;
228
+ cursor: pointer;
229
+ }
230
+
231
+ .link:hover {
232
+ text-decoration: underline;
233
+ }
234
+
235
+ .header-logo {
236
+ padding: 16px;
237
+ border-radius: 16px;
238
+ background-color: #333;
239
+ }
240
+
241
+ .header-logo-image {
242
+ width: 24px;
243
+ height: 24px;
244
+ object-fit: contain;
245
+ }
246
+ /* Progress styles */
247
+ .operation-complete {
248
+ color: #4ade80;
249
+ }
250
+ .operation-error {
251
+ color: #ff9999;
252
+ }
253
+ </style>
254
+ </head>
255
+
256
+ <body>
257
+ <div class="container">
258
+ <div class="header">
259
+ <div class="header-text">
260
+ <h1>Talk To Figma MCP Plugin</h1>
261
+ <p>Connect Figma to AI agents using MCP</p>
262
+ </div>
263
+ </div>
264
+
265
+ <div class="tabs">
266
+ <div id="tab-connection" class="tab active">Connection</div>
267
+ <div id="tab-about" class="tab">About</div>
268
+ </div>
269
+
270
+ <div id="content-connection" class="tab-content active">
271
+ <div class="section">
272
+ <div style="margin-bottom: 16px">
273
+ <button id="btn-connect" class="primary" style="width: 100%">
274
+ Connect
275
+ </button>
276
+ </div>
277
+
278
+ <div id="port-container">
279
+ <label for="port">WebSocket Server Port</label>
280
+ <input
281
+ type="number"
282
+ id="port"
283
+ placeholder="3055"
284
+ value="3055"
285
+ min="1024"
286
+ max="65535"
287
+ />
288
+ </div>
289
+ </div>
290
+
291
+ <div id="connection-status" class="status disconnected">
292
+ Not connected to MCP server
293
+ </div>
294
+
295
+ <div id="mcp-config" class="section hidden">
296
+ <h2>MCP Configuration</h2>
297
+ <p>
298
+ Copy this configuration to your <code>mcp.json</code> file in
299
+ Cursor:
300
+ </p>
301
+ <textarea
302
+ id="mcp-json"
303
+ rows="5"
304
+ readonly
305
+ style="
306
+ width: 100%;
307
+ background: #2d2d2d;
308
+ color: #e0e0e0;
309
+ padding: 12px;
310
+ border: 1px solid #444444;
311
+ border-radius: 4px;
312
+ font-family: monospace;
313
+ margin-bottom: 8px;
314
+ "
315
+ ></textarea>
316
+ <button id="btn-copy" class="secondary">Copy to Clipboard</button>
317
+ </div>
318
+
319
+ <!-- Add Progress Bar Section -->
320
+ <div id="progress-container" class="section hidden">
321
+ <h2>Operation Progress</h2>
322
+ <div id="progress-message">No operation in progress</div>
323
+ <div
324
+ style="
325
+ width: 100%;
326
+ background-color: #444;
327
+ border-radius: 4px;
328
+ margin-top: 8px;
329
+ "
330
+ >
331
+ <div
332
+ id="progress-bar"
333
+ style="
334
+ width: 0%;
335
+ height: 8px;
336
+ background-color: #18a0fb;
337
+ border-radius: 4px;
338
+ transition: width 0.3s;
339
+ "
340
+ ></div>
341
+ </div>
342
+ <div
343
+ style="
344
+ display: flex;
345
+ justify-content: space-between;
346
+ margin-top: 4px;
347
+ font-size: 12px;
348
+ "
349
+ >
350
+ <div id="progress-status">Not started</div>
351
+ <div id="progress-percentage">0%</div>
352
+ </div>
353
+ </div>
354
+ </div>
355
+
356
+ <div id="content-about" class="tab-content">
357
+ <div class="section">
358
+ <h2>About Talk To Figma Plugin</h2>
359
+ <p>
360
+ This plugin lets AI agents (Cursor, Claude Code) read and modify
361
+ Figma designs via MCP.
362
+ <a
363
+ class="link"
364
+ onclick="window.open(`https://github.com/anthropics/cursor-talk-to-figma-mcp`, '_blank')"
365
+ >Github</a
366
+ >
367
+ </p>
368
+ <p>Version: 0.4.0</p>
369
+
370
+ <h2>How to Use</h2>
371
+ <ol>
372
+ <li>Start the relay: <code>bunx @biolab/talk-to-figma --relay</code></li>
373
+ <li>Connect to the relay using the port number (default: 3055)</li>
374
+ <li>In your AI agent, call <code>join_channel</code> with the channel name shown above</li>
375
+ </ol>
376
+ </div>
377
+ </div>
378
+ </div>
379
+
380
+ <script>
381
+ // WebSocket connection state
382
+ const state = {
383
+ connected: false,
384
+ socket: null,
385
+ serverPort: 3055,
386
+ pendingRequests: new Map(),
387
+ channel: null,
388
+ };
389
+
390
+ // UI Elements
391
+ const portInput = document.getElementById("port");
392
+ const connectButton = document.getElementById("btn-connect");
393
+ const connectionStatus = document.getElementById("connection-status");
394
+
395
+ // Tabs
396
+ const tabs = document.querySelectorAll(".tab");
397
+ const tabContents = document.querySelectorAll(".tab-content");
398
+
399
+ // Add UI elements for progress tracking
400
+ const progressContainer = document.getElementById("progress-container");
401
+ const progressBar = document.getElementById("progress-bar");
402
+ const progressMessage = document.getElementById("progress-message");
403
+ const progressStatus = document.getElementById("progress-status");
404
+ const progressPercentage = document.getElementById("progress-percentage");
405
+
406
+ // Initialize UI
407
+ function updateConnectionStatus(isConnected, message) {
408
+ state.connected = isConnected;
409
+
410
+ let statusMessage =
411
+ message ||
412
+ (isConnected
413
+ ? "Connected to MCP server"
414
+ : "Not connected to MCP server");
415
+
416
+ // Add instructions for localhost when disconnected
417
+ if (!isConnected) {
418
+ statusMessage += `<br><br>Run this in your terminal, then connect<br><code>bunx @biolab/talk-to-figma --relay</code>`;
419
+ }
420
+
421
+ connectionStatus.innerHTML = statusMessage;
422
+ connectionStatus.className = `status ${
423
+ isConnected ? "connected" : "disconnected"
424
+ }`;
425
+
426
+ // Update connect button text and style based on connection state
427
+ connectButton.textContent = isConnected ? "Disconnect" : "Connect";
428
+ connectButton.className = isConnected ? "secondary" : "primary";
429
+ portInput.disabled = isConnected;
430
+
431
+ // Show/hide MCP config section
432
+ const mcpConfig = document.getElementById("mcp-config");
433
+ mcpConfig.className = `section ${isConnected ? "" : "hidden"}`;
434
+
435
+ // Update MCP config text when showing
436
+ if (isConnected) {
437
+ updateMcpConfig();
438
+ }
439
+ }
440
+
441
+ // Connect to WebSocket server
442
+ async function connectToServer(port) {
443
+ try {
444
+ if (state.connected && state.socket) {
445
+ updateConnectionStatus(true, "Already connected to server");
446
+ return;
447
+ }
448
+
449
+ state.serverPort = port;
450
+ const wsUrl = `ws://localhost:${port}`;
451
+
452
+ state.socket = new WebSocket(wsUrl);
453
+
454
+ state.socket.onopen = () => {
455
+ // Generate random channel name
456
+ const channelName = generateChannelName();
457
+ console.log("Joining channel:", channelName);
458
+ state.channel = channelName;
459
+
460
+ // Join the channel using the same format as App.tsx
461
+ state.socket.send(
462
+ JSON.stringify({
463
+ type: "join",
464
+ channel: channelName.trim(),
465
+ })
466
+ );
467
+ };
468
+
469
+ state.socket.onmessage = (event) => {
470
+ try {
471
+ const data = JSON.parse(event.data);
472
+ console.log("Received message:", data);
473
+
474
+ if (data.type === "system") {
475
+ // Successfully joined channel
476
+ if (data.message && data.message.result) {
477
+ state.connected = true;
478
+ const channelName = data.channel;
479
+ updateConnectionStatus(
480
+ true,
481
+ `Connected to server in channel: <strong>${channelName}</strong>`
482
+ );
483
+
484
+ // Notify the plugin code
485
+ parent.postMessage(
486
+ {
487
+ pluginMessage: {
488
+ type: "notify",
489
+ message: `Connected to MCP server on port ${port} in channel: ${channelName}`,
490
+ },
491
+ },
492
+ "*"
493
+ );
494
+ }
495
+ } else if (data.type === "error") {
496
+ console.error("Error:", data.message);
497
+ updateConnectionStatus(false, `Error: ${data.message}`);
498
+ state.socket.close();
499
+ }
500
+
501
+ handleSocketMessage(data);
502
+ } catch (error) {
503
+ console.error("Error parsing message:", error);
504
+ }
505
+ };
506
+
507
+ state.socket.onclose = () => {
508
+ state.connected = false;
509
+ state.socket = null;
510
+ updateConnectionStatus(false, "Disconnected from server");
511
+ };
512
+
513
+ state.socket.onerror = (error) => {
514
+ console.error("WebSocket error:", error);
515
+ updateConnectionStatus(false, "Connection error");
516
+ state.connected = false;
517
+ state.socket = null;
518
+ };
519
+ } catch (error) {
520
+ console.error("Connection error:", error);
521
+ updateConnectionStatus(
522
+ false,
523
+ `Connection error: ${error.message || "Unknown error"}`
524
+ );
525
+ }
526
+ }
527
+
528
+ // Disconnect from websocket server
529
+ function disconnectFromServer() {
530
+ if (state.socket) {
531
+ state.socket.close();
532
+ state.socket = null;
533
+ state.connected = false;
534
+ updateConnectionStatus(false, "Disconnected from server");
535
+ }
536
+ }
537
+
538
+ // Handle messages from the WebSocket
539
+ async function handleSocketMessage(payload) {
540
+ const data = payload.message;
541
+ console.log("handleSocketMessage", data);
542
+
543
+ // If it's a response to a previous request
544
+ if (data.id && state.pendingRequests.has(data.id)) {
545
+ const { resolve, reject } = state.pendingRequests.get(data.id);
546
+ state.pendingRequests.delete(data.id);
547
+
548
+ if (data.error) {
549
+ reject(new Error(data.error));
550
+ } else {
551
+ resolve(data.result);
552
+ }
553
+ return;
554
+ }
555
+
556
+ // If it's a new command
557
+ if (data.command) {
558
+ try {
559
+ // Send the command to the plugin code
560
+ parent.postMessage(
561
+ {
562
+ pluginMessage: {
563
+ type: "execute-command",
564
+ id: data.id,
565
+ command: data.command,
566
+ params: data.params,
567
+ },
568
+ },
569
+ "*"
570
+ );
571
+ } catch (error) {
572
+ // Send error back to WebSocket
573
+ sendErrorResponse(
574
+ data.id,
575
+ error.message || "Error executing command"
576
+ );
577
+ }
578
+ }
579
+ }
580
+
581
+ // Send a command to the WebSocket server
582
+ async function sendCommand(command, params) {
583
+ return new Promise((resolve, reject) => {
584
+ if (!state.connected || !state.socket) {
585
+ reject(new Error("Not connected to server"));
586
+ return;
587
+ }
588
+
589
+ const id = generateId();
590
+ state.pendingRequests.set(id, { resolve, reject });
591
+
592
+ state.socket.send(
593
+ JSON.stringify({
594
+ id,
595
+ type: "message",
596
+ channel: state.channel,
597
+ message: {
598
+ id,
599
+ command,
600
+ params,
601
+ },
602
+ })
603
+ );
604
+
605
+ // Set timeout to reject the promise after 30 seconds
606
+ setTimeout(() => {
607
+ if (state.pendingRequests.has(id)) {
608
+ state.pendingRequests.delete(id);
609
+ reject(new Error("Request timed out"));
610
+ }
611
+ }, 30000);
612
+ });
613
+ }
614
+
615
+ // Send success response back to WebSocket
616
+ function sendSuccessResponse(id, result) {
617
+ if (!state.connected || !state.socket) {
618
+ console.error("Cannot send response: socket not connected");
619
+ return;
620
+ }
621
+
622
+ state.socket.send(
623
+ JSON.stringify({
624
+ id,
625
+ type: "message",
626
+ channel: state.channel,
627
+ message: {
628
+ id,
629
+ result,
630
+ },
631
+ })
632
+ );
633
+ }
634
+
635
+ // Send error response back to WebSocket
636
+ function sendErrorResponse(id, errorMessage) {
637
+ if (!state.connected || !state.socket) {
638
+ console.error("Cannot send error response: socket not connected");
639
+ return;
640
+ }
641
+
642
+ state.socket.send(
643
+ JSON.stringify({
644
+ id,
645
+ type: "message",
646
+ channel: state.channel,
647
+ message: {
648
+ id,
649
+ error: errorMessage,
650
+ result: {},
651
+ },
652
+ })
653
+ );
654
+ }
655
+
656
+ // Helper to generate unique IDs
657
+ function generateId() {
658
+ return (
659
+ Date.now().toString(36) + Math.random().toString(36).substr(2, 5)
660
+ );
661
+ }
662
+
663
+ // Add this function after the generateId() function
664
+ function generateChannelName() {
665
+ const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
666
+ let result = "";
667
+ for (let i = 0; i < 8; i++) {
668
+ result += characters.charAt(
669
+ Math.floor(Math.random() * characters.length)
670
+ );
671
+ }
672
+ return result;
673
+ }
674
+
675
+ // Tab switching
676
+ tabs.forEach((tab) => {
677
+ tab.addEventListener("click", () => {
678
+ tabs.forEach((t) => t.classList.remove("active"));
679
+ tabContents.forEach((c) => c.classList.remove("active"));
680
+
681
+ tab.classList.add("active");
682
+ const contentId = "content-" + tab.id.split("-")[1];
683
+ document.getElementById(contentId).classList.add("active");
684
+ });
685
+ });
686
+
687
+ // Replace the separate connect/disconnect button listeners with a single toggle
688
+ connectButton.addEventListener("click", () => {
689
+ if (state.connected) {
690
+ updateConnectionStatus(false, "Disconnecting...");
691
+ connectionStatus.className = "status info";
692
+ disconnectFromServer();
693
+ } else {
694
+ const port = parseInt(portInput.value, 10) || 3055;
695
+ updateConnectionStatus(false, "Connecting...");
696
+ connectionStatus.className = "status info";
697
+ connectToServer(port);
698
+ }
699
+ });
700
+
701
+ // Function to update progress UI
702
+ function updateProgressUI(progressData) {
703
+ // Show progress container if hidden
704
+ progressContainer.classList.remove("hidden");
705
+
706
+ // Update progress bar
707
+ const progress = progressData.progress || 0;
708
+ progressBar.style.width = `${progress}%`;
709
+ progressPercentage.textContent = `${progress}%`;
710
+
711
+ // Update message
712
+ progressMessage.textContent =
713
+ progressData.message || "Operation in progress";
714
+
715
+ // Update status text based on operation state
716
+ if (progressData.status === "started") {
717
+ progressStatus.textContent = "Started";
718
+ progressStatus.className = "";
719
+ } else if (progressData.status === "in_progress") {
720
+ progressStatus.textContent = "In Progress";
721
+ progressStatus.className = "";
722
+ } else if (progressData.status === "completed") {
723
+ progressStatus.textContent = "Completed";
724
+ progressStatus.className = "operation-complete";
725
+
726
+ // Hide progress container after 5 seconds
727
+ setTimeout(() => {
728
+ progressContainer.classList.add("hidden");
729
+ }, 5000);
730
+ } else if (progressData.status === "error") {
731
+ progressStatus.textContent = "Error";
732
+ progressStatus.className = "operation-error";
733
+ }
734
+ }
735
+
736
+ // Send operation progress update to server
737
+ function sendProgressUpdateToServer(progressData) {
738
+ if (!state.connected || !state.socket) {
739
+ console.error("Cannot send progress update: socket not connected");
740
+ return;
741
+ }
742
+
743
+ console.log("Sending progress update to server:", progressData);
744
+
745
+ state.socket.send(
746
+ JSON.stringify({
747
+ id: progressData.commandId,
748
+ type: "progress_update",
749
+ channel: state.channel,
750
+ message: {
751
+ id: progressData.commandId,
752
+ type: "progress_update",
753
+ data: progressData,
754
+ },
755
+ })
756
+ );
757
+ }
758
+
759
+ // Reset progress UI
760
+ function resetProgressUI() {
761
+ progressContainer.classList.add("hidden");
762
+ progressBar.style.width = "0%";
763
+ progressMessage.textContent = "No operation in progress";
764
+ progressStatus.textContent = "Not started";
765
+ progressStatus.className = "";
766
+ progressPercentage.textContent = "0%";
767
+ }
768
+
769
+ // Listen for messages from the plugin code
770
+ window.onmessage = (event) => {
771
+ const message = event.data.pluginMessage;
772
+ if (!message) return;
773
+
774
+ console.log("Received message from plugin:", message);
775
+
776
+ switch (message.type) {
777
+ case "connection-status":
778
+ updateConnectionStatus(message.connected, message.message);
779
+ break;
780
+ case "auto-connect":
781
+ connectButton.click();
782
+ break;
783
+ case "auto-disconnect":
784
+ connectButton.click();
785
+ break;
786
+ case "command-result":
787
+ // Forward the result from plugin code back to WebSocket
788
+ sendSuccessResponse(message.id, message.result);
789
+ break;
790
+ case "command-error":
791
+ // Forward the error from plugin code back to WebSocket
792
+ sendErrorResponse(message.id, message.error);
793
+ break;
794
+ case "command_progress":
795
+ // Update UI with progress information
796
+ updateProgressUI(message);
797
+ // Forward progress update to server
798
+ sendProgressUpdateToServer(message);
799
+ break;
800
+ }
801
+ };
802
+
803
+ // Add this to the script section, after all the other event listeners
804
+ document.getElementById("btn-copy").addEventListener("click", () => {
805
+ const textarea = document.getElementById("mcp-json");
806
+ textarea.select();
807
+ document.execCommand("copy");
808
+ const button = document.getElementById("btn-copy");
809
+ button.textContent = "Copied!";
810
+ setTimeout(() => {
811
+ button.textContent = "Copy to Clipboard";
812
+ }, 2000);
813
+ });
814
+
815
+ // Add this function to the script section
816
+ function updateMcpConfig() {
817
+ const mcpConfig = {
818
+ mcpServers: {
819
+ TalkToFigma: {
820
+ command: "bunx",
821
+ args: ["@biolab/talk-to-figma@latest"],
822
+ },
823
+ },
824
+ };
825
+
826
+ document.getElementById("mcp-json").value = JSON.stringify(
827
+ mcpConfig,
828
+ null,
829
+ 2
830
+ );
831
+ }
832
+ </script>
833
+ </body>
834
+ </html>