1// Jev choosing songs and judging code, outside the booth: the mood picker 2// and radio on the welcome tab, and the header's "is it art?". Each is one 3// call through the relay (ask.mjs). 4import { askJev } from './ask.mjs'; 5import { artOf, artRequest, songCode } from './critic.mjs'; 6 7const MOOD_MAX = 280; 8 9// A Choice among songs: the option is the song's id, described by its 10// title and README paragraph. 11const songCriteria = (songs) => 12 Object.fromEntries(songs.map((s) => [s.id, `${s.title}: ${s.description || s.theme}`.slice(0, 400)])); 13 14function chosen(answer, songs) { 15 const song = songs.find((s) => s.id === answer?.choice); 16 if (!song) throw new Error(`Jev answered "${answer?.choice}", which is not a song`); 17 return { song, p: answer.probabilities?.[song.id], confidence: answer.confidence }; 18} 19 20export async function pickByMood(songs, mood) { 21 const playable = songs.filter((s) => s.code); 22 const answers = await askJev( 23 { 24 song: { 25 type: 'choice', 26 instructions: 27 'A listener said how they feel (`mood`). Choose the song that fits them best: match the mood, or ' + 28 'lift it if that is what they are asking for.', 29 criteria: songCriteria(playable), 30 }, 31 }, 32 { mood: mood.slice(0, MOOD_MAX) }, 33 ); 34 return chosen(answers.song, playable); 35} 36 37// The songs the radio may pick from when a signed-in listener has a 38// history (radioHistory.mjs): `playable` without the most recently played 39// ones (`recent`, song ids newest first), leaving at least MIN_CHOICES, so 40// a long history narrows the choice but never empties it. 41export const MIN_CHOICES = 3; 42export function notRecent(playable, recent) { 43 const avoid = new Set(); 44 for (const id of recent) { 45 if (!playable.some((s) => s.id === id) || avoid.has(id)) continue; 46 if (playable.length - avoid.size <= MIN_CHOICES) break; 47 avoid.add(id); 48 } 49 return playable.filter((s) => !avoid.has(s.id)); 50} 51 52// Autoplay and the next-track button: what plays after `current` (a site 53// song, or null when the editor holds something else: an edited song, a 54// listener's, the listener's own code), given what has played in this tab 55// (`history`, titles, oldest first). Signed in, `recent` is the listener's 56// own history across sessions and devices (song ids, newest first), and the 57// most recent of those are not offered at all (notRecent). 58export async function pickNext(songs, current, history, { recent } = {}) { 59 const others = songs.filter((s) => s.code && s.id !== current?.id); 60 const playable = recent ? notRecent(others, recent) : others; 61 const answers = await askJev( 62 { 63 song: { 64 type: 'choice', 65 instructions: current 66 ? 'You are the DJ of a radio station playing only these songs. One just played (`justPlayed`). Choose ' + 67 'the next: keep the energy moving, vary the style, and avoid anything in `playedRecently`.' 68 : 'You are the DJ of a radio station playing only these songs. Choose the next for this listener from ' + 69 'what they have played (`playedRecently`, oldest first): vary the style, and avoid anything in it.', 70 criteria: songCriteria(playable), 71 }, 72 }, 73 { 74 ...(current ? { justPlayed: `${current.title}: ${current.description}` } : {}), 75 playedRecently: history.slice(-5), 76 }, 77 ); 78 return chosen(answers.song, playable); 79} 80 81// The art critic on whatever is in the editor: the same question, and the 82// same state shape, as the scores on the song cards (tools/critic/). 83// Resolves to `{ art, criteria }` (critic.mjs). 84export async function judgeArt({ title, spec, code, measured = null }) { 85 const { questions, state } = artRequest({ title, spec, code: songCode(code), measured }); 86 const verdict = artOf(await askJev(questions, state)); 87 if (!verdict) throw new Error('Jev left a criterion unscored'); 88 return verdict; 89}