self modifying website
at main 58 kB view raw
1<!doctype html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>C:\PLASTIC.EXE - Self-Modifying System</title> 7 <style> 8 @import url("https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap"); 9 10 body { 11 background: #f0f0f0; 12 font-family: "Courier Prime", "Courier New", monospace; 13 color: #000000; 14 margin: 0; 15 padding: 20px; 16 line-height: 1.2; 17 font-size: 14px; 18 } 19 20 .terminal { 21 background: #ffffff; 22 border: 2px solid #808080; 23 box-shadow: 24 inset -2px -2px #c0c0c0, 25 inset 2px 2px #404040; 26 max-width: 800px; 27 margin: 0 auto; 28 padding: 0; 29 } 30 31 .title-bar { 32 background: #c0c0c0; 33 color: #000000; 34 padding: 4px 8px; 35 font-weight: bold; 36 border-bottom: 1px solid #808080; 37 font-size: 12px; 38 } 39 40 .content { 41 padding: 10px; 42 background: #ffffff; 43 } 44 45 .prompt { 46 color: #000000; 47 margin: 0; 48 } 49 50 .cursor { 51 background: #000000; 52 color: #ffffff; 53 animation: blink 1s infinite; 54 } 55 56 @keyframes blink { 57 0%, 58 50% { 59 opacity: 1; 60 } 61 51%, 62 100% { 63 opacity: 0; 64 } 65 } 66 67 h1 { 68 color: #000000; 69 font-size: 24px; 70 margin: 10px 0; 71 text-transform: uppercase; 72 } 73 74 h2 { 75 color: #000000; 76 font-size: 16px; 77 margin: 15px 0 5px 0; 78 text-transform: uppercase; 79 } 80 81 .command { 82 color: #ffffff; 83 background: #808080; 84 padding: 2px 4px; 85 display: inline-block; 86 margin: 2px; 87 } 88 89 .button { 90 background: #c0c0c0; 91 color: #000000; 92 border: 2px outset #c0c0c0; 93 padding: 4px 8px; 94 font-family: inherit; 95 font-size: 12px; 96 cursor: pointer; 97 margin: 5px 2px; 98 display: inline-block; 99 text-decoration: none; 100 text-transform: uppercase; 101 } 102 103 .button:active { 104 border: 2px inset #c0c0c0; 105 } 106 107 .button:hover { 108 background: #d0d0d0; 109 } 110 111 .editor { 112 width: calc(100% - 20px); 113 height: 150px; 114 background: #0000ff; 115 color: #ffffff; 116 font-family: inherit; 117 margin: 10px 0; 118 padding: 10px; 119 border: 2px inset #c0c0c0; 120 font-size: 12px; 121 resize: none; 122 } 123 124 .status-bar { 125 background: #c0c0c0; 126 color: #000000; 127 padding: 2px 8px; 128 border-top: 1px solid #808080; 129 font-size: 11px; 130 display: flex; 131 justify-content: space-between; 132 } 133 134 .file-listing { 135 margin: 10px 0; 136 } 137 138 .file-line { 139 margin: 2px 0; 140 font-family: inherit; 141 } 142 143 .dir { 144 color: #000080; 145 } 146 147 .exe { 148 color: #000000; 149 } 150 151 .txt { 152 color: #000000; 153 } 154 155 .error { 156 color: #ffffff; 157 background: #ff0000; 158 padding: 2px 4px; 159 } 160 161 ul { 162 margin: 5px 0; 163 padding-left: 20px; 164 } 165 166 li { 167 margin: 2px 0; 168 } 169 170 .separator { 171 color: #808080; 172 margin: 10px 0; 173 } 174 175 .ascii-art { 176 color: #000000; 177 font-size: 10px; 178 line-height: 1; 179 white-space: pre; 180 margin: 10px 0; 181 } 182 183 /* Modal styles */ 184 .modal { 185 display: none; 186 position: fixed; 187 z-index: 9999; 188 left: 0; 189 top: 0; 190 width: 100%; 191 height: 100%; 192 background-color: rgba(0, 0, 0, 0.7); 193 overflow: auto; 194 opacity: 1; 195 transition: opacity 0.2s ease-in-out; 196 } 197 198 .modal-content { 199 background-color: #ffffff; 200 border: 2px solid #808080; 201 box-shadow: 202 inset -2px -2px #c0c0c0, 203 inset 2px 2px #404040, 204 0 0 10px rgba(0, 0, 0, 0.5); 205 margin: 10% auto; 206 width: 80%; 207 max-width: 600px; 208 animation: fadeIn 0.2s ease-in-out; 209 position: relative; /* Ensure proper positioning of close button */ 210 } 211 212 @keyframes fadeIn { 213 from { 214 opacity: 0; 215 transform: translateY(-20px); 216 } 217 to { 218 opacity: 1; 219 transform: translateY(0); 220 } 221 } 222 223 .modal-body { 224 padding: 15px; 225 max-height: 70vh; 226 overflow-y: auto; 227 } 228 229 .input-group { 230 margin: 10px 0; 231 } 232 233 .input-group label { 234 display: inline-block; 235 width: 80px; 236 } 237 238 .input-group input { 239 font-family: "Courier Prime", "Courier New", monospace; 240 padding: 5px; 241 background: #ffffff; 242 border: 2px inset #c0c0c0; 243 } 244 245 .save-list { 246 max-height: 200px; 247 overflow-y: auto; 248 border: 1px solid #c0c0c0; 249 margin: 10px 0; 250 padding: 5px; 251 } 252 253 .save-item { 254 display: flex; 255 justify-content: space-between; 256 margin: 5px 0; 257 padding: 8px; 258 border-bottom: 1px solid #c0c0c0; 259 background-color: #f0f0f0; 260 } 261 262 .hotkey { 263 cursor: pointer; 264 padding: 2px 5px; 265 margin: 0 2px; 266 border: 1px solid #c0c0c0; 267 border-radius: 3px; 268 } 269 270 .hotkey:hover { 271 background-color: #c0c0c0; 272 } 273 274 .modal-close { 275 position: absolute; 276 right: 10px; 277 top: 2px; 278 cursor: pointer; 279 font-size: 16px; 280 font-weight: bold; 281 color: #000000; 282 background: #d0d0d0; 283 border: 1px solid #808080; 284 border-radius: 3px; 285 width: 20px; 286 height: 20px; 287 text-align: center; 288 line-height: 18px; 289 box-shadow: 290 inset -1px -1px #404040, 291 inset 1px 1px #ffffff; 292 } 293 294 .modal-close:hover { 295 background-color: #c0c0c0; 296 color: #ff0000; 297 } 298 299 .modal-close:active { 300 box-shadow: 301 inset 1px 1px #404040, 302 inset -1px -1px #ffffff; 303 } 304 </style> 305 </head> 306 <body> 307 <!-- Help Modal --> 308 <div id="help-modal" class="modal"> 309 <div class="modal-content"> 310 <div class="title-bar"> 311 PLASTIC.EXE - Help 312 <span 313 class="modal-close" 314 onclick="hideModal('help-modal')" 315 tabindex="0" 316 role="button" 317 aria-label="Close help modal" 318 ></span 319 > 320 </div> 321 <div class="modal-body"> 322 <h2>Keyboard Shortcuts:</h2> 323 <ul> 324 <li><strong>F1</strong>: Show this help screen</li> 325 <li> 326 <strong>F2</strong>: Save the current page state 327 </li> 328 <li><strong>F3</strong>: Load a saved page state</li> 329 <li><strong>Esc</strong>: Close modals</li> 330 <li> 331 <strong>Ctrl+Enter</strong>: Execute code in the 332 editor 333 </li> 334 </ul> 335 336 <h2>How to Use PLASTIC:</h2> 337 <ol> 338 <li> 339 Type a description of what you want to change in the 340 editor 341 </li> 342 <li> 343 Press Ctrl+Enter or click the "GENERATE & EXECUTE" 344 button 345 </li> 346 <li> 347 PLASTIC will modify itself according to your 348 description 349 </li> 350 <li>Use F2 to save your modified system</li> 351 <li>Use F3 to load a previously saved state</li> 352 </ol> 353 354 <h2>About PLASTIC:</h2> 355 <p> 356 PLASTIC.EXE is a self-modifying system that lets you 357 transform its interface and functionality through 358 natural language commands. 359 </p> 360 </div> 361 <div style="text-align: center; margin-top: 15px"> 362 <div class="button" onclick="hideModal('help-modal')"> 363 CLOSE 364 </div> 365 </div> 366 </div> 367 </div> 368 369 <!-- Save Modal --> 370 <div id="save-modal" class="modal"> 371 <div class="modal-content"> 372 <div class="title-bar"> 373 PLASTIC.EXE - Save 374 <span 375 class="modal-close" 376 onclick="hideModal('save-modal')" 377 tabindex="0" 378 role="button" 379 aria-label="Close save modal" 380 ></span 381 > 382 </div> 383 <div class="modal-body"> 384 <h2>Save Current State</h2> 385 <div class="input-group"> 386 <label for="save-name">Name:</label> 387 <input 388 type="text" 389 id="save-name" 390 placeholder="Enter save name" 391 /> 392 </div> 393 <div 394 style=" 395 display: flex; 396 justify-content: center; 397 gap: 10px; 398 margin-top: 15px; 399 " 400 > 401 <div class="button" onclick="saveCurrentHTML()"> 402 SAVE 403 </div> 404 <div class="button" onclick="hideModal('save-modal')"> 405 CANCEL 406 </div> 407 </div> 408 <div id="save-message"></div> 409 410 <h2>Saved States</h2> 411 <div id="saved-list" class="save-list"></div> 412 </div> 413 </div> 414 </div> 415 416 <!-- Load Modal --> 417 <div id="load-modal" class="modal"> 418 <div class="modal-content"> 419 <div class="title-bar"> 420 PLASTIC.EXE - Load 421 <span 422 class="modal-close" 423 onclick="hideModal('load-modal')" 424 tabindex="0" 425 role="button" 426 aria-label="Close load modal" 427 ></span 428 > 429 </div> 430 <div class="modal-body"> 431 <h2>Load Saved State</h2> 432 <div id="load-list" class="save-list"></div> 433 <div id="load-message"></div> 434 <div style="text-align: center; margin-top: 15px"> 435 <div class="button" onclick="hideModal('load-modal')"> 436 CANCEL 437 </div> 438 </div> 439 </div> 440 </div> 441 </div> 442 <div class="terminal"> 443 <div class="title-bar"> 444 C:\PLASTIC.EXE - [Self-Modifying System v3.0] 445 </div> 446 447 <div class="content"> 448 <p class="prompt">C:\PLASTIC> PLASTIC.EXE</p> 449 450 <pre class="ascii-art"> 451██████╗ ██╗ █████╗ ███████╗████████╗██╗ ██████╗ 452██╔══██╗██║ ██╔══██╗██╔════╝╚══██╔══╝██║██╔════╝ 453██████╔╝██║ ███████║███████╗ ██║ ██║██║ 454██╔═══╝ ██║ ██╔══██║╚════██║ ██║ ██║██║ 455██║ ███████╗██║ ██║███████║ ██║ ██║╚██████╗ 456╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝</pre 457 > 458 459 <h2>System Information:</h2> 460 <p>PLASTIC v3.0 - Self-Modifying Code System</p> 461 <p> 462 Copyright (C) 1995 463 <a href="https://dunkirk.sh">DUNKIRK Corp (Kieran Klukas)</a 464 >. 465 </p> 466 <p> 467 All rights reserved. Licensed to: REGISTERED USER under MIT 468 </p> 469 470 <div class="separator"> 471 ════════════════════════════════════════════════════════════ 472 </div> 473 474 <h2>About This System:</h2> 475 <p> 476 PLASTIC.EXE is an experimental self-modifying executable 477 that allows real-time code injection and system 478 modification. WARNING: Use at your own risk. System may 479 become unstable. 480 </p> 481 482 <h2>Experimental Code Generator:</h2> 483 <p>Describe changes to make to the page:</p> 484 <textarea 485 class="editor" 486 id="codeEditor" 487 placeholder="e.g., 'Add a retro calculator widget', 'Change theme to green terminal', or 'Completely redesign as a DOS file manager'" 488 ></textarea> 489 <div class="button" onclick="generateAndExecute()"> 490 GENERATE & EXECUTE 491 </div> 492 <div class="button" onclick="clearEditor()">CLEAR</div> 493 <div 494 id="statusDisplay" 495 style="color: #000080; margin-top: 10px" 496 ></div> 497 498 <h2>System Features:</h2> 499 <ul> 500 <li> 501 Real-time <span class="command">EXEC</span> code 502 modification 503 </li> 504 <li>Compatible with DOS 6.22 and Windows 95</li> 505 <li>640K memory optimized</li> 506 <li>No TSR conflicts detected</li> 507 <li>Supports EGA/VGA graphics modes</li> 508 </ul> 509 510 <div class="separator"> 511 ════════════════════════════════════════════════════════════ 512 </div> 513 514 <p> 515 Runtime: <span id="runtime">00:13:37</span> | Memory: 516 <span id="memory">589K free</span> | CPU: 80486DX-33 517 </p> 518 </div> 519 520 <div class="status-bar"> 521 <span 522 ><span class="hotkey" onclick="showHelp()">F1=Help</span> 523 <span class="hotkey" onclick="showSaveModal()" 524 >F2=Save</span 525 > 526 <span class="hotkey" onclick="showLoadModal()" 527 >F3=Load</span 528 > 529 F10=Menu</span 530 > 531 <span id="time">12:00</span> 532 </div> 533 </div> 534 535 <script> 536 // Tool call system for AI to interact with the page 537 window.toolCallbacks = { 538 replaceElement: function (selector, newHTML) { 539 const element = document.querySelector(selector); 540 if (element) { 541 element.outerHTML = newHTML; 542 return { 543 success: true, 544 message: `Replaced element: ${selector}`, 545 }; 546 } 547 return { 548 success: false, 549 message: `Element not found: ${selector}`, 550 }; 551 }, 552 553 updateElement: function (selector, newContent) { 554 const element = document.querySelector(selector); 555 if (element) { 556 element.innerHTML = newContent; 557 return { 558 success: true, 559 message: `Updated element: ${selector}`, 560 }; 561 } 562 return { 563 success: false, 564 message: `Element not found: ${selector}`, 565 }; 566 }, 567 568 addElement: function ( 569 parentSelector, 570 newHTML, 571 position = "beforeend", 572 ) { 573 const parent = document.querySelector(parentSelector); 574 if (parent) { 575 parent.insertAdjacentHTML(position, newHTML); 576 return { 577 success: true, 578 message: `Added element to: ${parentSelector}`, 579 }; 580 } 581 return { 582 success: false, 583 message: `Parent not found: ${parentSelector}`, 584 }; 585 }, 586 587 removeElement: function (selector) { 588 const element = document.querySelector(selector); 589 if (element) { 590 element.remove(); 591 return { 592 success: true, 593 message: `Removed element: ${selector}`, 594 }; 595 } 596 return { 597 success: false, 598 message: `Element not found: ${selector}`, 599 }; 600 }, 601 602 updateStyle: function (selector, styleObj) { 603 const element = document.querySelector(selector); 604 if (element) { 605 Object.assign(element.style, styleObj); 606 return { 607 success: true, 608 message: `Updated styles for: ${selector}`, 609 }; 610 } 611 return { 612 success: false, 613 message: `Element not found: ${selector}`, 614 }; 615 }, 616 617 executeJS: function (code) { 618 try { 619 const result = eval(code); 620 return { 621 success: true, 622 message: "JavaScript executed", 623 result: result, 624 }; 625 } catch (error) { 626 return { 627 success: false, 628 message: `JS Error: ${error.message}`, 629 }; 630 } 631 }, 632 }; 633 634 function executeToolCall(toolCall) { 635 const { function: func, arguments: args } = toolCall; 636 console.log("Executing tool call:", func, args); 637 638 if (window.toolCallbacks[func]) { 639 try { 640 // Handle different function signatures 641 if (func === "removeElement") { 642 // removeElement expects just a selector 643 const selector = 644 args.selector || 645 Object.keys(args)[0] || 646 Object.values(args)[0]; 647 return window.toolCallbacks[func](selector); 648 } else if (func === "updateStyle" && args.styleObj) { 649 return window.toolCallbacks[func]( 650 args.selector, 651 args.styleObj, 652 ); 653 } else if (func === "executeJS") { 654 return window.toolCallbacks[func](args.code); 655 } else if (func === "updateElement") { 656 return window.toolCallbacks[func]( 657 args.selector, 658 args.newContent, 659 ); 660 } else if (func === "replaceElement") { 661 return window.toolCallbacks[func]( 662 args.selector, 663 args.newHTML, 664 ); 665 } else if (func === "addElement") { 666 return window.toolCallbacks[func]( 667 args.parentSelector, 668 args.newHTML, 669 args.position, 670 ); 671 } else { 672 return window.toolCallbacks[func]( 673 ...Object.values(args), 674 ); 675 } 676 } catch (error) { 677 return { 678 success: false, 679 message: `Error executing ${func}: ${error.message}`, 680 }; 681 } 682 } 683 return { success: false, message: `Unknown tool: ${func}` }; 684 } 685 686 async function generateAndExecute() { 687 const userPrompt = document.getElementById("codeEditor").value; 688 const statusDiv = document.getElementById("statusDisplay"); 689 690 if (userPrompt.trim() === "") { 691 statusDiv.textContent = "ERROR: No description provided"; 692 return; 693 } 694 695 statusDiv.textContent = "CONNECTING TO AI SYSTEM..."; 696 697 try { 698 const currentPageHTML = document.documentElement.outerHTML; 699 700 const response = await fetch( 701 "https://ai.hackclub.com/chat/completions", 702 { 703 method: "POST", 704 headers: { 705 "Content-Type": "application/json", 706 }, 707 body: JSON.stringify({ 708 messages: [ 709 { 710 role: "user", 711 content: `Here is the current HTML page:\n\n${currentPageHTML}\n\nUser request: "${userPrompt}"\n\nIMPORTANT: You must respond with ONLY one of these two formats:\n\nFORMAT 1 - Tool calls (for precise modifications):\n{"tool_calls": [{"function": "functionName", "arguments": {"param": "value"}}]}\n\nFORMAT 2 - Raw HTML (to append to page):\n<div>Your HTML content here</div>\n\nDO NOT include any explanatory text, markdown formatting, or additional commentary. Respond with ONLY the JSON or HTML.\n\nAvailable tools with correct argument formats:\n1. removeElement: {"function": "removeElement", "arguments": {"selector": ".class-name"}}\n2. updateElement: {"function": "updateElement", "arguments": {"selector": ".class-name", "newContent": "new content"}}\n3. replaceElement: {"function": "replaceElement", "arguments": {"selector": ".class-name", "newHTML": "<div>new html</div>"}}\n4. addElement: {"function": "addElement", "arguments": {"parentSelector": ".parent", "newHTML": "<div>content</div>", "position": "beforeend"}}\n5. updateStyle: {"function": "updateStyle", "arguments": {"selector": ".class-name", "styleObj": {"color": "#000000"}}}\n6. executeJS: {"function": "executeJS", "arguments": {"code": "console.log('hello');"}}\n\nUse DOS/retro aesthetic with flat colors: #000000 (black), #ffffff (white), #c0c0c0 (gray), #000080 (blue), #ff0000 (red). Use monospace fonts.`, 712 }, 713 ], 714 }), 715 }, 716 ); 717 718 if (!response.ok) { 719 throw new Error( 720 `HTTP ${response.status}: ${response.statusText}`, 721 ); 722 } 723 724 statusDiv.textContent = "AI PROCESSING..."; 725 726 const data = await response.json(); 727 console.log("API Response:", data); 728 729 let generatedContent; 730 if ( 731 data.choices && 732 data.choices[0] && 733 data.choices[0].message 734 ) { 735 generatedContent = data.choices[0].message.content; 736 } else if (data.content) { 737 generatedContent = data.content; 738 } else if (data.response) { 739 generatedContent = data.response; 740 } else if (typeof data === "string") { 741 generatedContent = data; 742 } else { 743 throw new Error("Unexpected API response format"); 744 } 745 746 statusDiv.textContent = "EXECUTING COMMANDS..."; 747 748 // Clean up response and extract JSON if present 749 let cleanResponse = generatedContent.trim(); 750 751 // Remove markdown formatting 752 cleanResponse = cleanResponse 753 .replace(/```json\n?/g, "") 754 .replace(/```\n?/g, ""); 755 756 // Try to extract JSON from mixed content 757 const jsonMatch = cleanResponse.match( 758 /\{[\s\S]*"tool_calls"[\s\S]*\}/, 759 ); 760 if (jsonMatch) { 761 cleanResponse = jsonMatch[0]; 762 } 763 764 console.log("Cleaned response:", cleanResponse); 765 766 // For safety, preprocess JavaScript code in JSON to escape problematic characters 767 cleanResponse = cleanResponse.replace( 768 /"code"\s*:\s*(`|")([^`"]*?)(`|")/g, 769 function (match, q1, code, q3) { 770 // Replace all literal backslashes with double backslashes in the code string 771 const escapedCode = code.replace(/\\/g, "\\\\"); 772 return `"code":${q1}${escapedCode}${q3}`; 773 }, 774 ); 775 776 // Check if response contains tool calls 777 try { 778 const toolResponse = safeJsonParse(cleanResponse); 779 if ( 780 toolResponse.tool_calls && 781 Array.isArray(toolResponse.tool_calls) 782 ) { 783 // Execute tool calls 784 const results = []; 785 for (const toolCall of toolResponse.tool_calls) { 786 const result = executeToolCall(toolCall); 787 results.push(result); 788 console.log("Tool call result:", result); 789 } 790 statusDiv.textContent = `EXECUTED ${results.length} COMMANDS`; 791 792 // Send feedback to AI about tool results 793 setTimeout( 794 () => sendToolFeedback(userPrompt, results), 795 100, 796 ); 797 } else { 798 throw new Error("Invalid tool call format"); 799 } 800 } catch (jsonError) { 801 console.log("JSON parse error:", jsonError); 802 console.log("Raw response:", generatedContent); 803 console.log("Attempting to parse as HTML..."); 804 805 // Not JSON, treat as HTML code 806 let cleanCode = generatedContent 807 .replace(/```html\n?/g, "") 808 .replace(/```\n?/g, ""); 809 810 statusDiv.textContent = "INJECTING CODE..."; 811 document.querySelector(".content").innerHTML += 812 cleanCode; 813 statusDiv.textContent = "CODE EXECUTION SUCCESSFUL"; 814 } 815 816 document.getElementById("codeEditor").value = ""; 817 818 // Clear status after 3 seconds 819 setTimeout(() => { 820 statusDiv.textContent = ""; 821 }, 3000); 822 } catch (error) { 823 statusDiv.textContent = `SYSTEM ERROR: ${error.message}`; 824 console.error("Error:", error); 825 } 826 } 827 828 function clearEditor() { 829 document.getElementById("codeEditor").value = ""; 830 document.getElementById("statusDisplay").textContent = ""; 831 } 832 833 async function sendToolFeedback(originalPrompt, toolResults) { 834 const statusDiv = document.getElementById("statusDisplay"); 835 836 try { 837 statusDiv.textContent = "SENDING FEEDBACK TO AI..."; 838 839 const currentPageHTML = document.documentElement.outerHTML; 840 const resultsText = toolResults 841 .map( 842 (r) => 843 `${r.success ? "✓" : "✗"} ${r.message}${r.result ? ` (result: ${r.result})` : ""}`, 844 ) 845 .join("\n"); 846 847 const response = await fetch( 848 "https://ai.hackclub.com/chat/completions", 849 { 850 method: "POST", 851 headers: { 852 "Content-Type": "application/json", 853 }, 854 body: JSON.stringify({ 855 messages: [ 856 { 857 role: "user", 858 content: `Previous request: "${originalPrompt}"\n\nTool execution results:\n${resultsText}\n\nCurrent page state:\n${currentPageHTML}\n\nBased on the tool results, do you need to make any follow-up modifications? If everything looks good, respond with "COMPLETE". If you need to make adjustments, respond with tool calls or HTML.\n\nIMPORTANT: Respond with ONLY one of these formats:\n- "COMPLETE" (if satisfied)\n- {"tool_calls": [...]} (for modifications)\n- Raw HTML (to append content)\n\nDO NOT include explanatory text.`, 859 }, 860 ], 861 }), 862 }, 863 ); 864 865 if (!response.ok) { 866 throw new Error( 867 `HTTP ${response.status}: ${response.statusText}`, 868 ); 869 } 870 871 const data = await response.json(); 872 let followUpContent; 873 874 if ( 875 data.choices && 876 data.choices[0] && 877 data.choices[0].message 878 ) { 879 followUpContent = data.choices[0].message.content; 880 } else if (data.content) { 881 followUpContent = data.content; 882 } else if (data.response) { 883 followUpContent = data.response; 884 } else { 885 throw new Error("Unexpected API response format"); 886 } 887 888 followUpContent = followUpContent.trim(); 889 console.log("Follow-up response:", followUpContent); 890 891 if ( 892 followUpContent === "COMPLETE" || 893 followUpContent === '"COMPLETE"' 894 ) { 895 statusDiv.textContent = "AI SATISFIED - TASK COMPLETE"; 896 setTimeout(() => (statusDiv.textContent = ""), 3000); 897 return; 898 } 899 900 // Process follow-up commands 901 statusDiv.textContent = "AI MAKING ADJUSTMENTS..."; 902 903 // Try to parse as tool calls 904 try { 905 const jsonMatch = followUpContent.match( 906 /\{[\s\S]*"tool_calls"[\s\S]*\}/, 907 ); 908 if (jsonMatch) { 909 // Preprocess JavaScript code in JSON to escape problematic characters 910 let cleanJson = jsonMatch[0].replace( 911 /"code"\s*:\s*(`|")([^`"]*?)(`|")/g, 912 function (match, q1, code, q3) { 913 // Replace all literal backslashes with double backslashes in the code string 914 const escapedCode = code.replace( 915 /\\/g, 916 "\\\\", 917 ); 918 return `"code":${q1}${escapedCode}${q3}`; 919 }, 920 ); 921 922 const toolResponse = safeJsonParse(cleanJson); 923 if ( 924 toolResponse.tool_calls && 925 Array.isArray(toolResponse.tool_calls) 926 ) { 927 const followUpResults = []; 928 for (const toolCall of toolResponse.tool_calls) { 929 const result = executeToolCall(toolCall); 930 followUpResults.push(result); 931 console.log( 932 "Follow-up tool result:", 933 result, 934 ); 935 } 936 statusDiv.textContent = `AI EXECUTED ${followUpResults.length} ADJUSTMENTS`; 937 } 938 } else { 939 // Treat as HTML 940 let cleanCode = followUpContent 941 .replace(/```html\n?/g, "") 942 .replace(/```\n?/g, ""); 943 document.querySelector(".content").innerHTML += 944 cleanCode; 945 statusDiv.textContent = 946 "AI ADDED FOLLOW-UP CONTENT"; 947 } 948 } catch (error) { 949 console.log("Follow-up parsing error:", error); 950 statusDiv.textContent = "AI FEEDBACK ERROR"; 951 } 952 953 setTimeout(() => (statusDiv.textContent = ""), 4000); 954 } catch (error) { 955 statusDiv.textContent = `FEEDBACK ERROR: ${error.message}`; 956 console.error("Feedback error:", error); 957 setTimeout(() => (statusDiv.textContent = ""), 3000); 958 } 959 } 960 961 // Debug helper for JSON parsing issues 962 function safeJsonParse(jsonString) { 963 try { 964 return JSON.parse(jsonString); 965 } catch (error) { 966 console.error("JSON parse error:", error); 967 console.error("Problem JSON:", jsonString); 968 // Try to escape any unescaped control characters 969 const escapedJson = jsonString.replace( 970 /[\u0000-\u001F]/g, 971 (match) => { 972 return ( 973 "\\u" + 974 ( 975 "0000" + match.charCodeAt(0).toString(16) 976 ).slice(-4) 977 ); 978 }, 979 ); 980 try { 981 return JSON.parse(escapedJson); 982 } catch (secondError) { 983 console.error( 984 "Second parse attempt failed:", 985 secondError, 986 ); 987 throw error; // Throw the original error 988 } 989 } 990 } 991 992 function deleteSave(key) { 993 if (confirm("Are you sure you want to delete this save?")) { 994 localStorage.removeItem(key); 995 // Refresh the load modal to show updated list 996 showLoadModal(); 997 } 998 } 999 1000 // Handle keyboard shortcuts 1001 function setupKeyboardHandlers() { 1002 const codeEditor = document.getElementById("codeEditor"); 1003 if (codeEditor) { 1004 // Remove any previous event listeners first (to avoid duplicates) 1005 codeEditor.removeEventListener("keydown", editorKeyHandler); 1006 // Add a new event listener 1007 codeEditor.addEventListener("keydown", editorKeyHandler); 1008 } else { 1009 console.error("codeEditor element not found, will retry"); 1010 // Retry after a short delay 1011 setTimeout(setupKeyboardHandlers, 100); 1012 } 1013 1014 // Global keyboard shortcuts 1015 document.addEventListener("keydown", function (e) { 1016 // F1 key - Help 1017 if (e.key === "F1") { 1018 e.preventDefault(); 1019 showHelp(); 1020 } 1021 // F2 key - Save 1022 else if (e.key === "F2") { 1023 e.preventDefault(); 1024 showSaveModal(); 1025 } 1026 // F3 key - Load 1027 else if (e.key === "F3") { 1028 e.preventDefault(); 1029 showLoadModal(); 1030 } 1031 // Escape key - Close any open modal 1032 else if (e.key === "Escape") { 1033 document.querySelectorAll(".modal").forEach((modal) => { 1034 if (modal.style.display === "block") { 1035 hideModal(modal.id); 1036 // Return focus to the editor after closing modal 1037 const editor = 1038 document.getElementById("codeEditor"); 1039 if (editor) { 1040 editor.focus(); 1041 } 1042 } 1043 }); 1044 } 1045 }); 1046 } 1047 1048 // Define the editor key handler function 1049 function editorKeyHandler(e) { 1050 if (e.ctrlKey && e.key === "Enter") { 1051 e.preventDefault(); 1052 generateAndExecute(); 1053 } 1054 } 1055 1056 // Set up the handlers immediately 1057 setupKeyboardHandlers(); 1058 1059 // Also ensure they're set up when the DOM is fully loaded 1060 // Modal functions 1061 function showModal(modalId) { 1062 const modal = document.getElementById(modalId); 1063 if (modal) { 1064 modal.style.display = "block"; 1065 // Prevent body scrolling when modal is open 1066 document.body.style.overflow = "hidden"; 1067 1068 // Make the modal appear with a nice fade-in effect 1069 modal.style.opacity = "0"; 1070 setTimeout(() => { 1071 modal.style.opacity = "1"; 1072 }, 10); 1073 1074 // Add/refresh click handler for closing when clicking outside 1075 const outsideClickHandler = function (e) { 1076 if (e.target === modal) { 1077 hideModal(modalId); 1078 } 1079 }; 1080 // Remove any existing handlers to avoid duplicates 1081 modal.removeEventListener("click", outsideClickHandler); 1082 modal.addEventListener("click", outsideClickHandler); 1083 1084 // Trap focus within the modal 1085 const focusableElements = modal.querySelectorAll( 1086 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]), .modal-close, .button', 1087 ); 1088 if (focusableElements.length > 0) { 1089 const firstElement = focusableElements[0]; 1090 const lastElement = 1091 focusableElements[focusableElements.length - 1]; 1092 1093 // Focus the first element 1094 setTimeout(() => { 1095 firstElement.focus(); 1096 }, 100); 1097 1098 // Add key event handler for tab key to trap focus 1099 const handleTabKey = function (e) { 1100 if (e.key === "Tab") { 1101 if ( 1102 e.shiftKey && 1103 document.activeElement === firstElement 1104 ) { 1105 e.preventDefault(); 1106 lastElement.focus(); 1107 } else if ( 1108 !e.shiftKey && 1109 document.activeElement === lastElement 1110 ) { 1111 e.preventDefault(); 1112 firstElement.focus(); 1113 } 1114 } 1115 }; 1116 1117 // Remove any existing handler first 1118 modal.removeEventListener("keydown", handleTabKey); 1119 modal.addEventListener("keydown", handleTabKey); 1120 1121 // Store the handler on the modal object for later cleanup 1122 modal._tabHandler = handleTabKey; 1123 } 1124 } 1125 } 1126 1127 function hideModal(modalId) { 1128 const modal = document.getElementById(modalId); 1129 if (modal) { 1130 // Fade out effect 1131 modal.style.opacity = "0"; 1132 1133 // Hide the modal after animation 1134 setTimeout(() => { 1135 modal.style.display = "none"; 1136 // Restore body scrolling when modal is closed 1137 document.body.style.overflow = "auto"; 1138 }, 200); 1139 1140 // Return focus to editor when modal is closed 1141 const editor = document.getElementById("codeEditor"); 1142 if (editor) { 1143 editor.focus(); 1144 } 1145 1146 // Clear any error messages 1147 const messageElements = document.querySelectorAll( 1148 "#save-message, #load-message", 1149 ); 1150 messageElements.forEach((el) => { 1151 if (el) el.textContent = ""; 1152 }); 1153 } 1154 } 1155 1156 function showHelp() { 1157 showModal("help-modal"); 1158 } 1159 1160 function showSaveModal() { 1161 document.getElementById("save-name").value = ""; 1162 showModal("save-modal"); 1163 // Focus the input field and select any existing text 1164 setTimeout(() => { 1165 const saveNameInput = document.getElementById("save-name"); 1166 saveNameInput.focus(); 1167 saveNameInput.select(); 1168 }, 100); 1169 } 1170 1171 function showLoadModal() { 1172 // Populate the load list 1173 const loadList = document.getElementById("load-list"); 1174 loadList.innerHTML = ""; 1175 1176 // Get all keys in localStorage that start with 'plastic-save-' 1177 const saves = []; 1178 for (let i = 0; i < localStorage.length; i++) { 1179 const key = localStorage.key(i); 1180 if (key && key.startsWith("plastic-save-")) { 1181 const saveName = key.replace("plastic-save-", ""); 1182 let timestamp = null; 1183 1184 // Try to extract timestamp if it's a JSON object 1185 try { 1186 const saveData = JSON.parse( 1187 localStorage.getItem(key), 1188 ); 1189 timestamp = saveData.timestamp; 1190 } catch (e) { 1191 // Old format, no timestamp available 1192 } 1193 1194 saves.push({ 1195 key, 1196 name: saveName, 1197 timestamp: timestamp, 1198 }); 1199 } 1200 } 1201 1202 if (saves.length === 0) { 1203 loadList.innerHTML = 1204 '<div class="save-item">No saves found</div>'; 1205 } else { 1206 // Sort saves by timestamp (newest first) or alphabetically if no timestamp 1207 saves.sort((a, b) => { 1208 if (a.timestamp && b.timestamp) { 1209 return b.timestamp - a.timestamp; // Newest first 1210 } else if (a.timestamp) { 1211 return -1; // a comes first 1212 } else if (b.timestamp) { 1213 return 1; // b comes first 1214 } else { 1215 return a.name.localeCompare(b.name); // Alphabetically 1216 } 1217 }); 1218 1219 saves.forEach((save) => { 1220 const saveItem = document.createElement("div"); 1221 saveItem.className = "save-item"; 1222 1223 // Format timestamp if available 1224 let dateStr = ""; 1225 if (save.timestamp) { 1226 const date = new Date(save.timestamp); 1227 dateStr = ` <span style="font-size: 12px; color: #808080;">(${date.toLocaleString()})</span>`; 1228 } 1229 1230 saveItem.innerHTML = ` 1231 <span>${save.name}${dateStr}</span> 1232 <div> 1233 <span class="hotkey" onclick="loadSave('${save.key}')" tabindex="0" role="button" aria-label="Load save ${save.name}">Load</span> 1234 <span class="separator">|</span> 1235 <span class="hotkey" onclick="deleteSave('${save.key}')" tabindex="0" role="button" aria-label="Delete save ${save.name}">Delete</span> 1236 </div> 1237 `; 1238 // Add double-click support to load the save directly 1239 saveItem.addEventListener("dblclick", () => 1240 loadSave(save.key), 1241 ); 1242 loadList.appendChild(saveItem); 1243 }); 1244 } 1245 1246 showModal("load-modal"); 1247 } 1248 1249 function saveCurrentHTML() { 1250 const saveName = document 1251 .getElementById("save-name") 1252 .value.trim(); 1253 if (!saveName) { 1254 const saveMessage = document.getElementById("save-message"); 1255 saveMessage.textContent = 1256 "Please enter a name for your save"; 1257 saveMessage.style.color = "#ff0000"; 1258 setTimeout(() => { 1259 saveMessage.textContent = ""; 1260 }, 3000); 1261 return; 1262 } 1263 1264 try { 1265 // Ensure all modals are closed in the saved HTML 1266 // First make a clone of the current document state 1267 const tempDiv = document.createElement("div"); 1268 tempDiv.innerHTML = document.documentElement.outerHTML; 1269 1270 // Find and hide all modals in the clone 1271 const modalElements = tempDiv.querySelectorAll(".modal"); 1272 modalElements.forEach((modal) => { 1273 modal.style.display = "none"; 1274 }); 1275 1276 // Save the current state 1277 const saveData = { 1278 html: tempDiv.innerHTML, 1279 timestamp: new Date().getTime(), 1280 name: saveName, 1281 }; 1282 1283 localStorage.setItem( 1284 `plastic-save-${saveName}`, 1285 JSON.stringify(saveData), 1286 ); 1287 1288 const saveMessage = document.getElementById("save-message"); 1289 saveMessage.textContent = `Saved as "${saveName}"`; 1290 saveMessage.style.color = "#008000"; 1291 setTimeout(() => { 1292 saveMessage.textContent = ""; 1293 hideModal("save-modal"); 1294 }, 1500); 1295 } catch (e) { 1296 const saveMessage = document.getElementById("save-message"); 1297 saveMessage.textContent = `Error saving: ${e.message}`; 1298 saveMessage.style.color = "#ff0000"; 1299 setTimeout(() => { 1300 saveMessage.textContent = ""; 1301 }, 3000); 1302 } 1303 } 1304 1305 // Add keyboard support for modals 1306 document.addEventListener("DOMContentLoaded", function () { 1307 const saveNameInput = document.getElementById("save-name"); 1308 if (saveNameInput) { 1309 // Remove any existing listeners to prevent duplicates 1310 const saveInputHandler = function (e) { 1311 if (e.key === "Enter") { 1312 e.preventDefault(); 1313 saveCurrentHTML(); 1314 } 1315 }; 1316 1317 saveNameInput.removeEventListener( 1318 "keydown", 1319 saveInputHandler, 1320 ); 1321 saveNameInput.addEventListener("keydown", saveInputHandler); 1322 } 1323 1324 document 1325 .querySelectorAll(".modal-close") 1326 .forEach((closeBtn) => { 1327 closeBtn.addEventListener("keydown", function (e) { 1328 if (e.key === "Enter" || e.key === " ") { 1329 e.preventDefault(); 1330 const modalId = this.closest(".modal").id; 1331 hideModal(modalId); 1332 } 1333 }); 1334 }); 1335 }); 1336 1337 function loadSave(key) { 1338 try { 1339 const savedData = localStorage.getItem(key); 1340 if (savedData) { 1341 let saveObj; 1342 1343 try { 1344 saveObj = JSON.parse(savedData); 1345 } catch (e) { 1346 saveObj = { html: savedData }; 1347 } 1348 1349 if ( 1350 confirm( 1351 "Loading will replace the current page. Continue?", 1352 ) 1353 ) { 1354 hideModal("load-modal"); 1355 1356 const scriptToAdd = ` 1357 <script> 1358 document.addEventListener('DOMContentLoaded', function() { 1359 // Ensure all modals are hidden 1360 document.querySelectorAll('.modal').forEach(modal => { 1361 modal.style.display = "none"; 1362 }); 1363 1364 // Re-setup keyboard handlers 1365 if (typeof setupKeyboardHandlers === 'function') { 1366 setupKeyboardHandlers(); 1367 } 1368 }); 1369 <\/script> 1370 `; 1371 1372 // Load the saved HTML with the added script 1373 document.open(); 1374 document.write(saveObj.html + scriptToAdd); 1375 document.close(); 1376 } 1377 } else { 1378 const loadMessage = 1379 document.getElementById("load-message"); 1380 loadMessage.textContent = "Save not found"; 1381 loadMessage.style.color = "#ff0000"; 1382 setTimeout(() => { 1383 loadMessage.textContent = ""; 1384 }, 3000); 1385 } 1386 } catch (e) { 1387 const loadMessage = document.getElementById("load-message"); 1388 loadMessage.textContent = `Error loading: ${e.message}`; 1389 loadMessage.style.color = "#ff0000"; 1390 setTimeout(() => { 1391 loadMessage.textContent = ""; 1392 }, 3000); 1393 } 1394 } 1395 </script> 1396 </body> 1397</html>