Entropyk/ui/index.html

382 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Entropyk - Test UI</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; font-family: 'Segoe UI', system-ui, sans-serif; background: #1a1d23; color: #e4e6eb; min-height: 100vh; }
.layout { display: grid; grid-template-columns: 180px 1fr 320px; grid-template-rows: 48px 1fr 140px; height: 100vh; }
header { grid-column: 1 / -1; background: #252830; padding: 0 20px; display: flex; align-items: center; border-bottom: 1px solid #3a3f4b; }
header h1 { margin: 0; font-size: 1.2rem; font-weight: 600; }
.palette { background: #252830; padding: 16px; border-right: 1px solid #3a3f4b; overflow-y: auto; }
.palette h3 { margin: 0 0 12px 0; font-size: 0.75rem; text-transform: uppercase; color: #8b919e; }
.palette-item { padding: 10px 12px; margin-bottom: 8px; background: #2d323d; border-radius: 8px; cursor: grab; font-size: 0.9rem; border: 2px solid transparent; transition: all 0.15s; }
.palette-item:hover { background: #363c48; border-color: #5a9cf5; }
.palette-item:active { cursor: grabbing; }
.palette-item.compressor { border-left: 4px solid #f59e5b; }
.palette-item.pump { border-left: 4px solid #5a9cf5; }
.palette-item.pipe { border-left: 4px solid #6ee7b7; }
.palette-item.valve { border-left: 4px solid #c4b5fd; }
.canvas { background: linear-gradient(rgba(58,63,75,0.3) 1px, transparent 1px), linear-gradient(90deg, rgba(58,63,75,0.3) 1px, transparent 1px); background-size: 24px 24px; background-color: #1e2229; position: relative; overflow: hidden; }
.canvas .component { position: absolute; min-width: 120px; padding: 12px 16px; background: #2d323d; border-radius: 10px; cursor: move; user-select: none; border: 2px solid transparent; box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.canvas .component:hover { border-color: #5a9cf5; }
.canvas .component.selected { border-color: #5a9cf5; box-shadow: 0 0 0 2px rgba(90,156,245,0.3); }
.canvas .component .label { font-weight: 600; font-size: 0.85rem; }
.canvas .component .type { font-size: 0.7rem; color: #8b919e; margin-top: 4px; }
.canvas .component .ports { display: flex; justify-content: space-between; margin-top: 8px; gap: 8px; }
.canvas .component .port { width: 12px; height: 12px; border-radius: 50%; background: #5a9cf5; border: 2px solid #3a3f4b; cursor: crosshair; flex-shrink: 0; }
.canvas .component .port.in { background: #6ee7b7; }
.canvas .component .port.out { background: #f59e5b; }
.canvas .component .port.connected { border-color: #5a9cf5; box-shadow: 0 0 6px #5a9cf5; }
.canvas .component.compressor { border-left: 4px solid #f59e5b; }
.canvas .component.pump { border-left: 4px solid #5a9cf5; }
.canvas .component.pipe { border-left: 4px solid #6ee7b7; }
.canvas .component.valve { border-left: 4px solid #c4b5fd; }
.config-panel { background: #252830; padding: 16px; border-left: 1px solid #3a3f4b; overflow-y: auto; }
.config-panel h3 { margin: 0 0 12px 0; font-size: 0.75rem; text-transform: uppercase; color: #8b919e; }
.config-panel .empty { color: #6b7280; font-size: 0.9rem; }
.config-panel .section { margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #3a3f4b; }
.config-panel .section:last-of-type { border-bottom: none; }
.config-panel input, .config-panel select, .config-panel textarea { width: 100%; padding: 8px 10px; margin-bottom: 8px; background: #1e2229; border: 1px solid #3a3f4b; border-radius: 6px; color: #e4e6eb; font-size: 0.9rem; }
.config-panel input:focus, .config-panel select:focus { outline: none; border-color: #5a9cf5; }
.config-panel label { display: block; font-size: 0.75rem; color: #8b919e; margin-bottom: 4px; }
.footer { grid-column: 1 / -1; background: #252830; padding: 12px 20px; border-top: 1px solid #3a3f4b; display: flex; align-items: center; gap: 12px; }
.footer button { padding: 8px 16px; background: #5a9cf5; border: none; border-radius: 6px; color: white; font-weight: 600; cursor: pointer; font-size: 0.9rem; }
.footer button:hover { background: #4a8ce5; }
.footer button.secondary { background: #3a3f4b; color: #e4e6eb; }
.footer button.secondary:hover { background: #4a4f5b; }
.footer button.danger { background: #ef4444; }
.footer button.danger:hover { background: #dc2626; }
#json-output { flex: 1; padding: 8px 12px; background: #1e2229; border: 1px solid #3a3f4b; border-radius: 6px; color: #8b919e; font-family: monospace; font-size: 0.75rem; resize: none; min-height: 80px; }
.connection-mode { background: #f59e5b !important; }
</style>
</head>
<body>
<div class="layout">
<header><h1>🔧 Entropyk - Test UI</h1></header>
<aside class="palette">
<h3>Composants</h3>
<div class="palette-item compressor" data-type="compressor" draggable="true">Compresseur</div>
<div class="palette-item pump" data-type="pump" draggable="true">Pompe</div>
<div class="palette-item pipe" data-type="pipe" draggable="true">Conduite</div>
<div class="palette-item valve" data-type="valve" draggable="true">Détendeur</div>
</aside>
<main class="canvas" id="canvas">
<svg id="connections-svg" style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0"></svg>
<div id="components-container" style="position:absolute;top:0;left:0;width:100%;height:100%;z-index:1"></div>
</main>
<aside class="config-panel">
<h3>Configuration</h3>
<div id="config-content"><p class="empty">Sélectionnez un composant</p></div>
</aside>
<footer class="footer">
<button onclick="calculate()">Calculer</button>
<button class="secondary" id="btn-connect" onclick="toggleConnectMode()">Relier (sortie → entrée)</button>
<button class="secondary" onclick="exportJson()">Exporter JSON</button>
<button class="secondary" onclick="clearAll()">Effacer tout</button>
<textarea id="json-output" readonly placeholder="Résultats..."></textarea>
</footer>
</div>
<script>
let components = [];
let connections = [];
let selectedId = null;
let idCounter = 0;
let dragOffset = { x: 0, y: 0 };
let connectMode = false;
let connectFrom = null;
const canvas = document.getElementById('canvas');
const componentsContainer = document.getElementById('components-container');
const configContent = document.getElementById('config-content');
const connectionsSvg = document.getElementById('connections-svg');
function getPorts(type) {
if (type === 'compressor') return [{ id: 'suction', name: 'Aspiration', dir: 'in' }, { id: 'discharge', name: 'Refoulement', dir: 'out' }];
return [{ id: 'inlet', name: 'Entrée', dir: 'in' }, { id: 'outlet', name: 'Sortie', dir: 'out' }];
}
function getDefaultConfig(type) {
switch (type) {
case 'compressor': return { fluid: 'R134a', mass_coeffs: '0.05,0.001,0.0005,0.00001', power_coeffs: '1000,50,30,0.5' };
case 'pump': return { fluid: 'Water', head_coeffs: '30,-10,-50', eff_coeffs: '0.5,0.3,-0.5', density: 1000 };
case 'pipe': return { fluid: 'Water', length: 10, diameter: 0.022, roughness: 0.0000015 };
case 'valve': return { fluid: 'R134a', opening: 1 };
default: return {};
}
}
document.querySelectorAll('.palette-item').forEach(item => {
item.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('type', item.dataset.type);
e.dataTransfer.effectAllowed = 'copy';
});
});
canvas.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
canvas.addEventListener('drop', (e) => {
e.preventDefault();
const type = e.dataTransfer.getData('type');
if (!type) return;
const rect = canvas.getBoundingClientRect();
addComponent(type, e.clientX - rect.left - 60, e.clientY - rect.top - 40);
});
function addComponent(type, x, y) {
idCounter++;
const id = `comp_${idCounter}`;
const ports = getPorts(type);
components.push({
id, type, x, y,
label: `${type}_${idCounter}`,
config: getDefaultConfig(type),
ports: Object.fromEntries(ports.map(p => [p.id, null]))
});
renderAll();
selectComponent(id);
}
function renderComponent(comp) {
let el = document.getElementById(comp.id);
if (!el) {
el = document.createElement('div');
el.className = `component ${comp.type}`;
el.id = comp.id;
el.innerHTML = `<div class="label"></div><div class="type"></div><div class="ports"></div>`;
el.addEventListener('mousedown', (e) => {
if (e.target.closest('.port')) return;
selectComponent(comp.id);
dragOffset = { x: e.clientX - comp.x, y: e.clientY - comp.y };
});
const portsDiv = el.querySelector('.ports');
getPorts(comp.type).forEach(p => {
const portEl = document.createElement('div');
portEl.className = `port ${p.dir}`;
portEl.title = p.name;
portEl.dataset.port = p.id;
portEl.addEventListener('mousedown', (e) => { e.stopPropagation(); portClick(comp.id, p.id, p.dir); });
portsDiv.appendChild(portEl);
});
componentsContainer.appendChild(el);
}
el.style.left = comp.x + 'px';
el.style.top = comp.y + 'px';
el.querySelector('.label').textContent = comp.label;
el.querySelector('.type').textContent = comp.type;
el.querySelectorAll('.port').forEach((p, i) => {
const portId = getPorts(comp.type)[i].id;
p.classList.toggle('connected', !!comp.ports[portId]);
});
}
function portClick(compId, portId, dir) {
if (!connectMode) return;
const comp = components.find(c => c.id === compId);
if (!comp) return;
if (!connectFrom) {
if (dir !== 'out') return;
connectFrom = { compId, portId };
document.getElementById('btn-connect').classList.add('connection-mode');
return;
}
if (dir !== 'in') { connectFrom = null; document.getElementById('btn-connect').classList.remove('connection-mode'); return; }
const fromComp = components.find(c => c.id === connectFrom.compId);
if (!fromComp || fromComp.id === compId) { connectFrom = null; document.getElementById('btn-connect').classList.remove('connection-mode'); return; }
connections = connections.filter(c => !(c.from.compId === connectFrom.compId && c.from.portId === connectFrom.portId) && !(c.to.compId === compId && c.to.portId === portId));
const oldTo = comp.ports[portId];
if (oldTo) {
const oldComp = components.find(c => c.id === oldTo.compId);
if (oldComp) oldComp.ports[oldTo.portId] = null;
}
connections.push({ from: connectFrom, to: { compId, portId } });
fromComp.ports[connectFrom.portId] = { compId, portId };
comp.ports[portId] = { compId: connectFrom.compId, portId: connectFrom.portId };
connectFrom = null;
document.getElementById('btn-connect').classList.remove('connection-mode');
renderAll();
}
function toggleConnectMode() {
connectMode = !connectMode;
connectFrom = null;
document.getElementById('btn-connect').classList.toggle('connection-mode', connectMode);
document.getElementById('btn-connect').textContent = connectMode ? 'Annuler' : 'Relier (sortie → entrée)';
}
function renderConnections() {
let path = '';
connections.forEach(c => {
const fromComp = components.find(x => x.id === c.from.compId);
const toComp = components.find(x => x.id === c.to.compId);
if (!fromComp || !toComp) return;
const fromEl = document.getElementById(fromComp.id);
const toEl = document.getElementById(toComp.id);
if (!fromEl || !toEl) return;
const fromPort = fromEl.querySelector(`.port.out, .port[data-port="${c.from.portId}"]`);
const toPort = toEl.querySelector(`.port.in, .port[data-port="${c.to.portId}"]`);
if (!fromPort || !toPort) return;
const r = canvas.getBoundingClientRect();
const fromRect = fromPort.getBoundingClientRect();
const toRect = toPort.getBoundingClientRect();
const x1 = fromRect.left - r.left + fromRect.width / 2;
const y1 = fromRect.top - r.top + fromRect.height / 2;
const x2 = toRect.left - r.left + toRect.width / 2;
const y2 = toRect.top - r.top + toRect.height / 2;
path += `M ${x1} ${y1} C ${x1 + 80} ${y1}, ${x2 - 80} ${y2}, ${x2} ${y2} `;
});
connectionsSvg.innerHTML = `<path d="${path}" fill="none" stroke="#5a9cf5" stroke-width="2"/>`;
}
function renderAll() {
components.forEach(renderComponent);
renderConnections();
}
function selectComponent(id) {
selectedId = id;
document.querySelectorAll('.component').forEach(el => el.classList.toggle('selected', el.id === id));
renderConfigPanel();
}
canvas.addEventListener('mousemove', (e) => {
if (selectedId && e.buttons === 1 && !e.target.closest('.port')) {
const comp = components.find(c => c.id === selectedId);
if (comp) {
const rect = canvas.getBoundingClientRect();
comp.x = Math.max(0, e.clientX - rect.left - dragOffset.x);
comp.y = Math.max(0, e.clientY - rect.top - dragOffset.y);
renderAll();
}
}
});
function escapeAttr(v) {
if (v === undefined || v === null) return '';
return String(v).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
}
function renderConfigPanel() {
if (!selectedId) {
configContent.innerHTML = '<p class="empty">Sélectionnez un composant pour configurer</p>';
return;
}
const comp = components.find(c => c.id === selectedId);
if (!comp) return;
let html = `<div class="section"><label>Nom</label><input type="text" value="${escapeAttr(comp.label)}" oninput="updateConfig('${comp.id}','label',this.value);updateLabel('${comp.id}',this.value)"></div>`;
html += `<div class="section"><label>Fluide</label><select onchange="updateConfig('${comp.id}','fluid',this.value)">`;
['R134a', 'R410A', 'Water', 'Air'].forEach(f => {
html += `<option value="${f}" ${comp.config.fluid === f ? 'selected' : ''}>${f}</option>`;
});
html += '</select></div>';
html += '<div class="section"><label>Ports (connexions)</label>';
getPorts(comp.type).forEach(p => {
const conn = comp.ports[p.id];
const label = conn ? components.find(c => c.id === conn.compId)?.label || conn.compId : '— Non connecté';
html += `<div style="margin-bottom:6px;font-size:0.8rem"><b>${p.name}</b>: ${label}</div>`;
});
html += '<small style="color:#8b919e">Mode "Relier" pour connecter sortie→entrée</small></div>';
if (comp.type === 'compressor') {
html += `<div class="section"><label>Coeffs débit (a00,a10,a01,a11)</label><input type="text" value="${escapeAttr(comp.config.mass_coeffs)}" oninput="updateConfig('${comp.id}','mass_coeffs',this.value)" placeholder="0.05,0.001,0.0005,0.00001"></div>`;
html += `<div class="section"><label>Coeffs puissance (b00,b10,b01,b11)</label><input type="text" value="${escapeAttr(comp.config.power_coeffs)}" oninput="updateConfig('${comp.id}','power_coeffs',this.value)" placeholder="1000,50,30,0.5"></div>`;
} else if (comp.type === 'pump') {
html += `<div class="section"><label>Hauteur H (a0,a1,a2)</label><input type="text" value="${escapeAttr(comp.config.head_coeffs)}" oninput="updateConfig('${comp.id}','head_coeffs',this.value)" placeholder="30,-10,-50"></div>`;
html += `<div class="section"><label>Rendement η (b0,b1,b2)</label><input type="text" value="${escapeAttr(comp.config.eff_coeffs)}" oninput="updateConfig('${comp.id}','eff_coeffs',this.value)" placeholder="0.5,0.3,-0.5"></div>`;
html += `<div class="section"><label>Densité (kg/m³)</label><input type="number" value="${escapeAttr(comp.config.density)}" oninput="updateConfig('${comp.id}','density',parseFloat(this.value)||0)" min="1" step="1"></div>`;
} else if (comp.type === 'pipe') {
html += `<div class="section"><label>Longueur (m)</label><input type="number" value="${escapeAttr(comp.config.length)}" oninput="updateConfig('${comp.id}','length',parseFloat(this.value)||0)" min="0.1" step="0.1"></div>`;
html += `<div class="section"><label>Diamètre (m)</label><input type="number" value="${escapeAttr(comp.config.diameter)}" oninput="updateConfig('${comp.id}','diameter',parseFloat(this.value)||0)" min="0.001" step="0.001"></div>`;
html += `<div class="section"><label>Rugosité (m)</label><input type="number" value="${escapeAttr(comp.config.roughness)}" oninput="updateConfig('${comp.id}','roughness',parseFloat(this.value)||0)" min="0" step="0.0000001"></div>`;
} else if (comp.type === 'valve') {
html += `<div class="section"><label>Ouverture (0-1)</label><input type="number" value="${escapeAttr(comp.config.opening)}" oninput="updateConfig('${comp.id}','opening',parseFloat(this.value)||0)" min="0" max="1" step="0.1"></div>`;
}
html += `<div class="section"><button class="danger" onclick="removeComponent('${comp.id}')">Supprimer</button></div>`;
configContent.innerHTML = html;
}
function updateLabel(id, value) {
const comp = components.find(c => c.id === id);
if (comp) { comp.label = value; renderComponent(comp); }
}
function updateConfig(id, key, value) {
const comp = components.find(c => c.id === id);
if (!comp) return;
if (key === 'label') comp.label = value;
else if (comp.config) comp.config[key] = value;
}
function removeComponent(id) {
connections = connections.filter(c => c.from.compId !== id && c.to.compId !== id);
components = components.filter(c => c.id !== id);
components.forEach(c => { Object.keys(c.ports || {}).forEach(k => { if (c.ports[k]?.compId === id) c.ports[k] = null; }); });
document.getElementById(id)?.remove();
if (selectedId === id) { selectedId = null; renderConfigPanel(); }
renderConnections();
}
async function calculate() {
const config = {
components: components.map(c => ({
id: c.id, type: c.type, label: c.label,
config: { ...c.config, fluid: c.config.fluid || 'R134a' },
ports: c.ports,
connections: connections.filter(x => x.from.compId === c.id || x.to.compId === c.id)
}))
};
const out = document.getElementById('json-output');
if (components.length === 0) { out.value = 'Ajoutez des composants puis cliquez Calculer.'; return; }
try {
const r = await fetch('/api/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ components: config.components.map(c => ({ id: c.id, type: c.type, label: c.label, config: c.config })) }) });
const data = await r.json();
let text = '';
for (const res of data.results) {
text += `\n=== ${res.label} (${res.type}) ===\n`;
if (res.error) text += 'Erreur: ' + res.error + '\n';
else text += JSON.stringify(res.results, null, 2) + '\n';
}
out.value = text.trim() || JSON.stringify(data, null, 2);
} catch (e) {
out.value = 'Erreur: ' + e.message + '\n\nLancez: cargo run -p entropyk-demo --bin ui-server';
}
}
function exportJson() {
document.getElementById('json-output').value = JSON.stringify({
components: components.map(c => ({ ...c, config: c.config })),
connections
}, null, 2);
}
function clearAll() {
components = [];
connections = [];
selectedId = null;
document.querySelectorAll('#components-container .component').forEach(el => el.remove());
renderConfigPanel();
renderConnections();
document.getElementById('json-output').value = '';
}
canvas.addEventListener('mousedown', (e) => {
if (!e.target.closest('.component') && !e.target.closest('.config-panel')) {
selectedId = null;
document.querySelectorAll('.component').forEach(el => el.classList.remove('selected'));
renderConfigPanel();
}
});
</script>
</body>
</html>