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:
@@ -518,7 +518,7 @@ async fn pick_signal_icon(
|
|||||||
Json(body): Json<IconAsk>,
|
Json(body): Json<IconAsk>,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||||
let Ok(key) = std::env::var("TYPESAFE_KEY") else {
|
let Ok(key) = std::env::var("TYPESAFE_KEY") else {
|
||||||
return Err(ApiError::not_found("this server has no TypeSafe key to pick icons with"));
|
return Err(ApiError::not_found("picking an icon is not set up on this server"));
|
||||||
};
|
};
|
||||||
let name = signal_name(&body.name)?;
|
let name = signal_name(&body.name)?;
|
||||||
let words = clean_words(body.keywords)?;
|
let words = clean_words(body.keywords)?;
|
||||||
@@ -538,7 +538,7 @@ async fn pick_signal_icon(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::warn!(error = %format!("{e:#}"), "Jev could not pick an icon");
|
tracing::warn!(error = %format!("{e:#}"), "Jev could not pick an icon");
|
||||||
ApiError::bad_gateway("Jev could not pick an icon just now")
|
ApiError::bad_gateway("no icon could be picked just now")
|
||||||
})?;
|
})?;
|
||||||
Ok(Json(serde_json::json!({ "icon": icon })))
|
Ok(Json(serde_json::json!({ "icon": icon })))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### 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).
|
- 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
|
### Changed
|
||||||
|
|||||||
@@ -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{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:hover,.iconcell[aria-selected="true"]{background:var(--raise);color:var(--fg)}
|
||||||
.iconcell .ficon{width:20px;height:20px}
|
.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
|
/* 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
|
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. */
|
folder's page, and 16 px left the triangle cramped against the art. */
|
||||||
|
|||||||
@@ -444,8 +444,8 @@ async function prefsModal(){
|
|||||||
<div class="inline"><input type="password" id="stskey" autocomplete="off" spellcheck="false"
|
<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'}">
|
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>
|
<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
|
<span class="hint">Signals pick what matters out of everything you subscribe to, on your own
|
||||||
Jev on your own key, from <a href="https://typesafe.ai" target="_blank" rel="noopener">typesafe.ai</a>.
|
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>
|
The key is kept on this server and never shown again.</span></div>
|
||||||
<div class="field boxed"><label for="tokname">API tokens</label>
|
<div class="field boxed"><label for="tokname">API tokens</label>
|
||||||
<div id="tokens"></div>
|
<div id="tokens"></div>
|
||||||
|
|||||||
@@ -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.
|
// 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>';
|
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.
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
for(const e of S.entries) box.appendChild(epEl(e));
|
for(const e of S.entries) box.appendChild(epEl(e));
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ function signalModal(sg?: Signal){
|
|||||||
<input type="text" id="sgname" value="${esc(sg?.name||'')}" placeholder="Formula 1"></div>
|
<input type="text" id="sgname" value="${esc(sg?.name||'')}" placeholder="Formula 1"></div>
|
||||||
<div id="sgpicker" hidden>
|
<div id="sgpicker" hidden>
|
||||||
<div class="inline"><input type="search" id="sgiconq" placeholder="Search icons: robot, car, music…" autocomplete="off">
|
<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>
|
<button type="button" class="dlink" id="sgnoicon">Initials</button></div>
|
||||||
<div class="icongrid" id="sgicons" role="listbox" aria-label="Icons"></div>
|
<div class="icongrid" id="sgicons" role="listbox" aria-label="Icons"></div>
|
||||||
</div></div>
|
</div></div>
|
||||||
@@ -147,10 +147,10 @@ function signalModal(sg?: Signal){
|
|||||||
<div class="chips" id="sgkw">${chipsHTML(sg?.keywords||[],'sgkwin')}</div></div>
|
<div class="chips" id="sgkw">${chipsHTML(sg?.keywords||[],'sgkwin')}</div></div>
|
||||||
<div class="field"><label for="sgdesc">In your own words (optional)</label>
|
<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">
|
<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
|
<span class="hint">Every item in the feeds you subscribe to is read, the last 30 days of them
|
||||||
days of them and each new one, and pulls out the ones about this subject: its name, these
|
and each new one, and the ones about this subject are pulled out: its name, these words and
|
||||||
words and your sentence together. The rest stays noise, in its own feed. Each item is one
|
your sentence together. The rest stays noise, in its own feed. Each item is one call on your
|
||||||
call on your TypeSafe key.${sg?' A change here reads everything again.':''}</span></div>
|
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>':''}
|
${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>
|
<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>`);
|
<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>('#sgnoicon').onclick=()=>{ icon=null; drawPick(); $('#sgpicker').hidden=true; };
|
||||||
$<HTMLButtonElement>('#sgjev').onclick=async()=>{
|
$<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{
|
try{
|
||||||
const got=await jevIcon($<HTMLInputElement>('#sgname').value.trim(),chipWords($('#sgkw')).filter((w): w is string=>!!w),
|
const got=await jevIcon($<HTMLInputElement>('#sgname').value.trim(),chipWords($('#sgkw')).filter((w): w is string=>!!w),
|
||||||
$<HTMLInputElement>('#sgdesc').value.trim()||null);
|
$<HTMLInputElement>('#sgdesc').value.trim()||null);
|
||||||
if(got){ icon=got; drawPick(); $('#sgpicker').hidden=true; $('#sgicon').setAttribute('aria-expanded','false'); }
|
if(got){ icon=got; drawPick(); $('#sgpicker').hidden=true; $('#sgicon').setAttribute('aria-expanded','false'); }
|
||||||
else toast('No icon matches those words');
|
else toast('No icon matches those words');
|
||||||
}catch(e){ toast(why(e),true); }
|
}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();
|
$<HTMLInputElement>('#sgname').focus();
|
||||||
$<HTMLButtonElement>('#sgsave').onclick=async()=>{
|
$<HTMLButtonElement>('#sgsave').onclick=async()=>{
|
||||||
@@ -204,7 +205,7 @@ function signalModal(sg?: Signal){
|
|||||||
if(!sg&&!icon) jevIcon(name,keywords,description)
|
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); })
|
.then(got=>{ if(got) return api(`/api/signals/${r.id}`,{method:'PATCH',body:JSON.stringify({icon:got})}).then(refreshSignals); })
|
||||||
.catch(()=>{});
|
.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.
|
// Open, so the new one is there to see.
|
||||||
if(!expanded.has(SIGNALS)) toggleGroup(SIGNALS);
|
if(!expanded.has(SIGNALS)) toggleGroup(SIGNALS);
|
||||||
await loadFeeds(true); selectFeed(':signal:'+(sg?sg.id:r.id));
|
await loadFeeds(true); selectFeed(':signal:'+(sg?sg.id:r.id));
|
||||||
|
|||||||
Reference in New Issue
Block a user