carbono / index.html
appvoid's picture
Update index.html
bf15f41 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Carbono UI — Minimal Mono</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root{
--fg:#fff;--bg:#000;--muted:#8a8a8a;--panel:#0e0e0e;--line:#222;--ink:#111;--ink2:#1a1a1a;
--radius:6px;--pad:10px;--fs:12px;--lh:1.25;--gap:10px;
--base-w:1200; /* logical pixels for layout before scaling */
--base-h:680;
}
/* reset */
*{box-sizing:border-box;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
html,body{height:100%;background:var(--bg);color:var(--fg);margin:0}
a{color:var(--fg);text-decoration:none;border-bottom:1px solid transparent}
a:hover{border-bottom-color:var(--fg)}
/* fit-to-viewport wrapper (no scroll) */
body{display:flex;align-items:center;justify-content:center;overflow:hidden}
#fit{
width:calc(var(--base-w)*1px);
height:calc(var(--base-h)*1px);
transform-origin: center; /* <<< FIX: Was 'top left', now it scales from the center */
display:flex;flex-direction:column;gap:var(--gap);
font:500 var(--fs)/var(--lh) ui-monospace,SFMono-Regular,Menlo,Consolas,Monaco,monospace;
letter-spacing:.1px;font-variant-numeric:tabular-nums;
}
/* top bar */
.top{
display:grid;grid-template-columns:1fr auto auto;gap:var(--gap);
align-items:center;padding:6px 8px;border:1px solid var(--line);border-radius:var(--radius);background:var(--panel)
}
.brand{text-transform:uppercase;letter-spacing:.5px;font-weight:700}
.muted{color:var(--muted)}
.chip{background:var(--fg);color:var(--bg);padding:2px 6px;border-radius:999px;cursor:pointer;user-select:none}
/* layout grid */
.grid{
display:grid;gap:var(--gap);
grid-template-columns: 3.5fr 3.5fr 3fr; /* L / M / R */
grid-template-rows: 1fr;
height:100%;
}
.panel{
display:flex;flex-direction:column;gap:var(--gap);
border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(180deg,var(--panel),var(--ink));
padding:var(--pad);
}
.block{border:1px solid var(--line);border-radius:var(--radius);background:var(--ink2);padding:8px}
.title{
font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin:0 0 6px 0;
padding-bottom:6px;border-bottom:1px solid var(--line)
}
/* compact form */
label{display:block;margin-bottom:4px;color:var(--muted)}
.row{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--gap)}
.input,textarea,select{
width:100%;background:#141414;border:1px solid var(--line);color:var(--fg);
padding:6px;border-radius:4px;outline:none;transition:border-color .15s ease;
}
.input:focus,textarea:focus,select:focus{border-color:#fff}
textarea{resize:none}
/* controls */
.btns{display:flex;gap:6px;flex-wrap:wrap}
button{
height:26px;line-height:24px;padding:0 10px;border-radius:4px;border:1px solid #fff;background:#fff;color:#000;
cursor:pointer;transition:filter .15s ease
}
button:hover{filter:brightness(.9)}
button:disabled{opacity:.6;cursor:not-allowed;filter:none}
/* canvases */
.canvas-wrap{height:170px;border:1px solid var(--line);border-radius:4px;position:relative;background:#0a0a0a}
canvas{position:absolute;inset:0;width:100%;height:100%}
/* progress bar */
.bar{height:4px;border-radius:999px;background:#161616;overflow:hidden}
.bar>i{display:block;height:100%;width:0;background:#fff;transition:width .2s linear}
/* small text blocks */
.hint{color:var(--muted);margin:4px 0 0 0}
.stat{display:grid;grid-template-columns:auto 1fr;gap:6px 10px}
.stat b{color:#fff}
/* two-up prediction area */
.pred{display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center}
/* hide scrollbars anywhere just in case */
.panel,.block{overflow:hidden}
@media (max-width:900px){
/* fallback: stack but still scaled to fit by transformer */
.grid{grid-template-columns:1fr}
}
@media (prefers-reduced-motion:reduce){
*{animation:none!important;transition:none!important}
}
</style>
</head>
<body>
<div id="fit">
<div class="top">
<div class="brand">Carbono Playground</div>
<div class="muted">Learning. Simple. Visual.</div>
<span id="loadDataBtn" class="chip">Load sample</span>
</div>
<div class="grid">
<!-- LEFT: Model Settings -->
<section class="panel" aria-label="Model Settings">
<h4 class="title">Model</h4>
<div class="block">
<label>Training Set</label>
<textarea id="trainingData" rows="3" placeholder="1,1,1,0&#10;1,0,1,0&#10;0,1,0,1"></textarea>
<div class="hint">Last number is target.</div>
</div>
<div class="row">
<div class="block">
<label>Epochs</label>
<input class="input" type="number" id="epochs" value="50" />
</div>
<div class="block">
<label>LR</label>
<input class="input" type="number" id="learningRate" value="0.1" step="0.001" />
</div>
<div class="block">
<label>Batch</label>
<input class="input" type="number" id="batchSize" value="8" />
</div>
<div class="block">
<label>Hidden</label>
<input class="input" type="number" id="numHiddenLayers" value="1" min="1" max="4" />
</div>
</div>
<div id="hiddenLayersConfig" class="block"></div>
<div class="block">
<label>Validation Set</label>
<textarea id="testData" rows="2" placeholder="0,0,0,1"></textarea>
</div>
</section>
<!-- MIDDLE: Training / Viz -->
<section class="panel" aria-label="Training & Visualization">
<h4 class="title">Training</h4>
<div class="canvas-wrap"><canvas id="lossGraph"></canvas></div>
<div class="hint">Loss: white = train, gray = val.</div>
<div class="bar"><i id="epochBar"></i></div>
<div id="stats" class="block stat"></div>
<h4 class="title">Visualization</h4>
<div class="canvas-wrap"><canvas id="networkGraph"></canvas></div>
<div class="hint">Internal representation.</div>
</section>
<!-- RIGHT: Control / Predict -->
<section class="panel" aria-label="Control">
<h4 class="title">Control</h4>
<div class="block btns">
<button id="trainButton">Train</button>
<button id="saveButton">Save</button>
<button id="loadButton">Load</button>
</div>
<h4 class="title">Predict</h4>
<div class="block pred">
<input class="input" type="text" id="predictionInput" placeholder="0.4, 0.2, 0.6" />
<button id="predictButton">Predict</button>
</div>
<div id="predictionResult" class="block"></div>
<div class="block">
<div class="hint">Repo: <a href="https://github.com/appvoid/carbono" target="_blank" rel="noopener">github/appvoid/carbono</a></div>
</div>
</section>
</div>
</div>
<script>
/* --------- Fit-to-viewport scaling (no scroll) --------- */
(function(){
const baseW = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--base-w'),10);
const baseH = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--base-h'),10);
const fitEl = document.getElementById('fit');
function fit(){ const s = Math.min(window.innerWidth/baseW, window.innerHeight/baseH); fitEl.style.transform = `scale(${s})`; }
window.addEventListener('resize', fit, {passive:true}); fit();
})();
/* ---------------- Carbono micro-lib (unchanged API; minor fixes) ---------------- */
class carbono {
constructor(debug = true) { this.layers=[]; this.weights=[]; this.biases=[]; this.activations=[]; this.details={}; this.debug=debug; }
layer(inputSize, outputSize, activation='tanh'){
this.layers.push({inputSize,outputSize,activation});
if(this.weights.length>0){
const lastOut = this.layers[this.layers.length-2].outputSize;
if(inputSize!==lastOut) throw new Error('Input size must match previous layer output size.');
}
const W=[]; for(let i=0;i<outputSize;i++){ const row=[]; for(let j=0;j<inputSize;j++){ row.push((Math.random()-0.5)*2*Math.sqrt(6/(inputSize+outputSize))); } W.push(row); }
this.weights.push(W); this.biases.push(Array(outputSize).fill(0.01)); this.activations.push(activation);
}
activationFunction(x,a){ switch(a){case 'tanh':return Math.tanh(x);case 'sigmoid':return 1/(1+Math.exp(-x));case 'relu':return Math.max(0,x);case 'selu':{const alpha=1.67326,scale=1.0507;return x>0?scale*x:scale*alpha*(Math.exp(x)-1);}default:throw new Error('Unknown activation');} }
activationDerivative(x,a){ switch(a){case 'tanh':return 1-Math.pow(Math.tanh(x),2);case 'sigmoid':{const s=1/(1+Math.exp(-x));return s*(1-s);}case 'relu':return x>0?1:0;case 'selu':{const alpha=1.67326,scale=1.0507;return x>0?scale:scale*alpha*Math.exp(x);}default:throw new Error('Unknown derivative');} }
positionalEncoding(input,maxLen){ const pe=new Array(maxLen).fill(0).map((_,pos)=>new Array(input[0].length).fill(0).map((_,i)=>{const ang=pos/Math.pow(10000,2*i/input[0].length);return pos%2===0?Math.sin(ang):Math.cos(ang);})); return input.map((seq,idx)=>seq.map((v,i)=>v+pe[idx][i])); }
multiHeadSelfAttention(input,numHeads=2){
const headSize=input[0].length/numHeads; const heads=[...Array(numHeads)].map(()=>[...Array(input.length)].map(()=>[...Array(headSize)].fill(0)));
for(let h=0;h<numHeads;h++) for(let i=0;i<input.length;i++) for(let j=0;j<headSize;j++) heads[h][i][j]=input[i][h*headSize+j];
const scores=[...Array(numHeads)].map(()=>[...Array(input.length)].map(()=>[...Array(input.length)].fill(0)));
for(let h=0;h<numHeads;h++) for(let i=0;i<input.length;i++) for(let j=0;j<input.length;j++){ let s=0; for(let k=0;k<headSize;k++) s+=heads[h][i][k]*heads[h][j][k]; scores[h][i][j]=s; }
const weights=scores.map(head=>head.map(row=>{const ex=row.map(v=>Math.exp(v)); const sum=ex.reduce((a,b)=>a+b,0); return ex.map(v=>v/sum)}));
const out=[...Array(input.length)].map(()=>[...Array(input[0].length)].fill(0));
for(let h=0;h<numHeads;h++) for(let i=0;i<input.length;i++) for(let j=0;j<headSize;j++) for(let k=0;k<input.length;k++) out[i][h*headSize+j]+=weights[h][i][k]*heads[h][k][j];
return out;
}
layerNormalization(arr){ const m=arr.reduce((s,v)=>s+v,0)/arr.length; const v=arr.reduce((s,x)=>s+Math.pow(x-m,2),0)/arr.length; return arr.map(x=>(x-m)/Math.sqrt(v+1e-5)); }
async train(trainSet,options={}){
const {epochs=200,learningRate=0.212,batchSize=16,printEveryEpochs=100,earlyStopThreshold=1e-6,testSet=null,callback=null}=options;
const start=Date.now(); const batch=Math.max(1,batchSize);
if(this.layers.length===0){ const n=trainSet[0].input.length; this.layer(n,n,'tanh'); this.layer(n,1,'tanh'); }
let lastTrainLoss=0,lastTestLoss=null;
for(let epoch=0;epoch<epochs;epoch++){
let trainError=0;
for(let b=0;b<trainSet.length;b+=batch){
const batchItems=trainSet.slice(b,b+batch); let batchError=0;
for(const data of batchItems){
const L=[data.input];
for(let i=0;i<this.weights.length;i++){
const inputs=L[i], W=this.weights[i], B=this.biases[i], act=this.activations[i]; const out=[];
for(let j=0;j<W.length;j++){ const w=W[j]; let sum=B[j]; for(let k=0;k<inputs.length;k++) sum+=inputs[k]*w[k]; out.push(this.activationFunction(sum,act)); }
L.push(out);
}
const outIn=L[L.length-1]; const outErr=[]; for(let i=0;i<outIn.length;i++) outErr.push((data.output[i]??0)-outIn[i]);
let layerErrors=[outErr];
for(let i=this.weights.length-2;i>=0;i--){
const Wnext=this.weights[i+1], nextErr=layerErrors[0], curIn=L[i+1], act=this.activations[i]; const errs=[];
for(let j=0;j<this.layers[i].outputSize;j++){ let e=0; for(let k=0;k<this.layers[i+1].outputSize;k++) e+=nextErr[k]*Wnext[k][j]; errs.push(e*this.activationDerivative(curIn[j],act)); }
layerErrors.unshift(errs);
}
for(let i=0;i<this.weights.length;i++){
const inputs=L[i], errs=layerErrors[i], W=this.weights[i], B=this.biases[i];
for(let j=0;j<W.length;j++){ const w=W[j]; for(let k=0;k<inputs.length;k++) w[k]+=learningRate*errs[j]*inputs[k]; B[j]+=learningRate*errs[j]; }
}
batchError+=Math.abs(outErr[0]??0);
}
trainError+=batchError;
}
lastTrainLoss=trainError/trainSet.length;
if(testSet){ let te=0; for(const d of testSet){ const p=this.predict(d.input); te+=Math.abs((d.output[0]??0)-(p[0]??0)); } lastTestLoss=te/testSet.length; }
if((epoch+1)%printEveryEpochs===0 && this.debug) console.log(`Epoch ${epoch+1} | Train ${lastTrainLoss.toFixed(6)}${testSet?` | Val ${lastTestLoss.toFixed(6)}`:''}`);
if(callback) await callback(epoch+1,lastTrainLoss,lastTestLoss);
await new Promise(r=>setTimeout(r,0));
if(lastTrainLoss<earlyStopThreshold) { if(this.debug) console.log(`Early stop @${epoch+1}`); break; }
}
const end=Date.now(); let params=0; for(let i=0;i<this.weights.length;i++){ params+=this.weights[i].flat().length+this.biases[i].length; }
const summary={trainLoss:lastTrainLoss,testLoss:lastTestLoss,parameters:params,training:{time:end-start,epochs,learningRate,batchSize:batch},layers:this.layers.map(l=>({inputSize:l.inputSize,outputSize:l.outputSize,activation:l.activation}))};
this.details=summary; return summary;
}
predict(input){
let x=input; const acts=[input], raw=[];
for(let i=0;i<this.weights.length;i++){ const W=this.weights[i], B=this.biases[i], a=this.activations[i]; const y=[], r=[];
for(let j=0;j<W.length;j++){ const w=W[j]; let s=B[j]; for(let k=0;k<x.length;k++) s+=x[k]*w[k]; r.push(s); y.push(this.activationFunction(s,a)); }
raw.push(r); acts.push(y); x=y;
}
this.lastActivations=acts; this.lastRawValues=raw; return x;
}
save(name='model'){ const data={weights:this.weights,biases:this.biases,activations:this.activations,layers:this.layers,details:this.details};
const blob=new Blob([JSON.stringify(data)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=`${name}.json`; a.click(); URL.revokeObjectURL(url);
}
load(callback){
const onChange=(e)=>{ const f=e.target.files[0]; if(!f) return; const r=new FileReader();
r.onload=(ev)=>{ try{ const data=JSON.parse(ev.target.result); this.weights=data.weights; this.biases=data.biases; this.activations=data.activations; this.layers=data.layers; this.details=data.details; callback&&callback(); if(this.debug) console.log('Loaded'); }catch(err){ if(this.debug) console.error('Load failed',err); } finally{ input.removeEventListener('change',onChange); input.remove(); } };
r.readAsText(f);
};
const input=document.createElement('input'); input.type='file'; input.accept='.json'; input.style.position='fixed'; input.style.opacity='0'; document.body.append(input); input.addEventListener('change',onChange); input.click();
}
}
/* ---------------- App ---------------- */
document.addEventListener('DOMContentLoaded',()=>{
const nn=new carbono();
let lossHistory=[];
const lossCanvas=document.getElementById('lossGraph');
const networkCanvas=document.getElementById('networkGraph');
const lossCtx=lossCanvas.getContext('2d');
const el={
loadDataBtn:document.getElementById('loadDataBtn'),
trainingData:document.getElementById('trainingData'),
testData:document.getElementById('testData'),
numHiddenLayers:document.getElementById('numHiddenLayers'),
hiddenLayersConfig:document.getElementById('hiddenLayersConfig'),
trainButton:document.getElementById('trainButton'),
stats:document.getElementById('stats'),
epochBar:document.getElementById('epochBar'),
epochs:document.getElementById('epochs'),
learningRate:document.getElementById('learningRate'),
batchSize:document.getElementById('batchSize'),
predictButton:document.getElementById('predictButton'),
predictionInput:document.getElementById('predictionInput'),
predictionResult:document.getElementById('predictionResult'),
saveButton:document.getElementById('saveButton'),
loadButton:document.getElementById('loadButton')
};
const parseCSV=(csv)=> csv.trim().split('\n').filter(Boolean).map(row=>{
const values=row.split(',').map(s=>Number(s.trim()));
return {input:values.slice(0,-1),output:[values[values.length-1]]};
});
function drawLossGraph(){
const {width,height}=lossCanvas;
lossCtx.clearRect(0,0,width,height);
if(lossHistory.length===0) return;
const maxLoss=Math.max(1e-9,...lossHistory.map(l=>Math.max(l.train, l.test??0)));
function line(data, color){
lossCtx.strokeStyle=color; lossCtx.beginPath();
data.forEach((v,i)=>{ const x=(i/(data.length-1))*width; const y=height-(v/maxLoss)*height; if(i===0) lossCtx.moveTo(x,y); else lossCtx.lineTo(x,y); });
lossCtx.stroke();
}
line(lossHistory.map(l=>l.train),'#ffffff');
if(lossHistory.some(l=>l.test!==undefined)) line(lossHistory.map(l=>l.test ?? 0),'#777777');
}
function createLayerConfigUI(n){
el.hiddenLayersConfig.innerHTML='';
for(let i=0;i<n;i++){
const block=document.createElement('div');
block.className='row';
block.style.marginTop='0';
block.innerHTML=`
<div style="grid-column: span 2;" class="block">
<label>Layer ${i+1} Nodes</label>
<input class="input" type="number" value="5" data-layer-index="${i}">
</div>
<div style="grid-column: span 2;" class="block">
<label>Activation</label>
<select class="input" data-layer-index="${i}">
<option>tanh</option>
<option>sigmoid</option>
<option>relu</option>
<option>selu</option>
</select>
</div>`;
el.hiddenLayersConfig.appendChild(block);
}
}
async function trainModel(){
lossHistory=[];
const trainingData=parseCSV(el.trainingData.value);
const testData=parseCSV(el.testData.value||'');
el.stats.innerHTML='';
const nHidden=parseInt(el.numHiddenLayers.value,10);
const layerCfg=[];
for(let i=0;i<nHidden;i++){
const size=parseInt(document.querySelector(`input[data-layer-index="${i}"]`).value,10);
const act=document.querySelector(`select[data-layer-index="${i}"]`).value;
layerCfg.push({size,activation:act});
}
nn.layers=[]; nn.weights=[]; nn.biases=[]; nn.activations=[];
const numInputs=trainingData[0].input.length;
nn.layer(numInputs, layerCfg[0].size, layerCfg[0].activation);
for(let i=1;i<layerCfg.length;i++) nn.layer(layerCfg[i-1].size, layerCfg[i].size, layerCfg[i].activation);
nn.layer(layerCfg[layerCfg.length-1].size, 1, 'tanh');
const opts={
epochs:parseInt(el.epochs.value,10),
learningRate:parseFloat(el.learningRate.value),
batchSize:parseInt(el.batchSize.value,10),
printEveryEpochs:1,
testSet:testData.length?testData:null,
callback:async (epoch,trainLoss,testLoss)=>{
lossHistory.push({train:trainLoss,test:testLoss});
drawLossGraph();
el.epochBar.style.width=`${(epoch/opts.epochs)*100}%`;
el.stats.innerHTML=`
<div><b>Epoch</b></div><div>${epoch}/${opts.epochs}</div>
<div><b>Train</b></div><div>${trainLoss.toFixed(6)}</div>
${testLoss!==null?`<div><b>Val</b></div><div>${testLoss.toFixed(6)}</div>`:''}
`;
}
};
try{ el.trainButton.disabled=true; el.trainButton.textContent='Training…'; await nn.train(trainingData,opts);
el.stats.innerHTML+=`<div><b>Status</b></div><div>Model trained</div>`;
}catch(e){ console.error('Training error:',e);
el.stats.innerHTML+=`<div><b>Error</b></div><div>${e.message}</div>`;
}finally{ el.trainButton.disabled=false; el.trainButton.textContent='Train'; }
}
function drawNetwork(){
const ctx=networkCanvas.getContext('2d');
ctx.clearRect(0,0,networkCanvas.width,networkCanvas.height);
if(!nn.lastActivations) return;
const pad=34; const W=networkCanvas.width-pad*2; const H=networkCanvas.height-pad*2;
const layers=[];
// inputs
const inSize=nn.layers[0].inputSize; const inX=pad; const inNodes=[];
for(let i=0;i<inSize;i++){ const y=pad+(inSize>1?(H*i)/(inSize-1):H/2); inNodes.push({x:inX,y,val:nn.lastActivations[0][i]||0}); }
layers.push(inNodes);
// hidden(s)
for(let i=1;i<nn.lastActivations.length-1;i++){
const L=nn.lastActivations[i]; const nodes=[]; const x=pad+(W*i)/(nn.lastActivations.length-1);
for(let j=0;j<L.length;j++){ const y=pad+(L.length>1?(H*j)/(L.length-1):H/2); nodes.push({x,y,val:L[j]}); }
layers.push(nodes);
}
// output
const outX=networkCanvas.width-pad; const outY=pad+H/2; layers.push([{x:outX,y:outY,val:nn.lastActivations.at(-1)[0]||0}]);
// connections
ctx.lineWidth=1;
for(let i=0;i<layers.length-1;i++){
const A=layers[i], B=layers[i+1], Wmat=nn.weights[i];
for(let j=0;j<A.length;j++) for(let k=0;k<B.length;k++){
const w=Wmat[k][j]; const sig=Math.abs((A[j].val||0)*w); const op=Math.min(Math.max(sig,0.06),1);
ctx.strokeStyle=`rgba(255,255,255,${op})`; ctx.beginPath(); ctx.moveTo(A[j].x,A[j].y); ctx.lineTo(B[k].x,B[k].y); ctx.stroke();
}
}
// nodes
for(const L of layers){ for(const n of L){ const r=3.5, op=Math.min(Math.max(Math.abs(n.val),0.3),1);
ctx.fillStyle=`rgba(255,255,255,${op})`; ctx.beginPath(); ctx.arc(n.x,n.y,r,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='rgba(255,255,255,1)'; ctx.lineWidth=.8; ctx.stroke();
}}
}
function sizeCanvases(){
[lossCanvas,networkCanvas].forEach(cv=>{ cv.width=cv.parentElement.clientWidth; cv.height=cv.parentElement.clientHeight; });
drawNetwork();
}
el.loadDataBtn.onclick=()=>{ el.trainingData.value=`1.0, 0.0, 0.0, 0.0
0.7, 0.7, 0.8, 1
0.0, 1.0, 0.0, 0.5`; el.testData.value=`0.4, 0.2, 0.6, 1.0
0.2, 0.82, 0.83, 1.0`; };
el.numHiddenLayers.addEventListener('change',(e)=>createLayerConfigUI(parseInt(e.target.value,10)));
el.trainButton.addEventListener('click',trainModel);
el.predictButton.addEventListener('click',()=>{
const input=el.predictionInput.value.split(',').map(s=>Number(s.trim())).filter(n=>!Number.isNaN(n));
const p=nn.predict(input);
el.predictionResult.textContent=`Prediction: ${Number.isFinite(p[0])?p[0].toFixed(6):'NaN'}`;
drawNetwork();
});
el.saveButton.addEventListener('click',()=>nn.save('model'));
el.loadButton.addEventListener('click',()=>nn.load(()=>{ el.stats.innerHTML+=`<div><b>Status</b></div><div>Model loaded</div>`; }));
window.addEventListener('resize', sizeCanvases, {passive:true});
createLayerConfigUI(parseInt(el.numHiddenLayers.value,10));
sizeCanvases();
});
</script>
</body>
</html>