jevstrudel.git / website / src / jev / picker.mjs
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}