self modifying website

Compare changes

Choose any two refs to compare.

Changed files
+1057 -77
+3 -1
README.md
···
# Plastic
-
The self modifying plastic website
+
The self modifying plastic website! It's hosted on cloudflare pages and runs in a single `index.html` :)
+
+
![a screenshot of the website](https://hc-cdn.hel1.your-objectstorage.com/s/v3/a0cd9437bae340aa75d1ca4061c68a3117c114a4_image.png)
<p align="center">
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/master/.github/images/line-break.svg" />
+1054 -76
index.html
···
white-space: pre;
margin: 10px 0;
}
+
+
/* Modal styles */
+
.modal {
+
display: none;
+
position: fixed;
+
z-index: 9999;
+
left: 0;
+
top: 0;
+
width: 100%;
+
height: 100%;
+
background-color: rgba(0, 0, 0, 0.7);
+
overflow: auto;
+
opacity: 1;
+
transition: opacity 0.2s ease-in-out;
+
}
+
+
.modal-content {
+
background-color: #ffffff;
+
border: 2px solid #808080;
+
box-shadow:
+
inset -2px -2px #c0c0c0,
+
inset 2px 2px #404040,
+
0 0 10px rgba(0, 0, 0, 0.5);
+
margin: 10% auto;
+
width: 80%;
+
max-width: 600px;
+
animation: fadeIn 0.2s ease-in-out;
+
position: relative; /* Ensure proper positioning of close button */
+
}
+
+
@keyframes fadeIn {
+
from {
+
opacity: 0;
+
transform: translateY(-20px);
+
}
+
to {
+
opacity: 1;
+
transform: translateY(0);
+
}
+
}
+
+
.modal-body {
+
padding: 15px;
+
max-height: 70vh;
+
overflow-y: auto;
+
}
+
+
.input-group {
+
margin: 10px 0;
+
}
+
+
.input-group label {
+
display: inline-block;
+
width: 80px;
+
}
+
+
.input-group input {
+
font-family: "Courier Prime", "Courier New", monospace;
+
padding: 5px;
+
background: #ffffff;
+
border: 2px inset #c0c0c0;
+
}
+
+
.save-list {
+
max-height: 200px;
+
overflow-y: auto;
+
border: 1px solid #c0c0c0;
+
margin: 10px 0;
+
padding: 5px;
+
}
+
+
.save-item {
+
display: flex;
+
justify-content: space-between;
+
margin: 5px 0;
+
padding: 8px;
+
border-bottom: 1px solid #c0c0c0;
+
background-color: #f0f0f0;
+
}
+
+
.hotkey {
+
cursor: pointer;
+
padding: 2px 5px;
+
margin: 0 2px;
+
border: 1px solid #c0c0c0;
+
border-radius: 3px;
+
}
+
+
.hotkey:hover {
+
background-color: #c0c0c0;
+
}
+
+
.modal-close {
+
position: absolute;
+
right: 10px;
+
top: 2px;
+
cursor: pointer;
+
font-size: 16px;
+
font-weight: bold;
+
color: #000000;
+
background: #d0d0d0;
+
border: 1px solid #808080;
+
border-radius: 3px;
+
width: 20px;
+
height: 20px;
+
text-align: center;
+
line-height: 18px;
+
box-shadow:
+
inset -1px -1px #404040,
+
inset 1px 1px #ffffff;
+
}
+
+
.modal-close:hover {
+
background-color: #c0c0c0;
+
color: #ff0000;
+
}
+
+
.modal-close:active {
+
box-shadow:
+
inset 1px 1px #404040,
+
inset -1px -1px #ffffff;
+
}
</style>
</head>
<body>
-
<div class="terminal">
-
<div class="title-bar">
-
C:\PLASTIC.EXE - [Self-Modifying System v2.1]
-
</div>
+
<!-- Help Modal -->
+
<div id="help-modal" class="modal">
+
<div class="modal-content">
+
<div class="title-bar">
+
PLASTIC.EXE - Help
+
<span
+
class="modal-close"
+
onclick="hideModal('help-modal')"
+
tabindex="0"
+
role="button"
+
aria-label="Close help modal"
+
>โœ•</span
+
>
+
</div>
+
<div class="modal-body">
+
<h2>Keyboard Shortcuts:</h2>
+
<ul>
+
<li><strong>F1</strong>: Show this help screen</li>
+
<li>
+
<strong>F2</strong>: Save the current page state
+
</li>
+
<li><strong>F3</strong>: Load a saved page state</li>
+
<li><strong>Esc</strong>: Close modals</li>
+
<li>
+
<strong>Ctrl+Enter</strong>: Execute code in the
+
editor
+
</li>
+
</ul>
-
<div class="content">
-
<p class="prompt">C:\PLASTIC> DIR</p>
+
<h2>How to Use PLASTIC:</h2>
+
<ol>
+
<li>
+
Type a description of what you want to change in the
+
editor
+
</li>
+
<li>
+
Press Ctrl+Enter or click the "GENERATE & EXECUTE"
+
button
+
</li>
+
<li>
+
PLASTIC will modify itself according to your
+
description
+
</li>
+
<li>Use F2 to save your modified system</li>
+
<li>Use F3 to load a previously saved state</li>
+
</ol>
-
<div class="file-listing">
-
<div class="file-line">
-
Volume in drive C is PLASTIC-SYS
+
<h2>About PLASTIC:</h2>
+
<p>
+
PLASTIC.EXE is a self-modifying system that lets you
+
transform its interface and functionality through
+
natural language commands.
+
</p>
+
</div>
+
<div style="text-align: center; margin-top: 15px">
+
<div class="button" onclick="hideModal('help-modal')">
+
CLOSE
</div>
-
<div class="file-line">Directory of C:\PLASTIC</div>
-
<div class="file-line"></div>
-
<div class="file-line">
-
<span class="dir">SYSTEM &lt;DIR&gt;</span> 12-15-95
-
3:42p
+
</div>
+
</div>
+
</div>
+
+
<!-- Save Modal -->
+
<div id="save-modal" class="modal">
+
<div class="modal-content">
+
<div class="title-bar">
+
PLASTIC.EXE - Save
+
<span
+
class="modal-close"
+
onclick="hideModal('save-modal')"
+
tabindex="0"
+
role="button"
+
aria-label="Close save modal"
+
>โœ•</span
+
>
+
</div>
+
<div class="modal-body">
+
<h2>Save Current State</h2>
+
<div class="input-group">
+
<label for="save-name">Name:</label>
+
<input
+
type="text"
+
id="save-name"
+
placeholder="Enter save name"
+
/>
</div>
-
<div class="file-line">
-
<span class="exe">PLASTIC EXE</span> 24,576 12-15-95
-
3:42p
+
<div
+
style="
+
display: flex;
+
justify-content: center;
+
gap: 10px;
+
margin-top: 15px;
+
"
+
>
+
<div class="button" onclick="saveCurrentHTML()">
+
SAVE
+
</div>
+
<div class="button" onclick="hideModal('save-modal')">
+
CANCEL
+
</div>
</div>
-
<div class="file-line">
-
<span class="txt">README TXT</span> 1,024 12-15-95 3:42p
-
</div>
-
<div class="file-line">
-
<span class="txt">CONFIG SYS</span> 512 12-15-95 3:42p
+
<div id="save-message"></div>
+
+
<h2>Saved States</h2>
+
<div id="saved-list" class="save-list"></div>
+
</div>
+
</div>
+
</div>
+
+
<!-- Load Modal -->
+
<div id="load-modal" class="modal">
+
<div class="modal-content">
+
<div class="title-bar">
+
PLASTIC.EXE - Load
+
<span
+
class="modal-close"
+
onclick="hideModal('load-modal')"
+
tabindex="0"
+
role="button"
+
aria-label="Close load modal"
+
>โœ•</span
+
>
+
</div>
+
<div class="modal-body">
+
<h2>Load Saved State</h2>
+
<div id="load-list" class="save-list"></div>
+
<div id="load-message"></div>
+
<div style="text-align: center; margin-top: 15px">
+
<div class="button" onclick="hideModal('load-modal')">
+
CANCEL
+
</div>
</div>
-
<div class="file-line">4 File(s) 26,112 bytes</div>
-
<div class="file-line">524,288 bytes free</div>
</div>
+
</div>
+
</div>
+
<div class="terminal">
+
<div class="title-bar">
+
C:\PLASTIC.EXE - [Self-Modifying System v3.0]
+
</div>
+
<div class="content">
<p class="prompt">C:\PLASTIC> PLASTIC.EXE</p>
<pre class="ascii-art">
···
>
<h2>System Information:</h2>
-
<p>PLASTIC v2.1 - Self-Modifying Code System</p>
-
<p>Copyright (C) 1995 DUNKIRK Corp.</p>
+
<p>PLASTIC v3.0 - Self-Modifying Code System</p>
+
<p>
+
Copyright (C) 1995
+
<a href="https://dunkirk.sh">DUNKIRK Corp (Kieran Klukas)</a
+
>.
+
</p>
<p>
All rights reserved. Licensed to: REGISTERED USER under MIT
</p>
···
<li>Supports EGA/VGA graphics modes</li>
</ul>
-
<h2>System Commands:</h2>
-
<p>
-
<span class="command">HELP</span>
-
<span class="command">DIR</span>
-
<span class="command">EDIT</span>
-
<span class="command">RUN</span>
-
<span class="command">EXIT</span>
-
</p>
-
<div class="separator">
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
</div>
-
<p>Runtime: 00:13:37 | Memory: 589K free | CPU: 80486DX-33</p>
-
<p class="error">
-
WARNING: Unauthorized modification may corrupt system files
+
<p>
+
Runtime: <span id="runtime">00:13:37</span> | Memory:
+
<span id="memory">589K free</span> | CPU: 80486DX-33
</p>
-
-
<p class="prompt">C:\PLASTIC> <span class="cursor">_</span></p>
</div>
<div class="status-bar">
-
<span>F1=Help F2=Save F3=Load F10=Menu</span>
-
<span>12:34 PM</span>
+
<span
+
><span class="hotkey" onclick="showHelp()">F1=Help</span>
+
<span class="hotkey" onclick="showSaveModal()"
+
>F2=Save</span
+
>
+
<span class="hotkey" onclick="showLoadModal()"
+
>F3=Load</span
+
>
+
F10=Menu</span
+
>
+
<span id="time">12:00</span>
</div>
</div>
<script>
+
// Tool call system for AI to interact with the page
+
window.toolCallbacks = {
+
replaceElement: function (selector, newHTML) {
+
const element = document.querySelector(selector);
+
if (element) {
+
element.outerHTML = newHTML;
+
return {
+
success: true,
+
message: `Replaced element: ${selector}`,
+
};
+
}
+
return {
+
success: false,
+
message: `Element not found: ${selector}`,
+
};
+
},
+
+
updateElement: function (selector, newContent) {
+
const element = document.querySelector(selector);
+
if (element) {
+
element.innerHTML = newContent;
+
return {
+
success: true,
+
message: `Updated element: ${selector}`,
+
};
+
}
+
return {
+
success: false,
+
message: `Element not found: ${selector}`,
+
};
+
},
+
+
addElement: function (
+
parentSelector,
+
newHTML,
+
position = "beforeend",
+
) {
+
const parent = document.querySelector(parentSelector);
+
if (parent) {
+
parent.insertAdjacentHTML(position, newHTML);
+
return {
+
success: true,
+
message: `Added element to: ${parentSelector}`,
+
};
+
}
+
return {
+
success: false,
+
message: `Parent not found: ${parentSelector}`,
+
};
+
},
+
+
removeElement: function (selector) {
+
const element = document.querySelector(selector);
+
if (element) {
+
element.remove();
+
return {
+
success: true,
+
message: `Removed element: ${selector}`,
+
};
+
}
+
return {
+
success: false,
+
message: `Element not found: ${selector}`,
+
};
+
},
+
+
updateStyle: function (selector, styleObj) {
+
const element = document.querySelector(selector);
+
if (element) {
+
Object.assign(element.style, styleObj);
+
return {
+
success: true,
+
message: `Updated styles for: ${selector}`,
+
};
+
}
+
return {
+
success: false,
+
message: `Element not found: ${selector}`,
+
};
+
},
+
+
executeJS: function (code) {
+
try {
+
const result = eval(code);
+
return {
+
success: true,
+
message: "JavaScript executed",
+
result: result,
+
};
+
} catch (error) {
+
return {
+
success: false,
+
message: `JS Error: ${error.message}`,
+
};
+
}
+
},
+
};
+
+
function executeToolCall(toolCall) {
+
const { function: func, arguments: args } = toolCall;
+
console.log("Executing tool call:", func, args);
+
+
if (window.toolCallbacks[func]) {
+
try {
+
// Handle different function signatures
+
if (func === "removeElement") {
+
// removeElement expects just a selector
+
const selector =
+
args.selector ||
+
Object.keys(args)[0] ||
+
Object.values(args)[0];
+
return window.toolCallbacks[func](selector);
+
} else if (func === "updateStyle" && args.styleObj) {
+
return window.toolCallbacks[func](
+
args.selector,
+
args.styleObj,
+
);
+
} else if (func === "executeJS") {
+
return window.toolCallbacks[func](args.code);
+
} else if (func === "updateElement") {
+
return window.toolCallbacks[func](
+
args.selector,
+
args.newContent,
+
);
+
} else if (func === "replaceElement") {
+
return window.toolCallbacks[func](
+
args.selector,
+
args.newHTML,
+
);
+
} else if (func === "addElement") {
+
return window.toolCallbacks[func](
+
args.parentSelector,
+
args.newHTML,
+
args.position,
+
);
+
} else {
+
return window.toolCallbacks[func](
+
...Object.values(args),
+
);
+
}
+
} catch (error) {
+
return {
+
success: false,
+
message: `Error executing ${func}: ${error.message}`,
+
};
+
}
+
}
+
return { success: false, message: `Unknown tool: ${func}` };
+
}
+
async function generateAndExecute() {
const userPrompt = document.getElementById("codeEditor").value;
const statusDiv = document.getElementById("statusDisplay");
···
messages: [
{
role: "user",
-
content: `Here is the current HTML page:\n\n${currentPageHTML}\n\nUser request: "${userPrompt}"\n\nGenerate HTML code that fits the DOS/retro terminal aesthetic for this request. Use flat colors like #00ff41 (green), #ffff00 (yellow), #00ffff (cyan), #ffffff (white), #c0c0c0 (gray), #000080 (blue), and #ff0000 (red). Use monospace fonts and simple borders. Make it look like it belongs in a 1990s DOS program. Only return the HTML code to add, no explanations.`,
+
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.`,
},
],
}),
···
statusDiv.textContent = "AI PROCESSING...";
const data = await response.json();
-
console.log("API Response:", data); // Debug log
+
console.log("API Response:", data);
-
let generatedCode;
+
let generatedContent;
if (
data.choices &&
data.choices[0] &&
data.choices[0].message
) {
-
generatedCode = data.choices[0].message.content;
+
generatedContent = data.choices[0].message.content;
} else if (data.content) {
-
generatedCode = data.content;
+
generatedContent = data.content;
} else if (data.response) {
-
generatedCode = data.response;
+
generatedContent = data.response;
} else if (typeof data === "string") {
-
generatedCode = data;
+
generatedContent = data;
} else {
throw new Error("Unexpected API response format");
}
-
// Clean up the code (remove markdown formatting if present)
-
let cleanCode = generatedCode
-
.replace(/```html\n?/g, "")
+
statusDiv.textContent = "EXECUTING COMMANDS...";
+
+
// Clean up response and extract JSON if present
+
let cleanResponse = generatedContent.trim();
+
+
// Remove markdown formatting
+
cleanResponse = cleanResponse
+
.replace(/```json\n?/g, "")
.replace(/```\n?/g, "");
-
statusDiv.textContent = "INJECTING CODE...";
+
// Try to extract JSON from mixed content
+
const jsonMatch = cleanResponse.match(
+
/\{[\s\S]*"tool_calls"[\s\S]*\}/,
+
);
+
if (jsonMatch) {
+
cleanResponse = jsonMatch[0];
+
}
+
+
console.log("Cleaned response:", cleanResponse);
-
// Insert the generated code
-
document.querySelector(".content").innerHTML += cleanCode;
-
document.getElementById("codeEditor").value = "";
+
// For safety, preprocess JavaScript code in JSON to escape problematic characters
+
cleanResponse = cleanResponse.replace(
+
/"code"\s*:\s*(`|")([^`"]*?)(`|")/g,
+
function (match, q1, code, q3) {
+
// Replace all literal backslashes with double backslashes in the code string
+
const escapedCode = code.replace(/\\/g, "\\\\");
+
return `"code":${q1}${escapedCode}${q3}`;
+
},
+
);
-
statusDiv.textContent = "CODE EXECUTION SUCCESSFUL";
+
// Check if response contains tool calls
+
try {
+
const toolResponse = safeJsonParse(cleanResponse);
+
if (
+
toolResponse.tool_calls &&
+
Array.isArray(toolResponse.tool_calls)
+
) {
+
// Execute tool calls
+
const results = [];
+
for (const toolCall of toolResponse.tool_calls) {
+
const result = executeToolCall(toolCall);
+
results.push(result);
+
console.log("Tool call result:", result);
+
}
+
statusDiv.textContent = `EXECUTED ${results.length} COMMANDS`;
+
+
// Send feedback to AI about tool results
+
setTimeout(
+
() => sendToolFeedback(userPrompt, results),
+
100,
+
);
+
} else {
+
throw new Error("Invalid tool call format");
+
}
+
} catch (jsonError) {
+
console.log("JSON parse error:", jsonError);
+
console.log("Raw response:", generatedContent);
+
console.log("Attempting to parse as HTML...");
+
+
// Not JSON, treat as HTML code
+
let cleanCode = generatedContent
+
.replace(/```html\n?/g, "")
+
.replace(/```\n?/g, "");
+
+
statusDiv.textContent = "INJECTING CODE...";
+
document.querySelector(".content").innerHTML +=
+
cleanCode;
+
statusDiv.textContent = "CODE EXECUTION SUCCESSFUL";
+
}
+
+
document.getElementById("codeEditor").value = "";
// Clear status after 3 seconds
setTimeout(() => {
···
document.getElementById("statusDisplay").textContent = "";
}
-
// Handle Ctrl+Enter in textarea
+
async function sendToolFeedback(originalPrompt, toolResults) {
+
const statusDiv = document.getElementById("statusDisplay");
+
+
try {
+
statusDiv.textContent = "SENDING FEEDBACK TO AI...";
+
+
const currentPageHTML = document.documentElement.outerHTML;
+
const resultsText = toolResults
+
.map(
+
(r) =>
+
`${r.success ? "โœ“" : "โœ—"} ${r.message}${r.result ? ` (result: ${r.result})` : ""}`,
+
)
+
.join("\n");
+
+
const response = await fetch(
+
"https://ai.hackclub.com/chat/completions",
+
{
+
method: "POST",
+
headers: {
+
"Content-Type": "application/json",
+
},
+
body: JSON.stringify({
+
messages: [
+
{
+
role: "user",
+
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.`,
+
},
+
],
+
}),
+
},
+
);
+
+
if (!response.ok) {
+
throw new Error(
+
`HTTP ${response.status}: ${response.statusText}`,
+
);
+
}
+
+
const data = await response.json();
+
let followUpContent;
+
+
if (
+
data.choices &&
+
data.choices[0] &&
+
data.choices[0].message
+
) {
+
followUpContent = data.choices[0].message.content;
+
} else if (data.content) {
+
followUpContent = data.content;
+
} else if (data.response) {
+
followUpContent = data.response;
+
} else {
+
throw new Error("Unexpected API response format");
+
}
+
+
followUpContent = followUpContent.trim();
+
console.log("Follow-up response:", followUpContent);
+
+
if (
+
followUpContent === "COMPLETE" ||
+
followUpContent === '"COMPLETE"'
+
) {
+
statusDiv.textContent = "AI SATISFIED - TASK COMPLETE";
+
setTimeout(() => (statusDiv.textContent = ""), 3000);
+
return;
+
}
+
+
// Process follow-up commands
+
statusDiv.textContent = "AI MAKING ADJUSTMENTS...";
+
+
// Try to parse as tool calls
+
try {
+
const jsonMatch = followUpContent.match(
+
/\{[\s\S]*"tool_calls"[\s\S]*\}/,
+
);
+
if (jsonMatch) {
+
// Preprocess JavaScript code in JSON to escape problematic characters
+
let cleanJson = jsonMatch[0].replace(
+
/"code"\s*:\s*(`|")([^`"]*?)(`|")/g,
+
function (match, q1, code, q3) {
+
// Replace all literal backslashes with double backslashes in the code string
+
const escapedCode = code.replace(
+
/\\/g,
+
"\\\\",
+
);
+
return `"code":${q1}${escapedCode}${q3}`;
+
},
+
);
+
+
const toolResponse = safeJsonParse(cleanJson);
+
if (
+
toolResponse.tool_calls &&
+
Array.isArray(toolResponse.tool_calls)
+
) {
+
const followUpResults = [];
+
for (const toolCall of toolResponse.tool_calls) {
+
const result = executeToolCall(toolCall);
+
followUpResults.push(result);
+
console.log(
+
"Follow-up tool result:",
+
result,
+
);
+
}
+
statusDiv.textContent = `AI EXECUTED ${followUpResults.length} ADJUSTMENTS`;
+
}
+
} else {
+
// Treat as HTML
+
let cleanCode = followUpContent
+
.replace(/```html\n?/g, "")
+
.replace(/```\n?/g, "");
+
document.querySelector(".content").innerHTML +=
+
cleanCode;
+
statusDiv.textContent =
+
"AI ADDED FOLLOW-UP CONTENT";
+
}
+
} catch (error) {
+
console.log("Follow-up parsing error:", error);
+
statusDiv.textContent = "AI FEEDBACK ERROR";
+
}
+
+
setTimeout(() => (statusDiv.textContent = ""), 4000);
+
} catch (error) {
+
statusDiv.textContent = `FEEDBACK ERROR: ${error.message}`;
+
console.error("Feedback error:", error);
+
setTimeout(() => (statusDiv.textContent = ""), 3000);
+
}
+
}
+
+
// Debug helper for JSON parsing issues
+
function safeJsonParse(jsonString) {
+
try {
+
return JSON.parse(jsonString);
+
} catch (error) {
+
console.error("JSON parse error:", error);
+
console.error("Problem JSON:", jsonString);
+
// Try to escape any unescaped control characters
+
const escapedJson = jsonString.replace(
+
/[\u0000-\u001F]/g,
+
(match) => {
+
return (
+
"\\u" +
+
(
+
"0000" + match.charCodeAt(0).toString(16)
+
).slice(-4)
+
);
+
},
+
);
+
try {
+
return JSON.parse(escapedJson);
+
} catch (secondError) {
+
console.error(
+
"Second parse attempt failed:",
+
secondError,
+
);
+
throw error; // Throw the original error
+
}
+
}
+
}
+
+
function deleteSave(key) {
+
if (confirm("Are you sure you want to delete this save?")) {
+
localStorage.removeItem(key);
+
// Refresh the load modal to show updated list
+
showLoadModal();
+
}
+
}
+
+
// Handle keyboard shortcuts
+
function setupKeyboardHandlers() {
+
const codeEditor = document.getElementById("codeEditor");
+
if (codeEditor) {
+
// Remove any previous event listeners first (to avoid duplicates)
+
codeEditor.removeEventListener("keydown", editorKeyHandler);
+
// Add a new event listener
+
codeEditor.addEventListener("keydown", editorKeyHandler);
+
} else {
+
console.error("codeEditor element not found, will retry");
+
// Retry after a short delay
+
setTimeout(setupKeyboardHandlers, 100);
+
}
+
+
// Global keyboard shortcuts
+
document.addEventListener("keydown", function (e) {
+
// F1 key - Help
+
if (e.key === "F1") {
+
e.preventDefault();
+
showHelp();
+
}
+
// F2 key - Save
+
else if (e.key === "F2") {
+
e.preventDefault();
+
showSaveModal();
+
}
+
// F3 key - Load
+
else if (e.key === "F3") {
+
e.preventDefault();
+
showLoadModal();
+
}
+
// Escape key - Close any open modal
+
else if (e.key === "Escape") {
+
document.querySelectorAll(".modal").forEach((modal) => {
+
if (modal.style.display === "block") {
+
hideModal(modal.id);
+
// Return focus to the editor after closing modal
+
const editor =
+
document.getElementById("codeEditor");
+
if (editor) {
+
editor.focus();
+
}
+
}
+
});
+
}
+
});
+
}
+
+
// Define the editor key handler function
+
function editorKeyHandler(e) {
+
if (e.ctrlKey && e.key === "Enter") {
+
e.preventDefault();
+
generateAndExecute();
+
}
+
}
+
+
// Set up the handlers immediately
+
setupKeyboardHandlers();
+
+
// Also ensure they're set up when the DOM is fully loaded
+
// Modal functions
+
function showModal(modalId) {
+
const modal = document.getElementById(modalId);
+
if (modal) {
+
modal.style.display = "block";
+
// Prevent body scrolling when modal is open
+
document.body.style.overflow = "hidden";
+
+
// Make the modal appear with a nice fade-in effect
+
modal.style.opacity = "0";
+
setTimeout(() => {
+
modal.style.opacity = "1";
+
}, 10);
+
+
// Add/refresh click handler for closing when clicking outside
+
const outsideClickHandler = function (e) {
+
if (e.target === modal) {
+
hideModal(modalId);
+
}
+
};
+
// Remove any existing handlers to avoid duplicates
+
modal.removeEventListener("click", outsideClickHandler);
+
modal.addEventListener("click", outsideClickHandler);
+
+
// Trap focus within the modal
+
const focusableElements = modal.querySelectorAll(
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]), .modal-close, .button',
+
);
+
if (focusableElements.length > 0) {
+
const firstElement = focusableElements[0];
+
const lastElement =
+
focusableElements[focusableElements.length - 1];
+
+
// Focus the first element
+
setTimeout(() => {
+
firstElement.focus();
+
}, 100);
+
+
// Add key event handler for tab key to trap focus
+
const handleTabKey = function (e) {
+
if (e.key === "Tab") {
+
if (
+
e.shiftKey &&
+
document.activeElement === firstElement
+
) {
+
e.preventDefault();
+
lastElement.focus();
+
} else if (
+
!e.shiftKey &&
+
document.activeElement === lastElement
+
) {
+
e.preventDefault();
+
firstElement.focus();
+
}
+
}
+
};
+
+
// Remove any existing handler first
+
modal.removeEventListener("keydown", handleTabKey);
+
modal.addEventListener("keydown", handleTabKey);
+
+
// Store the handler on the modal object for later cleanup
+
modal._tabHandler = handleTabKey;
+
}
+
}
+
}
+
+
function hideModal(modalId) {
+
const modal = document.getElementById(modalId);
+
if (modal) {
+
// Fade out effect
+
modal.style.opacity = "0";
+
+
// Hide the modal after animation
+
setTimeout(() => {
+
modal.style.display = "none";
+
// Restore body scrolling when modal is closed
+
document.body.style.overflow = "auto";
+
}, 200);
+
+
// Return focus to editor when modal is closed
+
const editor = document.getElementById("codeEditor");
+
if (editor) {
+
editor.focus();
+
}
+
+
// Clear any error messages
+
const messageElements = document.querySelectorAll(
+
"#save-message, #load-message",
+
);
+
messageElements.forEach((el) => {
+
if (el) el.textContent = "";
+
});
+
}
+
}
+
+
function showHelp() {
+
showModal("help-modal");
+
}
+
+
function showSaveModal() {
+
document.getElementById("save-name").value = "";
+
showModal("save-modal");
+
// Focus the input field and select any existing text
+
setTimeout(() => {
+
const saveNameInput = document.getElementById("save-name");
+
saveNameInput.focus();
+
saveNameInput.select();
+
}, 100);
+
}
+
+
function showLoadModal() {
+
// Populate the load list
+
const loadList = document.getElementById("load-list");
+
loadList.innerHTML = "";
+
+
// Get all keys in localStorage that start with 'plastic-save-'
+
const saves = [];
+
for (let i = 0; i < localStorage.length; i++) {
+
const key = localStorage.key(i);
+
if (key && key.startsWith("plastic-save-")) {
+
const saveName = key.replace("plastic-save-", "");
+
let timestamp = null;
+
+
// Try to extract timestamp if it's a JSON object
+
try {
+
const saveData = JSON.parse(
+
localStorage.getItem(key),
+
);
+
timestamp = saveData.timestamp;
+
} catch (e) {
+
// Old format, no timestamp available
+
}
+
+
saves.push({
+
key,
+
name: saveName,
+
timestamp: timestamp,
+
});
+
}
+
}
+
+
if (saves.length === 0) {
+
loadList.innerHTML =
+
'<div class="save-item">No saves found</div>';
+
} else {
+
// Sort saves by timestamp (newest first) or alphabetically if no timestamp
+
saves.sort((a, b) => {
+
if (a.timestamp && b.timestamp) {
+
return b.timestamp - a.timestamp; // Newest first
+
} else if (a.timestamp) {
+
return -1; // a comes first
+
} else if (b.timestamp) {
+
return 1; // b comes first
+
} else {
+
return a.name.localeCompare(b.name); // Alphabetically
+
}
+
});
+
+
saves.forEach((save) => {
+
const saveItem = document.createElement("div");
+
saveItem.className = "save-item";
+
+
// Format timestamp if available
+
let dateStr = "";
+
if (save.timestamp) {
+
const date = new Date(save.timestamp);
+
dateStr = ` <span style="font-size: 12px; color: #808080;">(${date.toLocaleString()})</span>`;
+
}
+
+
saveItem.innerHTML = `
+
<span>${save.name}${dateStr}</span>
+
<div>
+
<span class="hotkey" onclick="loadSave('${save.key}')" tabindex="0" role="button" aria-label="Load save ${save.name}">Load</span>
+
<span class="separator">|</span>
+
<span class="hotkey" onclick="deleteSave('${save.key}')" tabindex="0" role="button" aria-label="Delete save ${save.name}">Delete</span>
+
</div>
+
`;
+
// Add double-click support to load the save directly
+
saveItem.addEventListener("dblclick", () =>
+
loadSave(save.key),
+
);
+
loadList.appendChild(saveItem);
+
});
+
}
+
+
showModal("load-modal");
+
}
+
+
function saveCurrentHTML() {
+
const saveName = document
+
.getElementById("save-name")
+
.value.trim();
+
if (!saveName) {
+
const saveMessage = document.getElementById("save-message");
+
saveMessage.textContent =
+
"Please enter a name for your save";
+
saveMessage.style.color = "#ff0000";
+
setTimeout(() => {
+
saveMessage.textContent = "";
+
}, 3000);
+
return;
+
}
+
+
try {
+
// Ensure all modals are closed in the saved HTML
+
// First make a clone of the current document state
+
const tempDiv = document.createElement("div");
+
tempDiv.innerHTML = document.documentElement.outerHTML;
+
+
// Find and hide all modals in the clone
+
const modalElements = tempDiv.querySelectorAll(".modal");
+
modalElements.forEach((modal) => {
+
modal.style.display = "none";
+
});
+
+
// Save the current state
+
const saveData = {
+
html: tempDiv.innerHTML,
+
timestamp: new Date().getTime(),
+
name: saveName,
+
};
+
+
localStorage.setItem(
+
`plastic-save-${saveName}`,
+
JSON.stringify(saveData),
+
);
+
+
const saveMessage = document.getElementById("save-message");
+
saveMessage.textContent = `Saved as "${saveName}"`;
+
saveMessage.style.color = "#008000";
+
setTimeout(() => {
+
saveMessage.textContent = "";
+
hideModal("save-modal");
+
}, 1500);
+
} catch (e) {
+
const saveMessage = document.getElementById("save-message");
+
saveMessage.textContent = `Error saving: ${e.message}`;
+
saveMessage.style.color = "#ff0000";
+
setTimeout(() => {
+
saveMessage.textContent = "";
+
}, 3000);
+
}
+
}
+
+
// Add keyboard support for modals
document.addEventListener("DOMContentLoaded", function () {
-
document
-
.getElementById("codeEditor")
-
.addEventListener("keydown", function (e) {
-
if (e.ctrlKey && e.key === "Enter") {
+
const saveNameInput = document.getElementById("save-name");
+
if (saveNameInput) {
+
// Remove any existing listeners to prevent duplicates
+
const saveInputHandler = function (e) {
+
if (e.key === "Enter") {
e.preventDefault();
-
generateAndExecute();
+
saveCurrentHTML();
}
+
};
+
+
saveNameInput.removeEventListener(
+
"keydown",
+
saveInputHandler,
+
);
+
saveNameInput.addEventListener("keydown", saveInputHandler);
+
}
+
+
document
+
.querySelectorAll(".modal-close")
+
.forEach((closeBtn) => {
+
closeBtn.addEventListener("keydown", function (e) {
+
if (e.key === "Enter" || e.key === " ") {
+
e.preventDefault();
+
const modalId = this.closest(".modal").id;
+
hideModal(modalId);
+
}
+
});
});
});
-
// Update time in status bar
-
setInterval(() => {
-
const now = new Date();
-
const timeStr = now.toLocaleTimeString([], {
-
hour: "2-digit",
-
minute: "2-digit",
-
});
-
const statusBarTime = document.querySelector(
-
".status-bar span:last-child",
-
);
-
if (statusBarTime) {
-
statusBarTime.textContent = timeStr;
+
function loadSave(key) {
+
try {
+
const savedData = localStorage.getItem(key);
+
if (savedData) {
+
let saveObj;
+
+
try {
+
saveObj = JSON.parse(savedData);
+
} catch (e) {
+
saveObj = { html: savedData };
+
}
+
+
if (
+
confirm(
+
"Loading will replace the current page. Continue?",
+
)
+
) {
+
hideModal("load-modal");
+
+
const scriptToAdd = `
+
<script>
+
document.addEventListener('DOMContentLoaded', function() {
+
// Ensure all modals are hidden
+
document.querySelectorAll('.modal').forEach(modal => {
+
modal.style.display = "none";
+
});
+
+
// Re-setup keyboard handlers
+
if (typeof setupKeyboardHandlers === 'function') {
+
setupKeyboardHandlers();
+
}
+
});
+
<\/script>
+
`;
+
+
// Load the saved HTML with the added script
+
document.open();
+
document.write(saveObj.html + scriptToAdd);
+
document.close();
+
}
+
} else {
+
const loadMessage =
+
document.getElementById("load-message");
+
loadMessage.textContent = "Save not found";
+
loadMessage.style.color = "#ff0000";
+
setTimeout(() => {
+
loadMessage.textContent = "";
+
}, 3000);
+
}
+
} catch (e) {
+
const loadMessage = document.getElementById("load-message");
+
loadMessage.textContent = `Error loading: ${e.message}`;
+
loadMessage.style.color = "#ff0000";
+
setTimeout(() => {
+
loadMessage.textContent = "";
+
}, 3000);
}
-
}, 1000);
+
}
</script>
</body>
</html>