mcp radio: bigger card + report height to host; reactions go to model context (no new prompt), Next stays a message
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mBuCaQSgWwqhtzsmTBQuM
This commit is contained in:
1 parent
ff6e9146cf
commit
c42a6f5c87
1 file changed
+15
-10
+15
-10
@@ -178,9 +178,10 @@ d.querySelector('button').onclick=async()=>{const s=document.getElementById('s-'
|
||||
const RADIO_HTML = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
|
||||
<style>
|
||||
:root{color-scheme:light dark;font-family:system-ui,sans-serif;--mut:color-mix(in srgb,CanvasText 55%,transparent);--line:color-mix(in srgb,CanvasText 18%,transparent)}
|
||||
body{margin:0;padding:14px;background:transparent;color:CanvasText}
|
||||
.card{display:flex;gap:14px;align-items:center}
|
||||
canvas{width:96px;height:96px;border-radius:12px;flex:none}
|
||||
body{margin:0;padding:16px 16px 18px;background:transparent;color:CanvasText}
|
||||
.card{display:flex;gap:16px;align-items:center}
|
||||
canvas{width:150px;height:150px;border-radius:14px;flex:none}
|
||||
@media (max-width:480px){canvas{width:110px;height:110px}}
|
||||
.t{font-size:16px;font-weight:600;margin:0}.g{font-size:12px;color:var(--mut);margin:2px 0 0;line-height:1.4}
|
||||
.st{font-size:12px;color:var(--mut);margin-top:6px;min-height:16px}
|
||||
.line{font-size:13px;font-style:italic;margin:10px 0 0;color:var(--mut)}
|
||||
@@ -190,12 +191,12 @@ button{font:inherit;font-size:13px;padding:5px 10px;border-radius:7px;border:1px
|
||||
button:disabled{opacity:.4;cursor:default}label{font-size:12px;color:var(--mut);display:flex;gap:4px;align-items:center;margin-left:auto}
|
||||
details{margin-top:10px;font-size:12px;color:var(--mut)}pre{white-space:pre-wrap;font:11px ui-monospace,monospace}
|
||||
</style></head><body>
|
||||
<div class="card"><canvas id="cv" width="192" height="192"></canvas>
|
||||
<div class="card"><canvas id="cv" width="300" height="300"></canvas>
|
||||
<div style="min-width:0"><p class="t" id="title">tuning in…</p><p class="g" id="tags"></p><div class="st" id="st">waiting for the DJ</div></div></div>
|
||||
<p class="line" id="line" hidden></p>
|
||||
<div class="bar"><i id="pos"></i></div>
|
||||
<div class="ctl">
|
||||
<button id="pp" disabled>Play</button><button id="up" disabled>More like this ↗</button><button id="down" disabled>Not this ↗</button><button id="next" disabled>Next ↗</button>
|
||||
<button id="pp" disabled>Play</button><button id="up" disabled>Like</button><button id="down" disabled>Not this</button><button id="next" disabled>Next ↗</button>
|
||||
<label><input type="checkbox" id="auto" checked> auto-DJ</label></div>
|
||||
<details><summary>sheet</summary><pre id="sheet"></pre><pre id="log"></pre></details>
|
||||
<script>${BRIDGE}
|
||||
@@ -206,8 +207,8 @@ const me=Math.random().toString(36).slice(2);
|
||||
if(bc)bc.onmessage=e=>{if(e.data&&e.data.on&&e.data.on!==me){[song,voice].forEach(a=>a&&a.pause());sent=true;$('pp').textContent='Play'}};
|
||||
function hue(k){const n=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const m=String(k||'').match(/^([A-G])(#|b)?/i);if(!m)return 260;let i=n.indexOf(m[1].toUpperCase());if(m[2]==='#')i++;if(m[2]==='b')i--;return((i+12)%12)*30}
|
||||
function cover(s){const c=$('cv'),x=c.getContext('2d'),h=hue(s.key),minor=/minor/i.test(s.key||''),bpm=+s.bpm||100;let r=s.seed||1;const rnd=()=>{r=(r*1103515245+12345)&0x7fffffff;return r/0x7fffffff};
|
||||
x.fillStyle='hsl('+h+' 45% '+(minor?18:30)+'%)';x.fillRect(0,0,192,192);const n=3+Math.round(bpm/30);
|
||||
for(let i=n;i>0;i--){x.beginPath();x.arc(96+(rnd()-.5)*30,96+(rnd()-.5)*30,i*(84/n),0,7);x.strokeStyle='hsl('+((h+i*(minor?8:20))%360)+' 70% '+(55+i*3)+'%)';x.lineWidth=1.5+rnd()*3;x.stroke()}}
|
||||
x.fillStyle='hsl('+h+' 45% '+(minor?18:30)+'%)';x.fillRect(0,0,300,300);const n=3+Math.round(bpm/30);
|
||||
for(let i=n;i>0;i--){x.beginPath();x.arc(150+(rnd()-.5)*46,150+(rnd()-.5)*46,i*(132/n),0,7);x.strokeStyle='hsl('+((h+i*(minor?8:20))%360)+' 70% '+(55+i*3)+'%)';x.lineWidth=1.5+rnd()*3;x.stroke()}}
|
||||
function show(s){S=s;$('title').textContent=s.title||'untitled';$('tags').textContent=[s.tags,s.bpm&&(s.bpm+' bpm'),s.key].filter(Boolean).join(' · ');
|
||||
if(s.dj_line){$('line').textContent='“'+s.dj_line+'”';$('line').hidden=false}$('sheet').textContent=JSON.stringify(s,null,1);cover(s)}
|
||||
function status(t){$('st').textContent=t}
|
||||
@@ -217,13 +218,17 @@ try{const r=await tool('render_track',s,180000);clearInterval(tick);song=new Aud
|
||||
const vu=await vp;if(vu)voice=new Audio(vu);wire();status('ready · '+Math.round((Date.now()-t0)/1000)+'s to cook');['pp','up','down','next'].forEach(i=>$(i).disabled=false);go()}
|
||||
catch(e){clearInterval(tick);status('render failed: '+e.message);$('next').disabled=false}}
|
||||
function wire(){song.ontimeupdate=()=>{$('pos').style.width=(100*song.currentTime/(song.duration||1))+'%'};
|
||||
song.onended=()=>{$('pp').textContent='Play';status('ended');if($('auto').checked)ask('track ended: "'+S.title+'". Next track please.')};
|
||||
song.onended=()=>{$('pp').textContent='Play';status('ended');tell('finished "'+S.title+'" all the way through');if($('auto').checked)ask('track ended: "'+S.title+'". Next track please.')};
|
||||
if(voice)voice.onended=()=>song.play().catch(()=>{status('tap Play');$('pp').textContent='Play'})}
|
||||
async function go(){if(bc)bc.postMessage({on:me});sent=false;try{if(voice&&voice.currentTime===0&&song.currentTime===0)await voice.play();else await song.play();$('pp').textContent='Pause';status('on air')}catch(e){status('tap Play to start');$('pp').textContent='Play'}}
|
||||
$('pp').onclick=()=>{const live=(voice&&!voice.paused)?voice:song;if(!live.paused){live.pause();$('pp').textContent='Play'}else{live.play();$('pp').textContent='Pause';if(bc)bc.postMessage({on:me})}};
|
||||
async function ask(text){if(sent)return;sent=true;try{await say('[gandora] '+text)}catch(e){log('send: '+e.message);sent=false}}
|
||||
$('up').onclick=()=>ask('loved "'+S.title+'" ('+S.tags+'). More in this direction next.');
|
||||
$('down').onclick=()=>{[song,voice].forEach(a=>a&&a.pause());ask('not feeling "'+S.title+'" ('+S.tags+'). Skip it and steer away from that.')};
|
||||
const events=[];
|
||||
async function tell(text){events.push(text);status(text);try{await rpc('ui/update-model-context',{content:[{type:'text',text:'[gandora] listener feedback on this card, oldest first:\n- '+events.join('\n- ')}],structuredContent:{title:S&&S.title,events}});log('context: '+text)}catch(e){log('context failed: '+e.message)}}
|
||||
$('up').onclick=()=>{tell('liked "'+S.title+'" ('+S.tags+')');$('up').textContent='Liked'};
|
||||
$('down').onclick=()=>{[song,voice].forEach(a=>a&&a.pause());$('pp').textContent='Play';tell('disliked "'+S.title+'" ('+S.tags+'), stopped it')};
|
||||
const fit=()=>note('ui/notifications/size-changed',{width:document.documentElement.scrollWidth,height:document.documentElement.scrollHeight});
|
||||
if('ResizeObserver' in window)new ResizeObserver(fit).observe(document.body);addEventListener('load',fit);
|
||||
$('next').onclick=()=>{[song,voice].forEach(a=>a&&a.pause());ask('skip "'+(S?S.title:'')+'". Next track please.')};
|
||||
on('ui/notifications/tool-input',p=>{if(p.arguments&&p.arguments.title&&!S)show(p.arguments)});
|
||||
on('ui/notifications/tool-result',p=>{const sc=p.structuredContent||(p.result&&p.result.structuredContent);if(sc&&sc.tags)begin(sc)});
|
||||
|
||||
Reference in new issue
Block a user