Sanad_Package_1/static/p1_widget.html

87 lines
6.2 KiB
HTML

<!-- P1 Quick Controls — injected into /full (the advanced Sanad SPA).
Self-contained + namespaced (p1w-) so it never clashes with the SPA DOM.
Reuses the same P1 endpoints as the clean control page. -->
<style>
#p1w-fab{position:fixed;right:18px;bottom:18px;z-index:99999;background:#2f7ad6;color:#fff;
border:0;border-radius:30px;padding:11px 16px;font:600 13px -apple-system,Segoe UI,Roboto,Arial;
box-shadow:0 4px 14px rgba(0,0,0,.4);cursor:pointer}
#p1w-root{position:fixed;right:18px;bottom:70px;z-index:99999;width:340px;max-height:78vh;overflow:auto;
background:#16223a;color:#e9f0fb;border:1px solid #2f4670;border-radius:12px;padding:14px;
display:none;box-shadow:0 10px 30px rgba(0,0,0,.5);
font:13px -apple-system,Segoe UI,Roboto,Arial}
#p1w-root.open{display:block}
#p1w-root h3{margin:2px 0 10px;font-size:14px;color:#bcd2ff}
#p1w-root .c{border:1px solid #27365a;border-radius:9px;padding:10px;margin:8px 0;background:#111d33}
#p1w-root .c h4{margin:0 0 7px;font-size:12px;color:#9cc1ff}
#p1w-root label{display:block;font-size:11px;color:#90a2c4;margin:6px 0 3px}
#p1w-root input,#p1w-root textarea,#p1w-root select{width:100%;background:#0e1830;border:1px solid #27365a;
color:#e9f0fb;border-radius:7px;padding:7px;font:inherit}
#p1w-root textarea{min-height:74px;resize:vertical;font-family:monospace;font-size:11.5px}
#p1w-root button{background:#2f7ad6;color:#fff;border:0;border-radius:7px;padding:7px 11px;cursor:pointer;font:inherit}
#p1w-root button.sec{background:#26395f}#p1w-root button.ok{background:#1f9d57}#p1w-root button.bad{background:#d6455d}
#p1w-root .row{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:7px}
#p1w-root .m{font-size:11px;color:#90a2c4;margin-top:5px;min-height:14px}
#p1w-root .m.ok{color:#7fe0a6}#p1w-root .m.err{color:#f2a3ae}
#p1w-root .vv{width:34px;text-align:right}
</style>
<button id="p1w-fab" type="button">⚙ P1 Controls</button>
<div id="p1w-root">
<h3>Sanad P1 — Quick Controls</h3>
<div class="c"><h4>🎙️ Conversation</h4>
<div class="row"><button id="p1w-start" class="ok">Start</button>
<button id="p1w-stop" class="bad sec">Stop</button>
<span class="m" id="p1w-m-live"></span></div></div>
<div class="c"><h4>💬 Say a line</h4>
<input id="p1w-say" type="text" placeholder="type a sentence…"/>
<div class="row"><button id="p1w-say-btn">Speak</button><span class="m" id="p1w-m-say"></span></div></div>
<div class="c"><h4>🪪 Persona (applied live)</h4>
<textarea id="p1w-persona" placeholder="loading…"></textarea>
<div class="row"><button id="p1w-persona-save" class="ok">Save &amp; Apply</button>
<span class="m" id="p1w-m-persona"></span></div></div>
<div class="c"><h4>🔊 Audio</h4>
<label>Speaker / mic profile</label><select id="p1w-prof"></select>
<div class="row"><button id="p1w-prof-apply">Use</button>
<button id="p1w-rescan" class="sec">Rescan</button></div>
<label>Chest volume</label>
<div class="row"><input id="p1w-vol" type="range" min="0" max="100" style="flex:1"/>
<b class="vv" id="p1w-vol-v"></b></div>
<div class="m" id="p1w-m-audio"></div></div>
</div>
<script>
(function(){
var $=function(id){return document.getElementById(id);};
async function api(method,path,body){var o={method:method,headers:{}};
if(body!==undefined){o.headers["Content-Type"]="application/json";o.body=JSON.stringify(body);}
var r=await fetch(path,o);var d=null;try{d=await r.json();}catch(e){}
if(!r.ok){var m=(d&&(d.detail||d.error||d.message))||(r.status);throw new Error(typeof m==="string"?m:JSON.stringify(m));}return d||{};}
function msg(id,t,k){var e=$(id);if(e){e.textContent=t||"";e.className="m "+(k||"");}}
var loaded=false;
$("p1w-fab").onclick=function(){var r=$("p1w-root");r.classList.toggle("open");
if(r.classList.contains("open")&&!loaded){loaded=true;init();}};
async function init(){
try{var pe=await api("GET","/api/p1/persona");$("p1w-persona").value=pe.system_prompt||"";}catch(e){}
try{var pr=await api("GET","/api/audio/profiles");var s=$("p1w-prof");s.innerHTML="";
(pr.profiles||[]).forEach(function(x){var o=document.createElement("option");o.value=x.id;
o.textContent=(x.name||x.id)+(x.available?" ✓":"");s.appendChild(o);});}catch(e){}
try{var v=await api("GET","/api/audio/g1-speaker/volume");if(typeof v.current_volume==="number"){
$("p1w-vol").value=v.current_volume;$("p1w-vol-v").textContent=v.current_volume;}}catch(e){}
refreshLive();
}
async function refreshLive(){try{var s=await api("GET","/api/live-subprocess/status");
msg("p1w-m-live",(s.state==="running"||s.running)?"live":"stopped",(s.state==="running"||s.running)?"ok":"");}catch(e){}}
$("p1w-start").onclick=async function(){try{await api("POST","/api/live-subprocess/start");}catch(e){msg("p1w-m-live",e.message,"err");}refreshLive();};
$("p1w-stop").onclick=async function(){try{await api("POST","/api/live-subprocess/stop");}catch(e){msg("p1w-m-live",e.message,"err");}refreshLive();};
$("p1w-say-btn").onclick=async function(){var t=$("p1w-say").value.trim();if(!t){return;}msg("p1w-m-say","…");
try{await api("POST","/api/p1/say",{text:t});msg("p1w-m-say","done","ok");}catch(e){msg("p1w-m-say",e.message,"err");}};
$("p1w-persona-save").onclick=async function(){msg("p1w-m-persona","saving…");
try{var r=await api("POST","/api/p1/persona",{content:$("p1w-persona").value});msg("p1w-m-persona",r.message||"saved","ok");refreshLive();}
catch(e){msg("p1w-m-persona",e.message,"err");}};
$("p1w-prof-apply").onclick=async function(){msg("p1w-m-audio","…");
try{await api("POST","/api/audio/select-profile",{profile_id:$("p1w-prof").value});msg("p1w-m-audio","switched","ok");}catch(e){msg("p1w-m-audio",e.message,"err");}};
$("p1w-rescan").onclick=async function(){msg("p1w-m-audio","rescanning…");
try{await api("POST","/api/audio/refresh");await init();msg("p1w-m-audio","rescanned","ok");}catch(e){msg("p1w-m-audio",e.message,"err");}};
var vt=null;$("p1w-vol").oninput=function(){$("p1w-vol-v").textContent=$("p1w-vol").value;clearTimeout(vt);
vt=setTimeout(async function(){try{await api("POST","/api/audio/g1-speaker/volume",{level:parseInt($("p1w-vol").value,10)});}catch(e){msg("p1w-m-audio",e.message,"err");}},300);};
})();
</script>