self modifying website
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>