<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Ace Editor • AI + Settings + Save Placeholder + Search</title>
<style>
:root { --bar-h: 48px; --bg: #0f1115; --bar: #151823; --fg: #e8e8e8; --muted:#8a8f98; }
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { background: var(--bg); color: var(--fg); font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; overflow: hidden; }
/* Top bar - responsive layout */
.topbar {
position: fixed; inset: 0 0 auto 0; height: var(--bar-h);
display: grid; grid-template-columns: 1fr auto; align-items: center;
padding: 0 10px; background: var(--bar); border-bottom: 1px solid #222837; z-index: 10;
}
/* Mobile: stack search below main toolbar */
@media (max-width: 768px) {
.topbar {
height: calc(var(--bar-h) * 2);
grid-template-columns: 1fr;
grid-template-rows: var(--bar-h) var(--bar-h);
padding: 0;
}
.main-toolbar {
grid-row: 1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
}
.search-toolbar {
grid-row: 2;
display: flex;
justify-content: center;
align-items: center;
padding: 0 10px;
border-top: 1px solid #1a1f2d;
}
.search {
width: 100%;
max-width: none;
}
.search input {
width: 100%;
min-width: 0;
}
}
/* Desktop: original layout */
@media (min-width: 769px) {
.topbar {
grid-template-columns: 1fr auto 1fr;
grid-template-areas: "left center right";
}
.main-toolbar {
display: contents;
}
.search-toolbar {
grid-area: center;
justify-self: center;
}
}
.bargroup { display: flex; align-items: center; gap: 8px; }
.left { justify-self: start; }
.right { justify-self: end; }
.iconbtn {
min-width: 40px; height: 34px; padding: 0 12px;
border-radius: 10px; border: 1px solid #2a3144;
background: #1a1f2d; color: #e6e6e6; display: grid; place-items: center; cursor: pointer;
transition: transform .06s ease, background .15s ease, border-color .15s ease;
font-size: 14px; line-height: 1;
}
.iconbtn:hover { background:#22283a; border-color:#3a4562; }
.iconbtn:active { transform: scale(.98); }
/* Save dropdown (placeholder) */
.dropdown { position: relative; }
.menu {
position: absolute; top: calc(100% + 6px); left: 0; min-width: 160px;
background: #0f1422; border: 1px solid #2a3144; border-radius: 10px; padding: 6px;
display: none; z-index: 20; box-shadow: 0 10px 30px rgba(0,0,0,.35);
}
.menu.open { display: block; }
.menu button {
width: 100%; text-align: left; padding: 8px 10px; border-radius: 8px; border: 1px solid transparent;
background: transparent; color: #e7ebf7; cursor: pointer; font: 14px system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
.menu button:hover { background: #17203a; border-color: #2e3a5a; }
/* Search */
.search {
display: flex; align-items: center; gap: 6px; background: #10162a;
border: 1px solid #2a3144; border-radius: 10px; padding: 4px 6px;
}
.search input {
background: transparent; border: 0; outline: none; color: #e6e9ee; width: 240px;
font: 14px system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
.search .sbtn {
min-width: 34px; height: 28px; padding: 0 8px; border-radius: 8px;
border: 1px solid #2a3144; background: #1a1f2d; color: #e6e6e6; cursor: pointer;
font-size: 13px;
}
.search .sbtn:hover { background:#22283a; border-color:#3a4562; }
/* Settings panel */
.panel {
position: fixed; top: calc(var(--bar-h) + 8px); right: 10px; width: 320px;
background: #111521; border: 1px solid #2a3144; border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,.35); padding: 10px; z-index: 15; display: none;
}
@media (max-width: 768px) {
.panel {
top: calc(var(--bar-h) * 2 + 8px);
right: 10px;
left: 10px;
width: auto;
}
}
.panel.open { display: block; }
.tabs { display: flex; gap: 6px; padding: 6px; background: #0e121c; border-radius: 10px; margin-bottom: 8px; }
.tabbtn {
flex: 1; text-align: center; padding: 8px 10px; border-radius: 8px; cursor: pointer;
border: 1px solid #2a3144; background: #12182a; color: var(--fg); font-size: 13px;
transition: background .15s ease, border-color .15s ease;
}
.tabbtn[aria-selected="true"] { background: #1a2238; border-color: #3a4562; }
.tabpanel { display: none; }
.tabpanel.active { display: block; }
.row { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px; padding: 8px; border-radius: 8px; }
.row + .row { margin-top: 4px; }
.row label { color: var(--muted); font-size: 12px; }
select, input[type="checkbox"], input[type="range"], input[type="text"] {
accent-color: #8ab4ff; background: #0c0f18; color: #e6e9ee; border: 1px solid #2a3144; border-radius: 8px;
padding: 6px 8px; font: inherit; min-height: 32px;
}
input[type="range"] { width: 140px; padding: 0; height: 28px; }
/* Editor - adjust top margin for mobile */
#editor { position: absolute; top: var(--bar-h); left: 0; right: 0; bottom: 0; }
@media (max-width: 768px) {
#editor { top: calc(var(--bar-h) * 2); }
}
/* AI Overlay (prompt only) */
.overlay {
position: fixed; inset: 0; display: none; z-index: 50;
background: rgba(5,7,12,0.72); backdrop-filter: blur(6px);
}
.overlay.open { display: grid; grid-template-rows: auto 1fr auto; }
.overlay-header {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 14px; background: #0f1422; border-bottom: 1px solid #22283a;
}
.overlay-title { font-weight: 600; color: #dfe5f3; }
.overlay-close {
width: 34px; height: 34px; border-radius: 10px; border: 1px solid #2a3144;
background: #1a1f2d; color: #e6e6e6; display: grid; place-items: center; cursor: pointer;
}
.overlay-body { padding: 10px; overflow: auto; }
.messages { max-width: 1000px; margin: 0 auto; display: grid; gap: 10px; }
.msg { background: #0f1524; border: 1px solid #232c45; border-radius: 12px; padding: 10px 12px; color: #e6ebf5; font-size: 14px; }
.msg.user { background: #152038; border-color: #2a3a5a; }
.msg.assistant { background: #101a2f; border-color: #243356; }
.overlay-footer { border-top: 1px solid #22283a; background: #0f1422; padding: 10px; display: grid; }
.promptbar {
max-width: 1000px; margin: 0 auto; display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: end;
}
.prompt {
min-height: 44px; max-height: 40vh; resize: none; overflow-y: auto;
padding: 10px 12px; border-radius: 12px;
background: #0c0f18; color: #e6e9ee; border: 1px solid #2a3144;
font: 14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
.askbtn {
height: 44px; padding: 0 16px; border-radius: 12px; border: 1px solid #2a3144;
background: #1b2a4a; color: #e6f0ff; cursor: pointer; font-weight: 600;
}
.askbtn:hover { background: #21335a; }
.askbtn:active { transform: translateY(1px); }
</style>
</head>
<body>
<div class="topbar" role="toolbar" aria-label="Toolbar">
<div class="main-toolbar">
<!-- Left: Save dropdown (placeholders) -->
<div class="bargroup left">
<div class="dropdown">
<button id="saveBtn" class="iconbtn" aria-haspopup="true" aria-expanded="false" aria-controls="saveMenu">💾 Save</button>
<div id="saveMenu" class="menu" role="menu">
<button id="doSave" role="menuitem">Save</button>
<button id="doArtifact" role="menuitem">Save Artifact</button>
</div>
</div>
</div>
<!-- Right: AI + Settings -->
<div class="bargroup right">
<button id="aiBtn" class="iconbtn" aria-label="Open AI overlay">AI</button>
<button id="settingsBtn" class="iconbtn" aria-label="Settings">⚙️</button>
</div>
</div>
<!-- Search (center on desktop, full width on mobile) -->
<div class="search-toolbar">
<div class="search" role="search">
<input id="searchInput" type="text" placeholder="Search…" aria-label="Search in file" />
<button id="searchPrev" class="sbtn" title="Previous (Shift+Enter)">↑</button>
<button id="searchNext" class="sbtn" title="Next (Enter)">↓</button>
</div>
</div>
</div>
<!-- Settings panel with Editor/AI tabs -->
<div id="panel" class="panel" role="dialog" aria-label="Settings">
<div class="tabs" role="tablist" aria-label="Settings tabs">
<button class="tabbtn" id="tab-editor" role="tab" aria-selected="true" aria-controls="panel-editor">Editor</button>
<button class="tabbtn" id="tab-ai" role="tab" aria-selected="false" aria-controls="panel-ai">AI</button>
</div>
<section id="panel-editor" class="tabpanel active" role="tabpanel" aria-labelledby="tab-editor">
<div class="row">
<label for="theme">Theme</label>
<select id="theme">
<option value="ace/theme/monokai">Monokai (dark)</option>
</select>
</div>
<div class="row">
<label for="fontsize">Font size</label>
<input id="fontsize" type="range" min="10" max="28" value="14" />
</div>
<div class="row">
<label for="wrap">Soft wrap</label>
<input id="wrap" type="checkbox" />
</div>
<div class="row">
<label for="mode">Mode</label>
<select id="mode">
<option value="ace/mode/html">HTML</option>
<option value="ace/mode/javascript">JavaScript</option>
<option value="ace/mode/php">PHP</option>
</select>
</div>
</section>
<section id="panel-ai" class="tabpanel" role="tabpanel" aria-labelledby="tab-ai">
<div class="row">
<label for="ai-provider">Provider</label>
<select id="ai-provider">
<option value="none">None</option>
<option value="openai">OpenAI</option>
<option value="deepseek">DeepSeek</option>
<option value="anthropic">Claude</option>
</select>
</div>
<div class="row">
<label for="ai-model">Model</label>
<input id="ai-model" type="text" placeholder="e.g. gpt-4o, deepseek-chat" />
</div>
<div class="row">
<label for="ai-temp">Temperature</label>
<input id="ai-temp" type="range" min="0" max="2" step="0.1" value="0.4" />
</div>
<div class="row">
<label for="ai-key">API Key</label>
<input id="ai-key" type="text" placeholder="sk-..." />
</div>
</section>
</div>
<!-- Fullscreen AI prompt overlay -->
<div id="aiOverlay" class="overlay" aria-modal="true" role="dialog" aria-labelledby="aiTitle">
<div class="overlay-header">
<div id="aiTitle" class="overlay-title">AI Assistant</div>
<button id="overlayClose" class="overlay-close" aria-label="Close">✕</button>
</div>
<div class="overlay-body">
<div id="messages" class="messages" aria-live="polite" aria-relevant="additions"></div>
</div>
<div class="overlay-footer">
<form id="promptForm" class="promptbar">
<textarea id="prompt" class="prompt" placeholder="Ask anything about the code… (paste or type, it expands)" rows="1"></textarea>
<button class="askbtn" id="askBtn" type="submit">Ask</button>
</form>
</div>
</div>
<div id="editor"></div>
<!-- Ace (minimal) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.6/ace.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.6/ext-language_tools.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.6/mode-html.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.6/mode-javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.6/mode-php.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.6/theme-monokai.min.js"></script>
<script>
// ===== Editor =====
const editor = ace.edit("editor");
// Disable workers to avoid security errors in sandboxed environments
editor.session.setUseWorker(false);
editor.session.setMode("ace/mode/html");
editor.setTheme("ace/theme/monokai");
editor.setOptions({
tabSize: 2, useSoftTabs: true, showPrintMargin: false, wrap: false,
enableBasicAutocompletion: true, enableLiveAutocompletion: true, enableSnippets: true,
fontSize: "14px",
});
const demo = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Hello</title>
<style>
body { font-family: system-ui; margin: 2rem }
.hi { font-size: 1.5rem }
</style>
</head>
<body>
<h1>Hello, world! 👋</h1>
<p class="hi">This was inserted as an example HTML+JS snippet.</p>
<button id="btn">Click me</button>
<script>
document.getElementById('btn').addEventListener('click', () => {
alert('Hello from JavaScript!');
console.log('Button clicked at', new Date().toISOString());
});
<\/script>
</body>
</html>`;
editor.setValue(demo, -1);
// ===== Helpers =====
const $ = (id) => document.getElementById(id);
// ===== Save dropdown (placeholders) =====
const saveBtn = $("saveBtn");
const saveMenu = $("saveMenu");
saveBtn.addEventListener("click", (e) => {
const open = saveMenu.classList.toggle("open");
saveBtn.setAttribute("aria-expanded", String(open));
e.stopPropagation();
});
document.addEventListener("click", (e) => {
if (!saveMenu.contains(e.target) && !saveBtn.contains(e.target)) {
saveMenu.classList.remove("open");
saveBtn.setAttribute("aria-expanded", "false");
}
});
// Buttons exist but do nothing yet:
$("doSave").addEventListener("click", () => { /* no-op for now */ saveMenu.classList.remove("open"); });
$("doArtifact").addEventListener("click", () => { /* no-op for now */ saveMenu.classList.remove("open"); });
// ===== Settings panel + tabs =====
const panel = $("panel");
const settingsBtn = $("settingsBtn");
settingsBtn.addEventListener("click", (e) => { panel.classList.toggle("open"); e.stopPropagation(); });
document.addEventListener("click", (e) => {
if (!panel.contains(e.target) && !settingsBtn.contains(e.target)) panel.classList.remove("open");
});
const themeSel = $("theme"), wrapChk = $("wrap"), fontRange = $("fontsize"), modeSel = $("mode");
const tabEditor = $("tab-editor"), tabAI = $("tab-ai");
const panelEditor = $("panel-editor"), panelAI = $("panel-ai");
function selectTab(which) {
const isEditor = which === "editor";
tabEditor.setAttribute("aria-selected", String(isEditor));
tabAI.setAttribute("aria-selected", String(!isEditor));
panelEditor.classList.toggle("active", isEditor);
panelAI.classList.toggle("active", !isEditor);
}
tabEditor.addEventListener("click", () => selectTab("editor"));
tabAI.addEventListener("click", () => selectTab("ai"));
const aiProvider = $("ai-provider"), aiModel = $("ai-model"), aiTemp = $("ai-temp"), aiKey = $("ai-key");
// Safe localStorage access with fallback
function getPrefs() {
try {
return JSON.parse(localStorage?.getItem?.("ace_min_prefs") || "{}");
} catch (e) {
console.warn("localStorage not available, using defaults");
return {};
}
}
function savePrefs() {
try {
localStorage?.setItem?.("ace_min_prefs", JSON.stringify({
theme: editor.getTheme(),
wrap: editor.session.getUseWrapMode(),
fontSize: editor.getFontSize(),
mode: editor.session.getMode().$id,
ai: { provider: aiProvider.value, model: aiModel.value, temperature: parseFloat(aiTemp.value), key: aiKey.value }
}));
} catch (e) {
console.warn("Could not save preferences:", e.message);
}
}
const prefs = getPrefs();
if (prefs.theme) editor.setTheme(prefs.theme), (themeSel.value = prefs.theme);
if (prefs.wrap != null) editor.session.setUseWrapMode(!!prefs.wrap), (wrapChk.checked = !!prefs.wrap);
if (prefs.fontSize) editor.setFontSize(prefs.fontSize), (fontRange.value = parseInt(prefs.fontSize));
if (prefs.mode) editor.session.setMode(prefs.mode), (modeSel.value = prefs.mode);
if (prefs.ai) {
aiProvider.value = prefs.ai.provider ?? "none";
aiModel.value = prefs.ai.model ?? "";
aiTemp.value = prefs.ai.temperature ?? 0.4;
aiKey.value = prefs.ai.key ?? "";
}
themeSel.addEventListener("change", (e) => { editor.setTheme(e.target.value); savePrefs(); });
wrapChk.addEventListener("change", (e) => { editor.session.setUseWrapMode(e.target.checked); savePrefs(); });
fontRange.addEventListener("input", (e) => editor.setFontSize(e.target.value + "px"));
fontRange.addEventListener("change", savePrefs);
modeSel.addEventListener("change", (e) => { editor.session.setMode(e.target.value); savePrefs(); });
aiProvider.addEventListener("change", savePrefs);
aiModel.addEventListener("change", savePrefs);
aiTemp.addEventListener("input", savePrefs);
aiKey.addEventListener("change", savePrefs);
// ===== Search =====
const searchInput = $("searchInput");
const searchNext = $("searchNext");
const searchPrev = $("searchPrev");
function doFind(backwards=false) {
const q = searchInput.value;
if (!q) return;
editor.find(q, {
backwards,
wrap: true,
caseSensitive: false,
wholeWord: false,
regExp: false,
preventScroll: false
});
editor.focus();
}
searchNext.addEventListener("click", () => doFind(false));
searchPrev.addEventListener("click", () => doFind(true));
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
doFind(e.shiftKey); // Shift+Enter = previous
}
});
// ===== AI overlay (prompt only) =====
const aiBtn = $("aiBtn");
const aiOverlay = $("aiOverlay");
const overlayClose = $("overlayClose");
const promptForm = $("promptForm");
const prompt = $("prompt");
const messages = $("messages");
function openOverlay() {
aiOverlay.classList.add("open");
setTimeout(() => { prompt.focus(); autoGrow(prompt); }, 0);
}
function closeOverlay() { aiOverlay.classList.remove("open"); aiBtn.focus(); }
aiBtn.addEventListener("click", openOverlay);
overlayClose.addEventListener("click", closeOverlay);
window.addEventListener("keydown", (e) => {
if (aiOverlay.classList.contains("open") && e.key === "Escape") closeOverlay();
});
function autoGrow(el) {
el.style.height = "auto";
el.style.height = Math.min(el.scrollHeight, window.innerHeight * 0.4) + "px";
}
prompt.addEventListener("input", () => autoGrow(prompt));
prompt.addEventListener("paste", () => setTimeout(() => autoGrow(prompt), 0));
// Stubbed ask handler (no API call yet)
promptForm.addEventListener("submit", (e) => {
e.preventDefault();
const text = prompt.value.trim();
if (!text) return;
appendMsg("user", text);
const selection = editor.getSelectedText();
const context = selection || editor.getValue().slice(0, 1500);
const reply = `You asked: "${text}"\n\n(Stub) I would analyze ${
selection ? "the selected code" : "the first part of the current file"
}:\n\n` + context;
appendMsg("assistant", reply);
prompt.value = "";
autoGrow(prompt);
messages.parentElement.scrollTop = messages.parentElement.scrollHeight;
});
function appendMsg(role, text) {
const div = document.createElement("div");
div.className = "msg " + (role === "user" ? "user" : "assistant");
div.textContent = text;
messages.appendChild(div);
}
</script>
</body>
</html>