code-whisperer-haven / index.html
Abmacode12's picture
Manual changes saved
4c3d8b8 verified
<!DOCTYPE html>
<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><!doctype 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) !important; background: rgba(46,229,157,.14) !important; }
/* 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) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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">&lt;/&gt;</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>