Jev picks a Signal's icon, on the server's key, and each Signal counts its items (#263)
Jev chooses among options and cannot name one, so the page searches Font Awesome's index for each word of the Signal's name, words and sentence, the best six each, and offers the likeliest 40. POST /api/signals/icon asks a choice question on the server's TYPESAFE_KEY and answers with one of those offered, never another. "Let Jev pick" in the icon picker, and a Signal made without an icon gets one a moment after. Tried against TypeSafe: for AI Development, laptop-code 0.45 just ahead of robot 0.40. Each Signal's row says "12 items · 3 unread", as a feed's row counts its own, in place of its words; /api/signals gives `items` beside `unread`, by the same rules as its list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ function signalFolder(list: HTMLElement, q: string){
|
||||
if(!open) return;
|
||||
for(const sg of mine){
|
||||
const el=feedRow(':signal:'+sg.id,'child',
|
||||
`<span class="fart">${signalTile(sg)}</span><div class="txt"><b>${esc(sg.name)}</b><small>${esc(sg.keywords.join(', '))}</small></div>`+badgeHTML(sg.unread),
|
||||
`<span class="fart">${signalTile(sg)}</span><div class="txt"><b>${esc(sg.name)}</b><small>${signalCounts(sg)}</small></div>`+badgeHTML(sg.unread),
|
||||
()=>selectFeed(':signal:'+sg.id));
|
||||
el.onpointerenter=el.ontouchstart=()=>prefetch(':signal:'+sg.id);
|
||||
list.appendChild(el);
|
||||
@@ -97,7 +97,7 @@ function renderSignals(){
|
||||
const el=document.createElement('div');
|
||||
el.className='childrow';
|
||||
el.innerHTML=signalTile(sg)+`<div class="txt"><b>${esc(sg.name)}</b>`+
|
||||
`<small class="meta">${esc(sg.keywords.join(', '))}</small></div><span class="badge${sg.unread?'':' zero'}">${sg.unread}</span>`;
|
||||
`<small class="meta">${signalCounts(sg)}</small></div><span class="badge${sg.unread?'':' zero'}">${sg.unread}</span>`;
|
||||
el.onclick=()=>selectFeed(':signal:'+sg.id);
|
||||
list.appendChild(el);
|
||||
}
|
||||
@@ -141,6 +141,7 @@ function signalModal(sg?: Signal){
|
||||
<input type="text" id="sgname" value="${esc(sg?.name||'')}" placeholder="Formula 1"></div>
|
||||
<div id="sgpicker" hidden>
|
||||
<div class="inline"><input type="search" id="sgiconq" placeholder="Search icons: robot, car, music…" autocomplete="off">
|
||||
<button type="button" class="dlink" id="sgjev">Let Jev pick</button>
|
||||
<button type="button" class="dlink" id="sgnoicon">Initials</button></div>
|
||||
<div class="icongrid" id="sgicons" role="listbox" aria-label="Icons"></div>
|
||||
</div></div>
|
||||
@@ -179,6 +180,16 @@ function signalModal(sg?: Signal){
|
||||
icon=b.dataset.ic!; drawPick(); $('#sgpicker').hidden=true; $('#sgicon').setAttribute('aria-expanded','false');
|
||||
};
|
||||
$<HTMLButtonElement>('#sgnoicon').onclick=()=>{ icon=null; drawPick(); $('#sgpicker').hidden=true; };
|
||||
$<HTMLButtonElement>('#sgjev').onclick=async()=>{
|
||||
const b=$<HTMLButtonElement>('#sgjev'); b.disabled=true; b.textContent='Asking Jev…';
|
||||
try{
|
||||
const got=await jevIcon($<HTMLInputElement>('#sgname').value.trim(),chipWords($('#sgkw')).filter((w): w is string=>!!w),
|
||||
$<HTMLInputElement>('#sgdesc').value.trim()||null);
|
||||
if(got){ icon=got; drawPick(); $('#sgpicker').hidden=true; $('#sgicon').setAttribute('aria-expanded','false'); }
|
||||
else toast('No icon matches those words');
|
||||
}catch(e){ toast(why(e),true); }
|
||||
finally{ b.disabled=false; b.textContent='Let Jev pick'; }
|
||||
};
|
||||
$<HTMLInputElement>('#sgname').focus();
|
||||
$<HTMLButtonElement>('#sgsave').onclick=async()=>{
|
||||
const name=$<HTMLInputElement>('#sgname').value.trim(), keywords=chipWords($('#sgkw')).filter((w): w is string=>!!w);
|
||||
@@ -191,6 +202,10 @@ function signalModal(sg?: Signal){
|
||||
try{
|
||||
const r=sg ? await api(`/api/signals/${sg.id}`,{method:'PATCH',body})
|
||||
: await api('/api/signals',{method:'POST',body});
|
||||
// A new one made without an icon gets Jev's pick, behind, so making it does not wait (#263).
|
||||
if(!sg&&!icon) jevIcon(name,keywords,description)
|
||||
.then(got=>{ if(got) return api(`/api/signals/${r.id}`,{method:'PATCH',body:JSON.stringify({icon:got})}).then(refreshSignals); })
|
||||
.catch(()=>{});
|
||||
closeModal(); toast(sg?'Saved. Jev reads everything again':'Listening. What Jev picks out shows up here');
|
||||
// Open, so the new one is there to see.
|
||||
if(!expanded.has(SIGNALS)) toggleGroup(SIGNALS);
|
||||
@@ -233,3 +248,24 @@ function iconMatches(all: [string,string,string][], q: string, max: number){
|
||||
};
|
||||
return all.map(i=>[rank(i),i] as const).filter(([r])=>r>=0).sort((a,b)=>a[0]-b[0]).slice(0,max).map(([,i])=>i);
|
||||
}
|
||||
|
||||
/// "12 items · 3 unread", as a feed's row counts its own.
|
||||
const signalCounts=(sg: Signal)=>`${plural(sg.items,'item')} · ${sg.unread} unread`;
|
||||
|
||||
/// The icons likeliest for a Signal: the best few for each word of its name, words and sentence,
|
||||
/// at most `max`. Jev picks from these, since it chooses among options and cannot name one.
|
||||
const ICON_STOP=new Set(['the','and','for','not','with','about','their','own','from','into','that','this','your']);
|
||||
function iconCandidates(all: [string,string,string][], name: string, keywords: string[], description: string|null, max=40){
|
||||
const words=[name,...keywords,description||''].join(' ').toLowerCase().split(/[^a-z0-9]+/).filter(w=>w.length>1&&!ICON_STOP.has(w));
|
||||
const out=new Map<string,string>();
|
||||
for(const w of [...new Set(words)])
|
||||
for(const [id,label,terms] of iconMatches(all,w,6)) if(out.size<max) out.set(id,`${label}: ${terms.split(' ').slice(0,8).join(', ')}`);
|
||||
return [...out];
|
||||
}
|
||||
/// Jev's pick of an icon, on the server's TypeSafe key, or null with nothing to choose from.
|
||||
async function jevIcon(name: string, keywords: string[], description: string|null): Promise<string|null>{
|
||||
const candidates=iconCandidates(await iconList(),name,keywords,description);
|
||||
if(!candidates.length) return null;
|
||||
const r=await api('/api/signals/icon',{method:'POST',body:JSON.stringify({name,keywords,description,candidates})});
|
||||
return r.icon;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ interface Signal {
|
||||
description: string | null;
|
||||
/// A Font Awesome Free icon, `solid/robot`, in place of its initials (#261).
|
||||
icon: string | null;
|
||||
/// The items it holds, read or not, and of those the unread.
|
||||
items: number;
|
||||
unread: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user