Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Espace Codage</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <link rel="stylesheet" href="style.css"> | |
| </head> | |
| <body class="text-gray-100 h-screen overflow-hidden"> | |
| <div class="app"> | |
| <!-- LEFT --> | |
| <section class="card" aria-label="Sidebar"> | |
| <div class="cardHeader"> | |
| <div class="title"><span class="dot"></span><span>Espace Codage</span></div> | |
| <span class="pill" id="pillProject">Projet 1</span> | |
| </div> | |
| <div class="sidebarBody"> | |
| <div class="searchRow"> | |
| <button class="iconBtn" id="btnNewProject" title="Nouveau projet">+</button> | |
| <div class="inputWrap"> | |
| <span class="mag">🔎</span> | |
| <input class="input" id="projectSearch" placeholder="Rechercher..." autocomplete="off" /> | |
| </div> | |
| </div> | |
| <div class="sectionLabel">Projets</div> | |
| <div class="list" id="projectList"></div> | |
| <div class="sectionLabel">Raccourcis</div> | |
| <div class="list" style="gap:10px"> | |
| <div class="item" data-shortcut="library"> | |
| <div class="badge">📚</div> | |
| <div class="itemText"> | |
| <div class="name">Bibliothèque</div> | |
| <div class="sub">Templates & snippets</div> | |
| </div> | |
| </div> | |
| <div class="item" data-shortcut="rosalinda"> | |
| <div class="badge">🤖</div> | |
| <div class="itemText"> | |
| <div class="name">Rosalinda</div> | |
| <div class="sub">Assistant local</div> | |
| </div> | |
| </div> | |
| <div class="item" data-shortcut="settings"> | |
| <div class="badge">⚙️</div> | |
| <div class="itemText"> | |
| <div class="name">Paramètres</div> | |
| <div class="sub">Préférences</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footerRow"> | |
| <span>Made with Espace Codage</span> | |
| <span class="pill" style="padding:6px 10px;">Profil</span> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- CENTER --> | |
| <section class="card" aria-label="Centre"> | |
| <div class="cardHeader"> | |
| <div class="crumbs"> | |
| <span class="crumb">Aperçu</span> | |
| <span class="crumb">Projet</span> | |
| <span class="crumb" id="crumbName">Projet 1</span> | |
| </div> | |
| <span class="pill" id="pillMode">Mode: Preview</span> | |
| </div> | |
| <div class="centerBody"> | |
| <div class="chatArea"> | |
| <div class="chatInner" id="chatInner"> | |
| <!-- Empty state injected --> | |
| </div> | |
| </div> | |
| <div class="composer"> | |
| <button class="iconBtn" id="btnPlus" title="Actions rapides">+</button> | |
| <button class="iconBtn" id="btnAttach" title="Joindre (local)">📎</button> | |
| <input class="composeInput" id="chatInput" placeholder="Écris ici… (ex: “crée une page”, ou colle du code HTML/CSS/JS)" /> | |
| <button class="iconBtn" id="btnMic" title="Micro (dictée)">🎤</button> | |
| <button class="sendBtn" id="btnSend" title="Envoyer">➤</button> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- RIGHT --> | |
| <section class="card" aria-label="Droite"> | |
| <div class="cardHeader"> | |
| <div class="title" style="gap:8px;"><span style="font-weight:900">Aperçu</span></div> | |
| <div class="toolRow"> | |
| <button class="iconBtn" id="btnBack" title="Retour">←</button> | |
| <button class="iconBtn" id="btnForward" title="Avancer">→</button> | |
| <button class="iconBtn" id="btnRefresh" title="Rafraîchir">⟳</button> | |
| <button class="iconBtn" id="btnFull" title="Plein écran aperçu">⤢</button> | |
| </div> | |
| </div> | |
| <div class="rightBody"> | |
| <div class="topTools"> | |
| <div class="tabs"> | |
| <div class="tab active" id="tabCode">Code</div> | |
| <div class="tab" id="tabPreview">Aperçu</div> | |
| </div> | |
| <div class="toolRow"> | |
| <button class="miniBtn primary" id="btnRun">Run</button> | |
| <button class="miniBtn" id="btnSave">Save</button> | |
| <button class="miniBtn" id="btnDownload">Download</button> | |
| <button class="miniBtn danger" id="btnReset">Reset</button> | |
| </div> | |
| </div> | |
| <div class="editorWrap" id="panelCode"> | |
| <div class="editorHeader"> | |
| <span><b>Editor</b> — HTML/CSS/JS (local)</span> | |
| <span class="pill" id="pillSaved">Auto-save</span> | |
| </div> | |
| <textarea class="editor" id="codeEditor" spellcheck="false"></textarea> | |
| </div> | |
| <div class="previewWrap" id="panelPreview" style="display:none"> | |
| <div class="editorHeader"> | |
| <span><b>Preview</b> — iframe sandbox</span> | |
| <span class="pill bad" id="pillPreviewState">Hors ligne</span> | |
| </div> | |
| <div class="previewBox" id="previewBox"> | |
| <!-- Empty preview injected --> | |
| </div> | |
| <div class="statusBar"> | |
| <span id="statusLeft">Statut: Hors ligne</span> | |
| <span id="statusRight">Mode: Preview</span> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| <script src="script.js"></script> | |
| <script> | |
| feather.replace(); | |
| </script> | |
| </body> | |
| </html> | |
| <html lang="fr"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Espace Codage — Rosalinda</title> | |
| <style> | |
| :root{ | |
| --bg:#0b1220; | |
| --panel:#0f1a2b; | |
| --panel2:#101f35; | |
| --border:rgba(255,255,255,.08); | |
| --text:#e8eefc; | |
| --muted:rgba(232,238,252,.65); | |
| --muted2:rgba(232,238,252,.45); | |
| --accent:#4ea1ff; | |
| --accent2:#7dd3fc; | |
| --danger:#ff6b6b; | |
| --ok:#2ee59d; | |
| --shadow: 0 10px 30px rgba(0,0,0,.35); | |
| --radius:16px; | |
| --radius2:22px; | |
| --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono","Courier New", monospace; | |
| --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; | |
| } | |
| *{box-sizing:border-box} | |
| html,body{height:100%} | |
| body{ | |
| margin:0; | |
| font-family:var(--sans); | |
| background: radial-gradient(1200px 600px at 40% 20%, rgba(78,161,255,.12), transparent 60%), | |
| radial-gradient(900px 500px at 80% 10%, rgba(125,211,252,.08), transparent 55%), | |
| var(--bg); | |
| color:var(--text); | |
| overflow:hidden; | |
| } | |
| /* Layout */ | |
| .app{ | |
| height:100vh; | |
| padding:14px; | |
| display:grid; | |
| grid-template-columns: 320px 1fr 420px; | |
| gap:14px; | |
| } | |
| .card{ | |
| background: linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.015)); | |
| border:1px solid var(--border); | |
| border-radius: var(--radius2); | |
| box-shadow: var(--shadow); | |
| overflow:hidden; | |
| display:flex; | |
| flex-direction:column; | |
| min-height:0; | |
| } | |
| .cardHeader{ | |
| padding:14px 14px 10px 14px; | |
| display:flex; | |
| align-items:center; | |
| justify-content:space-between; | |
| gap:10px; | |
| border-bottom:1px solid var(--border); | |
| background: rgba(255,255,255,.02); | |
| } | |
| .title{ | |
| display:flex; | |
| align-items:center; | |
| gap:10px; | |
| font-weight:800; | |
| letter-spacing:.2px; | |
| } | |
| .dot{ | |
| width:10px;height:10px;border-radius:999px; | |
| background: rgba(78,161,255,.9); | |
| box-shadow: 0 0 0 4px rgba(78,161,255,.15); | |
| flex:0 0 auto; | |
| } | |
| .pill{ | |
| font-size:12px; | |
| padding:6px 10px; | |
| border-radius:999px; | |
| border:1px solid var(--border); | |
| background: rgba(255,255,255,.02); | |
| color:var(--muted); | |
| } | |
| .pill.ok{ color: rgba(46,229,157,.95); border-color: rgba(46,229,157,.25); background: rgba(46,229,157,.06); } | |
| .pill.bad{ color: rgba(255,107,107,.95); border-color: rgba(255,107,107,.25); background: rgba(255,107,107,.06); } | |
| /* Sidebar */ | |
| .sidebarBody{ padding:14px; display:flex; flex-direction:column; gap:14px; min-height:0; } | |
| .searchRow{ display:flex; gap:10px; align-items:center; } | |
| .iconBtn{ | |
| width:40px;height:40px;border-radius:12px; | |
| border:1px solid var(--border); | |
| background: rgba(255,255,255,.02); | |
| color:var(--text); | |
| cursor:pointer; | |
| display:grid;place-items:center; | |
| transition:.15s transform, .15s background; | |
| user-select:none; | |
| } | |
| .iconBtn:hover{ transform: translateY(-1px); background: rgba(255,255,255,.04); } | |
| .iconBtn:active{ transform: translateY(0px) scale(.98); } | |
| .input{ | |
| width:100%; | |
| height:40px; | |
| border-radius:12px; | |
| border:1px solid var(--border); | |
| background: rgba(0,0,0,.22); | |
| color:var(--text); | |
| padding:0 12px 0 38px; | |
| outline:none; | |
| font-size:14px; | |
| } | |
| .input::placeholder{ color: rgba(232,238,252,.45); } | |
| .inputWrap{ position:relative; flex:1; } | |
| .mag{ | |
| position:absolute; left:12px; top:50%; transform:translateY(-50%); | |
| color: rgba(232,238,252,.45); | |
| font-size:14px; | |
| } | |
| .sectionLabel{ | |
| font-size:12px; | |
| color: var(--muted2); | |
| letter-spacing:.12em; | |
| text-transform:uppercase; | |
| margin-top:2px; | |
| } | |
| .list{ display:flex; flex-direction:column; gap:10px; min-height:0; overflow:auto; padding-right:6px; } | |
| .item{ | |
| padding:12px; | |
| border:1px solid var(--border); | |
| border-radius:14px; | |
| background: rgba(255,255,255,.02); | |
| display:flex; | |
| gap:12px; | |
| align-items:center; | |
| cursor:pointer; | |
| transition:.15s background, .15s transform; | |
| user-select:none; | |
| } | |
| .item:hover{ background: rgba(255,255,255,.04); transform: translateY(-1px); } | |
| .item.active{ border-color: rgba(78,161,255,.35); background: rgba(78,161,255,.08); } | |
| .badge{ | |
| width:34px;height:34px;border-radius:12px; | |
| display:grid;place-items:center; | |
| border:1px solid var(--border); | |
| background: rgba(0,0,0,.22); | |
| color: rgba(232,238,252,.85); | |
| font-family: var(--mono); | |
| font-size:12px; | |
| } | |
| .itemText{ display:flex; flex-direction:column; gap:2px; } | |
| .itemText .name{ font-weight:700; } | |
| .itemText .sub{ font-size:12px; color: var(--muted); } | |
| .footerRow{ | |
| margin-top:auto; | |
| display:flex; align-items:center; justify-content:space-between; gap:10px; | |
| padding-top:10px; | |
| border-top:1px solid var(--border); | |
| color: var(--muted); | |
| font-size:12px; | |
| } | |
| /* Center (Chat) */ | |
| .centerBody{ padding:14px; display:flex; flex-direction:column; gap:12px; min-height:0; } | |
| .crumbs{ | |
| display:flex; align-items:center; gap:8px; | |
| color: var(--muted); | |
| font-size:12px; | |
| } | |
| .crumb{ padding:6px 10px; border-radius:999px; border:1px solid var(--border); background: rgba(255,255,255,.02); } | |
| .chatArea{ | |
| flex:1; | |
| min-height:0; | |
| border:1px dashed rgba(255,255,255,.12); | |
| border-radius: var(--radius2); | |
| background: rgba(0,0,0,.18); | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| padding:18px; | |
| position:relative; | |
| overflow:hidden; | |
| } | |
| .chatInner{ | |
| width:100%; | |
| height:100%; | |
| display:flex; | |
| flex-direction:column; | |
| gap:12px; | |
| overflow:auto; | |
| padding-right:8px; | |
| } | |
| .emptyState{ | |
| text-align:center; | |
| color: var(--muted); | |
| max-width:520px; | |
| padding:22px; | |
| margin:auto; | |
| } | |
| .emptyIcon{ | |
| width:84px;height:84px;border-radius:18px; | |
| margin:0 auto 14px auto; | |
| border:1px solid var(--border); | |
| background: radial-gradient(circle at 35% 30%, rgba(255,255,255,.12), rgba(255,255,255,.02) 60%), | |
| rgba(0,0,0,.22); | |
| } | |
| .emptyTitle{ font-weight:800; color: var(--text); font-size:20px; margin-bottom:6px; } | |
| .emptySub{ font-size:13px; color: var(--muted); } | |
| .msg{ | |
| display:flex; | |
| gap:10px; | |
| align-items:flex-start; | |
| } | |
| .avatar{ | |
| width:34px;height:34px;border-radius:12px; | |
| border:1px solid var(--border); | |
| background: rgba(0,0,0,.22); | |
| display:grid;place-items:center; | |
| font-weight:800; | |
| color: rgba(232,238,252,.9); | |
| flex:0 0 auto; | |
| } | |
| .bubble{ | |
| max-width: 880px; | |
| border:1px solid var(--border); | |
| border-radius:16px; | |
| padding:10px 12px; | |
| background: rgba(255,255,255,.02); | |
| } | |
| .bubble.user{ border-color: rgba(78,161,255,.25); background: rgba(78,161,255,.08); } | |
| .meta{ | |
| font-size:12px; | |
| color: var(--muted2); | |
| margin-bottom:6px; | |
| display:flex; gap:10px; align-items:center; | |
| } | |
| .text{ white-space:pre-wrap; line-height:1.4; font-size:14px; } | |
| .codeBlock{ | |
| margin-top:8px; | |
| padding:10px; | |
| border-radius:14px; | |
| border:1px solid rgba(255,255,255,.10); | |
| background: rgba(0,0,0,.28); | |
| font-family: var(--mono); | |
| font-size:12.5px; | |
| overflow:auto; | |
| } | |
| /* Composer */ | |
| .composer{ | |
| display:flex; | |
| gap:10px; | |
| align-items:center; | |
| padding:10px; | |
| border:1px solid var(--border); | |
| border-radius: 18px; | |
| background: rgba(255,255,255,.02); | |
| } | |
| .composeInput{ | |
| flex:1; | |
| height:42px; | |
| border-radius:14px; | |
| border:1px solid transparent; | |
| background: rgba(0,0,0,.20); | |
| color: var(--text); | |
| padding:0 12px; | |
| outline:none; | |
| font-size:14px; | |
| } | |
| .composeInput:focus{ border-color: rgba(78,161,255,.35); } | |
| .sendBtn{ | |
| width:46px;height:46px;border-radius:16px; | |
| border:1px solid rgba(78,161,255,.35); | |
| background: rgba(78,161,255,.22); | |
| cursor:pointer; | |
| display:grid;place-items:center; | |
| transition:.15s transform, .15s background; | |
| } | |
| .sendBtn:hover{ transform: translateY(-1px); background: rgba(78,161,255,.28); } | |
| .sendBtn:active{ transform: translateY(0) scale(.98); } | |
| .micOn{ border-color: rgba(46,229,157,.35) ; background: rgba(46,229,157,.14) ; } | |
| /* Right panel */ | |
| .rightBody{ padding:12px; display:flex; flex-direction:column; gap:10px; min-height:0; } | |
| .topTools{ | |
| display:flex; align-items:center; justify-content:space-between; gap:10px; | |
| } | |
| .tabs{ | |
| display:flex; gap:8px; align-items:center; | |
| } | |
| .tab{ | |
| padding:8px 10px; | |
| border-radius:999px; | |
| border:1px solid var(--border); | |
| background: rgba(255,255,255,.02); | |
| color: var(--muted); | |
| cursor:pointer; | |
| user-select:none; | |
| font-size:12px; | |
| } | |
| .tab.active{ | |
| color: rgba(232,238,252,.95); | |
| border-color: rgba(78,161,255,.35); | |
| background: rgba(78,161,255,.10); | |
| } | |
| .toolRow{ display:flex; gap:8px; align-items:center; } | |
| .miniBtn{ | |
| padding:8px 10px; | |
| border-radius:12px; | |
| border:1px solid var(--border); | |
| background: rgba(255,255,255,.02); | |
| cursor:pointer; | |
| color: var(--text); | |
| font-size:12px; | |
| user-select:none; | |
| transition:.15s transform, .15s background; | |
| } | |
| .miniBtn:hover{ transform: translateY(-1px); background: rgba(255,255,255,.04); } | |
| .miniBtn:active{ transform: translateY(0) scale(.98); } | |
| .miniBtn.primary{ border-color: rgba(78,161,255,.35); background: rgba(78,161,255,.18); } | |
| .miniBtn.danger{ border-color: rgba(255,107,107,.35); background: rgba(255,107,107,.12); } | |
| .editorWrap{ | |
| flex:1; | |
| min-height:0; | |
| border:1px solid var(--border); | |
| border-radius: var(--radius2); | |
| overflow:hidden; | |
| background: rgba(0,0,0,.16); | |
| display:flex; | |
| flex-direction:column; | |
| } | |
| .editorHeader{ | |
| padding:10px 12px; | |
| border-bottom:1px solid var(--border); | |
| display:flex; align-items:center; justify-content:space-between; | |
| color: var(--muted); | |
| font-size:12px; | |
| background: rgba(255,255,255,.02); | |
| } | |
| textarea.editor{ | |
| flex:1; | |
| min-height:0; | |
| width:100%; | |
| resize:none; | |
| border:none; | |
| outline:none; | |
| background: transparent; | |
| color: rgba(232,238,252,.95); | |
| padding:12px; | |
| font-family: var(--mono); | |
| font-size:12.8px; | |
| line-height:1.45; | |
| tab-size:2; | |
| } | |
| .previewWrap{ | |
| flex:1; | |
| min-height:0; | |
| border:1px solid var(--border); | |
| border-radius: var(--radius2); | |
| overflow:hidden; | |
| background: rgba(0,0,0,.16); | |
| display:flex; | |
| flex-direction:column; | |
| } | |
| .previewBox{ | |
| flex:1; | |
| min-height:0; | |
| display:grid; | |
| place-items:center; | |
| padding:16px; | |
| position:relative; | |
| } | |
| iframe{ | |
| width:100%; | |
| height:100%; | |
| border:none; | |
| background:white; | |
| border-radius: 14px; | |
| } | |
| .previewEmpty{ | |
| text-align:center; | |
| color: var(--muted); | |
| max-width:320px; | |
| } | |
| .previewEmpty .big{ | |
| font-weight:900; | |
| color: var(--text); | |
| margin-top:12px; | |
| } | |
| .statusBar{ | |
| display:flex; align-items:center; justify-content:space-between; | |
| padding:10px 12px; | |
| border-top:1px solid var(--border); | |
| color: var(--muted); | |
| font-size:12px; | |
| background: rgba(255,255,255,.02); | |
| } | |
| /* Scrollbars */ | |
| .list::-webkit-scrollbar, | |
| .chatInner::-webkit-scrollbar, | |
| textarea.editor::-webkit-scrollbar{ | |
| width:10px; | |
| } | |
| .list::-webkit-scrollbar-thumb, | |
| .chatInner::-webkit-scrollbar-thumb, | |
| textarea.editor::-webkit-scrollbar-thumb{ | |
| background: rgba(255,255,255,.10); | |
| border:3px solid transparent; | |
| background-clip: padding-box; | |
| border-radius:999px; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 1100px){ | |
| body{ overflow:auto; } | |
| .app{ grid-template-columns: 1fr; height:auto; overflow:auto; } | |
| .card{ min-height: 320px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <!-- LEFT --> | |
| <section class="card" aria-label="Sidebar"> | |
| <div class="cardHeader"> | |
| <div class="title"><span class="dot"></span><span>Espace Codage</span></div> | |
| <span class="pill" id="pillProject">Projet 1</span> | |
| </div> | |
| <div class="sidebarBody"> | |
| <div class="searchRow"> | |
| <button class="iconBtn" id="btnNewProject" title="Nouveau projet">+</button> | |
| <div class="inputWrap"> | |
| <span class="mag">🔎</span> | |
| <input class="input" id="projectSearch" placeholder="Rechercher..." autocomplete="off" /> | |
| </div> | |
| </div> | |
| <div class="sectionLabel">Projets</div> | |
| <div class="list" id="projectList"></div> | |
| <div class="sectionLabel">Raccourcis</div> | |
| <div class="list" style="gap:10px"> | |
| <div class="item" data-shortcut="library"> | |
| <div class="badge">📚</div> | |
| <div class="itemText"> | |
| <div class="name">Bibliothèque</div> | |
| <div class="sub">Templates & snippets</div> | |
| </div> | |
| </div> | |
| <div class="item" data-shortcut="rosalinda"> | |
| <div class="badge">🤖</div> | |
| <div class="itemText"> | |
| <div class="name">Rosalinda</div> | |
| <div class="sub">Assistant local</div> | |
| </div> | |
| </div> | |
| <div class="item" data-shortcut="settings"> | |
| <div class="badge">⚙️</div> | |
| <div class="itemText"> | |
| <div class="name">Paramètres</div> | |
| <div class="sub">Préférences</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footerRow"> | |
| <span>Made with Espace Codage</span> | |
| <span class="pill" style="padding:6px 10px;">Profil</span> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- CENTER --> | |
| <section class="card" aria-label="Centre"> | |
| <div class="cardHeader"> | |
| <div class="crumbs"> | |
| <span class="crumb">Aperçu</span> | |
| <span class="crumb">Projet</span> | |
| <span class="crumb" id="crumbName">Projet 1</span> | |
| </div> | |
| <span class="pill" id="pillMode">Mode: Preview</span> | |
| </div> | |
| <div class="centerBody"> | |
| <div class="chatArea"> | |
| <div class="chatInner" id="chatInner"> | |
| <!-- Empty state injected --> | |
| </div> | |
| </div> | |
| <div class="composer"> | |
| <button class="iconBtn" id="btnPlus" title="Actions rapides">+</button> | |
| <button class="iconBtn" id="btnAttach" title="Joindre (local)">📎</button> | |
| <input class="composeInput" id="chatInput" placeholder="Écris ici… (ex: “crée une page”, ou colle du code HTML/CSS/JS)" /> | |
| <button class="iconBtn" id="btnMic" title="Micro (dictée)">🎤</button> | |
| <button class="sendBtn" id="btnSend" title="Envoyer">➤</button> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- RIGHT --> | |
| <section class="card" aria-label="Droite"> | |
| <div class="cardHeader"> | |
| <div class="title" style="gap:8px;"><span style="font-weight:900">Aperçu</span></div> | |
| <div class="toolRow"> | |
| <button class="iconBtn" id="btnBack" title="Retour">←</button> | |
| <button class="iconBtn" id="btnForward" title="Avancer">→</button> | |
| <button class="iconBtn" id="btnRefresh" title="Rafraîchir">⟳</button> | |
| <button class="iconBtn" id="btnFull" title="Plein écran aperçu">⤢</button> | |
| </div> | |
| </div> | |
| <div class="rightBody"> | |
| <div class="topTools"> | |
| <div class="tabs"> | |
| <div class="tab active" id="tabCode">Code</div> | |
| <div class="tab" id="tabPreview">Aperçu</div> | |
| </div> | |
| <div class="toolRow"> | |
| <button class="miniBtn primary" id="btnRun">Run</button> | |
| <button class="miniBtn" id="btnSave">Save</button> | |
| <button class="miniBtn" id="btnDownload">Download</button> | |
| <button class="miniBtn danger" id="btnReset">Reset</button> | |
| </div> | |
| </div> | |
| <div class="editorWrap" id="panelCode"> | |
| <div class="editorHeader"> | |
| <span><b>Editor</b> — HTML/CSS/JS (local)</span> | |
| <span class="pill" id="pillSaved">Auto-save</span> | |
| </div> | |
| <textarea class="editor" id="codeEditor" spellcheck="false"></textarea> | |
| </div> | |
| <div class="previewWrap" id="panelPreview" style="display:none"> | |
| <div class="editorHeader"> | |
| <span><b>Preview</b> — iframe sandbox</span> | |
| <span class="pill bad" id="pillPreviewState">Hors ligne</span> | |
| </div> | |
| <div class="previewBox" id="previewBox"> | |
| <!-- Empty preview injected --> | |
| </div> | |
| <div class="statusBar"> | |
| <span id="statusLeft">Statut: Hors ligne</span> | |
| <span id="statusRight">Mode: Preview</span> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| <script> | |
| /* ========================= | |
| Espace Codage — 100% local | |
| - Projets + autosave (localStorage) | |
| - Chat Rosalinda (assistant local) | |
| - Micro dictée (Web Speech API) | |
| - Éditeur code + aperçu iframe sandbox | |
| ========================= */ | |
| (() => { | |
| const $ = (q) => document.querySelector(q); | |
| // --- UI refs | |
| const projectList = $("#projectList"); | |
| const projectSearch = $("#projectSearch"); | |
| const btnNewProject = $("#btnNewProject"); | |
| const chatInner = $("#chatInner"); | |
| const chatInput = $("#chatInput"); | |
| const btnSend = $("#btnSend"); | |
| const btnAttach = $("#btnAttach"); | |
| const btnPlus = $("#btnPlus"); | |
| const btnMic = $("#btnMic"); | |
| const pillProject = $("#pillProject"); | |
| const crumbName = $("#crumbName"); | |
| const tabCode = $("#tabCode"); | |
| const tabPreview = $("#tabPreview"); | |
| const panelCode = $("#panelCode"); | |
| const panelPreview = $("#panelPreview"); | |
| const codeEditor = $("#codeEditor"); | |
| const btnRun = $("#btnRun"); | |
| const btnSave = $("#btnSave"); | |
| const btnDownload = $("#btnDownload"); | |
| const btnReset = $("#btnReset"); | |
| const previewBox = $("#previewBox"); | |
| const pillPreviewState = $("#pillPreviewState"); | |
| const statusLeft = $("#statusLeft"); | |
| const statusRight = $("#statusRight"); | |
| const btnBack = $("#btnBack"); | |
| const btnForward = $("#btnForward"); | |
| const btnRefresh = $("#btnRefresh"); | |
| const btnFull = $("#btnFull"); | |
| // --- Storage keys | |
| const KEY = { | |
| projects: "espaceCodage.projects.v1", | |
| activeId: "espaceCodage.activeId.v1" | |
| }; | |
| // --- Default template | |
| const DEFAULT_CODE = `<!doctype html> | |
| <html lang="fr"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Mon Aperçu</title> | |
| <style> | |
| body{font-family:system-ui,Segoe UI,Roboto,Arial;margin:0;padding:24px;background:#f6f7fb;} | |
| .card{max-width:820px;margin:0 auto;background:#fff;border:1px solid #e7e7ef;border-radius:16px;padding:18px;box-shadow:0 10px 25px rgba(0,0,0,.06);} | |
| h1{margin:0 0 8px 0;} | |
| p{margin:0;color:#444;line-height:1.5;} | |
| .btn{display:inline-block;margin-top:12px;padding:10px 14px;border-radius:12px;border:1px solid #d8d8e6;background:#f2f6ff;cursor:pointer} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <h1>✅ Aperçu OK</h1> | |
| <p>Ce projet tourne en local (iframe sandbox). Modifie le code, puis clique <b>Run</b>.</p> | |
| <button class="btn" onclick="alert('Hello Rosalinda 👋')">Tester</button> | |
| </div> | |
| </body> | |
| </html>`; | |
| // --- Helpers | |
| const nowTime = () => new Date().toLocaleTimeString([], {hour:"2-digit", minute:"2-digit"}); | |
| const escapeHTML = (s) => | |
| s.replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); | |
| function readProjects(){ | |
| try{ | |
| const raw = localStorage.getItem(KEY.projects); | |
| if(!raw) return null; | |
| return JSON.parse(raw); | |
| }catch{ return null; } | |
| } | |
| function writeProjects(projects){ | |
| localStorage.setItem(KEY.projects, JSON.stringify(projects)); | |
| } | |
| function getActiveId(){ | |
| return localStorage.getItem(KEY.activeId); | |
| } | |
| function setActiveId(id){ | |
| localStorage.setItem(KEY.activeId, id); | |
| } | |
| function uid(){ | |
| return "p_" + Math.random().toString(16).slice(2) + Date.now().toString(16); | |
| } | |
| function ensureData(){ | |
| let projects = readProjects(); | |
| if(!projects || !Array.isArray(projects) || projects.length === 0){ | |
| projects = [ | |
| { id: uid(), name:"Projet 1", code: DEFAULT_CODE, chat: [] }, | |
| { id: uid(), name:"Projet 2", code: DEFAULT_CODE, chat: [] }, | |
| ]; | |
| writeProjects(projects); | |
| setActiveId(projects[0].id); | |
| } | |
| if(!getActiveId()){ | |
| setActiveId(projects[0].id); | |
| } | |
| return projects; | |
| } | |
| function getState(){ | |
| const projects = ensureData(); | |
| const activeId = getActiveId(); | |
| const active = projects.find(p => p.id === activeId) || projects[0]; | |
| return { projects, active }; | |
| } | |
| function updateState(mutator){ | |
| const { projects, active } = getState(); | |
| mutator(projects, active); | |
| writeProjects(projects); | |
| } | |
| // --- UI render | |
| function renderProjects(){ | |
| const { projects, active } = getState(); | |
| const q = projectSearch.value.trim().toLowerCase(); | |
| projectList.innerHTML = ""; | |
| projects | |
| .filter(p => p.name.toLowerCase().includes(q)) | |
| .forEach((p, idx) => { | |
| const el = document.createElement("div"); | |
| el.className = "item" + (p.id === active.id ? " active" : ""); | |
| el.innerHTML = ` | |
| <div class="badge"></></div> | |
| <div class="itemText"> | |
| <div class="name">${escapeHTML(p.name)}</div> | |
| <div class="sub">${p.id === active.id ? "Actif" : "Clique pour ouvrir"}</div> | |
| </div> | |
| `; | |
| el.addEventListener("click", () => { | |
| setActiveId(p.id); | |
| loadActiveProject(); | |
| }); | |
| projectList.appendChild(el); | |
| }); | |
| pillProject.textContent = active.name; | |
| crumbName.textContent = active.name; | |
| } | |
| function renderChat(){ | |
| const { active } = getState(); | |
| chatInner.innerHTML = ""; | |
| if(!active.chat || active.chat.length === 0){ | |
| chatInner.innerHTML = ` | |
| <div class="emptyState"> | |
| <div class="emptyIcon"></div> | |
| <div class="emptyTitle">Zone centrale vide</div> | |
| <div class="emptySub">La barre de saisie reste en bas comme demandé</div> | |
| </div> | |
| `; | |
| return; | |
| } | |
| for(const m of active.chat){ | |
| const row = document.createElement("div"); | |
| row.className = "msg"; | |
| const isUser = m.role === "user"; | |
| row.innerHTML = ` | |
| <div class="avatar">${isUser ? "A" : "R"}</div> | |
| <div class="bubble ${isUser ? "user" : ""}"> | |
| <div class="meta"><span>${isUser ? "Amine" : "Rosalinda"}</span><span>•</span><span>${escapeHTML(m.time)}</span></div> | |
| <div class="text">${escapeHTML(m.text)}</div> | |
| ${m.code ? `<div class="codeBlock">${escapeHTML(m.code)}</div>` : ""} | |
| </div> | |
| `; | |
| chatInner.appendChild(row); | |
| } | |
| chatInner.scrollTop = chatInner.scrollHeight; | |
| } | |
| function renderPreviewEmpty(){ | |
| previewBox.innerHTML = ` | |
| <div class="previewEmpty"> | |
| <div class="emptyIcon" style="width:92px;height:92px;border-radius:20px;"></div> | |
| <div class="big">Échec du chargement de l'aperçu</div> | |
| <div style="margin-top:6px;color:rgba(232,238,252,.6);font-size:13px"> | |
| Vérifiez l'URL / le serveur local, puis rafraîchissez | |
| </div> | |
| </div> | |
| `; | |
| pillPreviewState.textContent = "Hors ligne"; | |
| pillPreviewState.classList.remove("ok"); | |
| pillPreviewState.classList.add("bad"); | |
| statusLeft.textContent = "Statut: Hors ligne"; | |
| } | |
| function renderPreviewFromCode(html){ | |
| previewBox.innerHTML = ""; | |
| const frame = document.createElement("iframe"); | |
| // sandbox: allow scripts inside the preview but isolate from parent. | |
| frame.setAttribute("sandbox", "allow-scripts allow-forms allow-modals allow-popups"); | |
| frame.srcdoc = html; | |
| previewBox.appendChild(frame); | |
| pillPreviewState.textContent = "En ligne (local)"; | |
| pillPreviewState.classList.remove("bad"); | |
| pillPreviewState.classList.add("ok"); | |
| statusLeft.textContent = "Statut: En ligne (local)"; | |
| } | |
| function loadActiveProject(){ | |
| const { active } = getState(); | |
| codeEditor.value = active.code || DEFAULT_CODE; | |
| renderProjects(); | |
| renderChat(); | |
| renderPreviewEmpty(); | |
| statusRight.textContent = "Mode: Preview"; | |
| } | |
| // --- Tabs | |
| function setTab(which){ | |
| const isCode = which === "code"; | |
| tabCode.classList.toggle("active", isCode); | |
| tabPreview.classList.toggle("active", !isCode); | |
| panelCode.style.display = isCode ? "flex" : "none"; | |
| panelPreview.style.display = isCode ? "none" : "flex"; | |
| } | |
| tabCode.addEventListener("click", () => setTab("code")); | |
| tabPreview.addEventListener("click", () => setTab("preview")); | |
| // --- Save code | |
| function saveCode(){ | |
| const code = codeEditor.value; | |
| updateState((projects, active) => { | |
| active.code = code; | |
| }); | |
| } | |
| let saveTimer = null; | |
| codeEditor.addEventListener("input", () => { | |
| if(saveTimer) clearTimeout(saveTimer); | |
| saveTimer = setTimeout(() => saveCode(), 250); | |
| }); | |
| btnSave.addEventListener("click", () => { | |
| saveCode(); | |
| toast("✅ Code sauvegardé"); | |
| }); | |
| // --- Run preview | |
| btnRun.addEventListener("click", () => { | |
| saveCode(); | |
| setTab("preview"); | |
| renderPreviewFromCode(codeEditor.value); | |
| }); | |
| // --- Download | |
| btnDownload.addEventListener("click", () => { | |
| saveCode(); | |
| const blob = new Blob([codeEditor.value], {type:"text/html;charset=utf-8"}); | |
| const a = document.createElement("a"); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = (getState().active.name || "projet") + ".html"; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| URL.revokeObjectURL(a.href); | |
| }); | |
| // --- Reset | |
| btnReset.addEventListener("click", () => { | |
| if(!confirm("Reset le code du projet actif ?")) return; | |
| codeEditor.value = DEFAULT_CODE; | |
| saveCode(); | |
| renderPreviewEmpty(); | |
| toast("♻️ Reset OK"); | |
| }); | |
| // --- Quick actions | |
| btnPlus.addEventListener("click", () => { | |
| addAssistantMessage("Actions rapides :\n- Écris “template” pour un modèle\n- Colle du code HTML/CSS/JS\n- Écris “run” pour lancer l’aperçu\n- Écris “aide” pour les commandes"); | |
| }); | |
| // Attach (local file import) | |
| btnAttach.addEventListener("click", async () => { | |
| const input = document.createElement("input"); | |
| input.type = "file"; | |
| input.accept = ".html,.txt"; | |
| input.onchange = async () => { | |
| const file = input.files?.[0]; | |
| if(!file) return; | |
| const text = await file.text(); | |
| codeEditor.value = text; | |
| saveCode(); | |
| addUserMessage(`J’ai importé un fichier: ${file.name}`); | |
| addAssistantMessage("✅ Fichier importé dans l’éditeur. Clique Run pour voir l’aperçu."); | |
| }; | |
| input.click(); | |
| }); | |
| // --- Projects | |
| btnNewProject.addEventListener("click", () => { | |
| const name = prompt("Nom du nouveau projet ?", "Nouveau Projet"); | |
| if(!name) return; | |
| updateState((projects) => { | |
| const p = { id: uid(), name, code: DEFAULT_CODE, chat: [] }; | |
| projects.unshift(p); | |
| setActiveId(p.id); | |
| }); | |
| loadActiveProject(); | |
| }); | |
| projectSearch.addEventListener("input", renderProjects); | |
| // --- Chat helpers | |
| function pushChat(role, text, code=null){ | |
| updateState((projects, active) => { | |
| active.chat = active.chat || []; | |
| active.chat.push({ role, text, time: nowTime(), code }); | |
| }); | |
| renderChat(); | |
| } | |
| function addUserMessage(text){ | |
| pushChat("user", text); | |
| } | |
| function addAssistantMessage(text, code=null){ | |
| pushChat("assistant", text, code); | |
| } | |
| // --- Rosalinda (assistant local simple) | |
| function extractCodeFromMessage(msg){ | |
| // support ```...``` blocks | |
| const m = msg.match(/```[\s\S]*?```/); | |
| if(!m) return null; | |
| return m[0].replace(/^```[a-zA-Z]*\n?/, "").replace(/```$/, ""); | |
| } | |
| function rosalindaReply(userText){ | |
| const t = userText.trim().toLowerCase(); | |
| if(t === "aide" || t === "help"){ | |
| return { | |
| text: | |
| `Commandes rapides : | |
| - template → met un template propre dans l’éditeur | |
| - run → lance l’aperçu | |
| - reset → remet le template par défaut | |
| - code: ... → met ton code directement dans l’éditeur | |
| - status → affiche l’état | |
| Tu peux aussi coller du code entre \`\`\` \`\`\`.` | |
| }; | |
| } | |
| if(t === "template"){ | |
| return { text:"✅ Template chargé dans l’éditeur. Tu peux modifier puis cliquer Run.", action:"template" }; | |
| } | |
| if(t === "run"){ | |
| return { text:"▶️ Je lance l’aperçu.", action:"run" }; | |
| } | |
| if(t === "reset"){ | |
| return { text:"♻️ Reset du code.", action:"reset" }; | |
| } | |
| if(t === "status"){ | |
| return { text:`Statut local : ${navigator.onLine ? "Internet dispo (optionnel)" : "Hors ligne (OK)"}.\nAperçu : fonctionne via iframe sandbox.\nSauvegarde : localStorage par projet.` }; | |
| } | |
| // If user pasted code block | |
| const code = extractCodeFromMessage(userText); | |
| if(code){ | |
| return { text:"✅ Code détecté. Je l’ai mis dans l’éditeur. Clique Run pour vérifier.", action:"setCode", code }; | |
| } | |
| // "code:" prefix | |
| if(t.startsWith("code:")){ | |
| const c = userText.slice(5).trim(); | |
| if(c) return { text:"✅ Code reçu. Je l’ai mis dans l’éditeur.", action:"setCode", code:c }; | |
| } | |
| // Simple smart hinting (local) | |
| if(t.includes("image") || t.includes("vidéo") || t.includes("video")){ | |
| return { | |
| text: | |
| `Je peux préparer l’interface et les boutons localement ✅ | |
| Mais pour générer VRAIMENT des images/vidéos, il faut un moteur local (modèle) installé sur ton PC. | |
| Si tu veux, on fera une version “plug-in” : bouton Image/Vidéo → envoie vers ton moteur local plus tard. | |
| Pour l’instant, l’éditeur + aperçu + micro + projets tournent parfaitement sans clé API.` | |
| }; | |
| } | |
| // Default response | |
| return { | |
| text: | |
| `OK. Dis-moi ce que tu veux construire : | |
| - une landing page | |
| - un dashboard | |
| - un formulaire | |
| - un thème e-commerce | |
| Ou colle ton code, je te le nettoie et je le rends propre.`, | |
| }; | |
| } | |
| function handleSend(){ | |
| const text = chatInput.value.trim(); | |
| if(!text) return; | |
| chatInput.value = ""; | |
| addUserMessage(text); | |
| const r = rosalindaReply(text); | |
| addAssistantMessage(r.text); | |
| if(r.action === "template"){ | |
| codeEditor.value = DEFAULT_CODE; | |
| saveCode(); | |
| } else if(r.action === "setCode"){ | |
| codeEditor.value = r.code || DEFAULT_CODE; | |
| saveCode(); | |
| } else if(r.action === "reset"){ | |
| codeEditor.value = DEFAULT_CODE; | |
| saveCode(); | |
| renderPreviewEmpty(); | |
| } else if(r.action === "run"){ | |
| saveCode(); | |
| setTab("preview"); | |
| renderPreviewFromCode(codeEditor.value); | |
| } | |
| } | |
| btnSend.addEventListener("click", handleSend); | |
| chatInput.addEventListener("keydown", (e) => { | |
| if(e.key === "Enter") handleSend(); | |
| }); | |
| // --- Micro (Web Speech API) | |
| let recognition = null; | |
| let listening = false; | |
| function initSpeech(){ | |
| const SR = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if(!SR) return null; | |
| const rec = new SR(); | |
| rec.lang = "fr-FR"; | |
| rec.interimResults = true; | |
| rec.continuous = false; | |
| return rec; | |
| } | |
| function setMicUI(on){ | |
| listening = on; | |
| btnMic.classList.toggle("micOn", on); | |
| btnMic.textContent = on ? "🟢" : "🎤"; | |
| } | |
| btnMic.addEventListener("click", () => { | |
| if(!recognition) recognition = initSpeech(); | |
| if(!recognition){ | |
| addAssistantMessage("⚠️ Dictée non supportée sur ce navigateur. Essaye Chrome Desktop."); | |
| return; | |
| } | |
| if(listening){ | |
| recognition.stop(); | |
| setMicUI(false); | |
| return; | |
| } | |
| let finalText = ""; | |
| recognition.onresult = (event) => { | |
| let interim = ""; | |
| for(let i=event.resultIndex; i<event.results.length; i++){ | |
| const chunk = event.results[i][0].transcript; | |
| if(event.results[i].isFinal) finalText += chunk; | |
| else interim += chunk; | |
| } | |
| chatInput.value = (finalText + interim).trim(); | |
| }; | |
| recognition.onerror = () => { | |
| setMicUI(false); | |
| addAssistantMessage("⚠️ Micro: erreur. Vérifie les autorisations micro du site."); | |
| }; | |
| recognition.onend = () => { | |
| setMicUI(false); | |
| if(chatInput.value.trim()){ | |
| // auto-send after dictation ends | |
| handleSend(); | |
| } | |
| }; | |
| setMicUI(true); | |
| recognition.start(); | |
| }); | |
| // --- Preview nav buttons (simple) | |
| btnRefresh.addEventListener("click", () => { | |
| const iframe = previewBox.querySelector("iframe"); | |
| if(iframe){ | |
| // reload srcdoc by resetting it | |
| const html = codeEditor.value; | |
| iframe.srcdoc = html; | |
| toast("⟳ Aperçu rafraîchi"); | |
| }else{ | |
| renderPreviewEmpty(); | |
| } | |
| }); | |
| btnBack.addEventListener("click", () => toast("↩︎ Historique non géré (local iframe).")); | |
| btnForward.addEventListener("click", () => toast("↪︎ Historique non géré (local iframe).")); | |
| btnFull.addEventListener("click", () => { | |
| const iframe = previewBox.querySelector("iframe"); | |
| if(!iframe){ | |
| toast("Aucun aperçu à afficher."); | |
| return; | |
| } | |
| // Open preview in new tab (data URL) | |
| const blob = new Blob([codeEditor.value], {type:"text/html;charset=utf-8"}); | |
| const url = URL.createObjectURL(blob); | |
| window.open(url, "_blank", "noopener,noreferrer"); | |
| setTimeout(() => URL.revokeObjectURL(url), 30_000); | |
| }); | |
| // --- Toast | |
| function toast(msg){ | |
| const t = document.createElement("div"); | |
| t.textContent = msg; | |
| t.style.position="fixed"; | |
| t.style.left="50%"; | |
| t.style.bottom="18px"; | |
| t.style.transform="translateX(-50%)"; | |
| t.style.padding="10px 12px"; | |
| t.style.borderRadius="14px"; | |
| t.style.border="1px solid rgba(255,255,255,.12)"; | |
| t.style.background="rgba(0,0,0,.55)"; | |
| t.style.backdropFilter="blur(8px)"; | |
| t.style.color="rgba(232,238,252,.95)"; | |
| t.style.fontSize="13px"; | |
| t.style.boxShadow="0 10px 24px rgba(0,0,0,.35)"; | |
| t.style.zIndex=9999; | |
| document.body.appendChild(t); | |
| setTimeout(()=>{ t.style.opacity="0"; t.style.transition="opacity .2s"; }, 1100); | |
| setTimeout(()=> t.remove(), 1400); | |
| } | |
| // --- Boot | |
| loadActiveProject(); | |
| setTab("code"); | |
| // Seed a first assistant message (only once) | |
| const seeded = localStorage.getItem("espaceCodage.seeded.v1"); | |
| if(!seeded){ | |
| addAssistantMessage("👋 Bonjour Amine. Je suis Rosalinda (local). Tu peux écrire “aide”, “template”, ou coller du code."); | |
| localStorage.setItem("espaceCodage.seeded.v1", "1"); | |
| } | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |