Signals' page copy says what happens, not that Jev does it; Auto spins while it picks (#263)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 19:28:52 +00:00
parent 15bdce486d
commit f34423735a
6 changed files with 20 additions and 14 deletions

View File

@@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Signals pick what matters out of the noise of every feed you subscribe to: New signal takes a name, the words to listen for and, if those are not enough, a sentence saying what you mean, and its page lists the items from your feeds that are about it, read and unread as in their own feeds. Settings takes your TypeSafe key, which Signals need (#254). They are kept in a Signals folder first in the feed list, under All Subscriptions, closed or open as an OPML is, with its own page listing them and a + there for a new one (#255, #256, #265). Each can have an icon of its own, searched for among all of Font Awesome Free, in place of its initials (#261), or Jev's pick of one, which a Signal made without an icon gets by itself; each Signal's row counts its items and unread as a feed's does (#263).
- Signals pick what matters out of the noise of every feed you subscribe to: New signal takes a name, the words to listen for and, if those are not enough, a sentence saying what you mean, and its page lists the items from your feeds that are about it, read and unread as in their own feeds. Settings takes your TypeSafe key, which Signals need (#254). They are kept in a Signals folder first in the feed list, under All Subscriptions, closed or open as an OPML is, with its own page listing them and a + there for a new one (#255, #256, #265). Each can have an icon of its own, searched for among all of Font Awesome Free, in place of its initials (#261), or one picked for it with Auto, which a Signal made without an icon gets by itself; each Signal's row counts its items and unread as a feed's does (#263).
- A list opens at once when you have seen it before or the page fetched it ahead: All Subscriptions and the feeds with something unread are fetched when the page loads, and a feed or filter as the pointer or a finger reaches it, then refreshed behind once shown. A list that has to be waited for shows a spinner instead of nothing (#247). Currently Listening and the Directory's front page are fetched ahead the same way and open at once (#248).
### Changed

View File

@@ -607,6 +607,11 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
.iconcell{height:40px;border-radius:8px;display:grid;place-items:center;color:var(--dim)}
.iconcell:hover,.iconcell[aria-selected="true"]{background:var(--raise);color:var(--fg)}
.iconcell .ficon{width:20px;height:20px}
/* Auto in the icon picker, while an icon is picked: the scan spinner, beside the word. */
.dlink.busy::after{
content:"";display:inline-block;width:10px;height:10px;margin-left:6px;vertical-align:-1px;border-radius:50%;
border:2px solid var(--line);border-top-color:var(--accent);animation:ipxspin .8s linear infinite;
}
/* Only a folder has a triangle, hung in the margin so every feed's art lines up with the places
above it. The button is the row's full height and 24 px wide: a near miss used to open the
folder's page, and 16 px left the triangle cramped against the art. */

View File

@@ -444,8 +444,8 @@ async function prefsModal(){
<div class="inline"><input type="password" id="stskey" autocomplete="off" spellcheck="false"
placeholder="${S.me?.typesafe_key_set?'Saved. Paste a new one to replace it':'Paste your key'}">
<button class="btn ico" id="stsforget" title="Forget the key" aria-label="Forget the key"${S.me?.typesafe_key_set?'':' hidden'}>${ICON.trash}</button></div>
<span class="hint">Signals pick what matters out of everything you subscribe to, using TypeSafe's
Jev on your own key, from <a href="https://typesafe.ai" target="_blank" rel="noopener">typesafe.ai</a>.
<span class="hint">Signals pick what matters out of everything you subscribe to, on your own
key from <a href="https://typesafe.ai" target="_blank" rel="noopener">typesafe.ai</a>.
The key is kept on this server and never shown again.</span></div>
<div class="field boxed"><label for="tokname">API tokens</label>
<div id="tokens"></div>

View File

@@ -134,7 +134,7 @@ function renderEntries(){
// Said only for a search, where the words typed are why; an empty feed or tab speaks for itself.
if(S.q) box.innerHTML='<p class="empty">Nothing matches that search.</p>';
// A new Signal is empty while Jev reads, which would otherwise look broken.
else if(sg&&S.filter==='all') box.innerHTML='<p class="empty">Nothing has come through yet. Jev reads new items as your feeds are checked.</p>';
else if(sg&&S.filter==='all') box.innerHTML='<p class="empty">Nothing has come through yet. New items are read as your feeds are checked.</p>';
return;
}
for(const e of S.entries) box.appendChild(epEl(e));

View File

@@ -139,7 +139,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" title="Let Jev pick an icon">Auto</button>
<button type="button" class="dlink" id="sgjev" title="Pick an icon for me">Auto</button>
<button type="button" class="dlink" id="sgnoicon">Initials</button></div>
<div class="icongrid" id="sgicons" role="listbox" aria-label="Icons"></div>
</div></div>
@@ -147,10 +147,10 @@ function signalModal(sg?: Signal){
<div class="chips" id="sgkw">${chipsHTML(sg?.keywords||[],'sgkwin')}</div></div>
<div class="field"><label for="sgdesc">In your own words (optional)</label>
<input type="text" id="sgdesc" value="${esc(sg?.description||'')}" placeholder="AI tools for writing software: coding agents, vibe coding">
<span class="hint">TypeSafe's Jev reads every item in the feeds you subscribe to, the last 30
days of them and each new one, and pulls out the ones about this subject: its name, these
words and your sentence together. The rest stays noise, in its own feed. Each item is one
call on your TypeSafe key.${sg?' A change here reads everything again.':''}</span></div>
<span class="hint">Every item in the feeds you subscribe to is read, the last 30 days of them
and each new one, and the ones about this subject are pulled out: its name, these words and
your sentence together. The rest stays noise, in its own feed. Each item is one call on your
TypeSafe key.${sg?' A change here reads everything again.':''}</span></div>
${nokey?'<p class="hint" style="color:var(--warn)">Add your TypeSafe key in Settings first: without one, nothing is picked out.</p>':''}
<div class="cardacts"><button class="btn ico" onclick="closeModal()" title="Cancel" aria-label="Cancel">${ICON.close}</button>
<button class="btn ico primary" id="sgsave" title="${sg?'Save':'Make signal'}" aria-label="${sg?'Save':'Make signal'}">${sg?ICON.check:ICON.plus}</button></div>`);
@@ -179,14 +179,15 @@ function signalModal(sg?: Signal){
};
$<HTMLButtonElement>('#sgnoicon').onclick=()=>{ icon=null; drawPick(); $('#sgpicker').hidden=true; };
$<HTMLButtonElement>('#sgjev').onclick=async()=>{
const b=$<HTMLButtonElement>('#sgjev'); b.disabled=true; b.textContent='Asking Jev…';
// A spinner while it is picked: what picks it is nobody's concern.
const b=$<HTMLButtonElement>('#sgjev'); b.disabled=true; b.classList.add('busy'); b.setAttribute('aria-busy','true');
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='Auto'; }
finally{ b.disabled=false; b.classList.remove('busy'); b.removeAttribute('aria-busy'); }
};
$<HTMLInputElement>('#sgname').focus();
$<HTMLButtonElement>('#sgsave').onclick=async()=>{
@@ -204,7 +205,7 @@ function signalModal(sg?: Signal){
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');
closeModal(); toast(sg?'Saved. Everything is read again':'Listening. What comes through shows up here');
// Open, so the new one is there to see.
if(!expanded.has(SIGNALS)) toggleGroup(SIGNALS);
await loadFeeds(true); selectFeed(':signal:'+(sg?sg.id:r.id));