@playpilot/tpi 8.38.2 → 8.39.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.38.2",
3
+ "version": "8.39.0-beta.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -8,6 +8,7 @@ import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
8
8
  import { exploreModalUrl, participantUrl, titleUrl } from './routes'
9
9
  import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
10
10
  import { encodeHtmlEntities } from './html'
11
+ import type { ParticipantData } from './types/participant'
11
12
 
12
13
  export const keyDataAttribute = 'data-playpilot-injection-key'
13
14
  export const keySelector = `[${keyDataAttribute}]`
@@ -36,6 +37,11 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
36
37
  for (const injection of sortInjections(foundInjections)) {
37
38
  injectionIndex++
38
39
 
40
+ if (injection.type === 'participant' && !injection.manual && !doesParticipantMeetThreshold(mergedInjections, injection.participant_details!)) {
41
+ failedMessages[injection.key] = 'Participant does not meet popularity threshold.'
42
+ continue
43
+ }
44
+
39
45
  const elementIndex = elements.findIndex(element => cleanPhrase(element.innerText).includes(cleanPhrase(injection.sentence)))
40
46
  const element = elements[elementIndex]
41
47
 
@@ -477,3 +483,22 @@ export function hasValidTypeData(injection: LinkInjection): boolean {
477
483
  export function getPlayPilotWrapperElement(): Element {
478
484
  return document.querySelector('[data-playpilot-link-injections]') || document.body
479
485
  }
486
+
487
+ export function doesParticipantMeetThreshold(injections: LinkInjection[], participant: ParticipantData): boolean {
488
+ if (participant.popularity === null || participant.popularity === undefined) return true
489
+
490
+ const defaultThreshold = 50
491
+ const configThreshold = window.PlayPilotLinkInjections?.config?.participant_injection_popularity_threshold
492
+ const threshold = configThreshold ?? defaultThreshold
493
+ const useArticleAverage = !!window.PlayPilotLinkInjections?.config?.participant_injection_popularity_threshold_use_article_average
494
+
495
+ if (useArticleAverage) {
496
+ const averagePopularity = injections
497
+ .filter(i => i.type === 'participant')
498
+ .reduce((total, { participant_details }, _, { length }) => total + (participant_details?.popularity || 0) / length, 0)
499
+
500
+ return participant.popularity >= averagePopularity * (threshold / 100)
501
+ }
502
+
503
+ return participant.popularity >= threshold
504
+ }
@@ -104,6 +104,17 @@ export type ConfigResponse = {
104
104
  */
105
105
  open_tpi_links_in_explore?: boolean
106
106
 
107
+ /**
108
+ * Participants with a popularity score lower than this threshold will not be injected
109
+ */
110
+ participant_injection_popularity_threshold?: number
111
+
112
+ /**
113
+ * Whether or not to base the threshold on the article participants average popularity score.
114
+ * When enabled, this turns `participant_injection_popularity_threshold` into a percentage.
115
+ */
116
+ participant_injection_popularity_threshold_use_article_average?: boolean
117
+
107
118
  /**
108
119
  * These options are all relevant for the Explore component, which can be inserted as a widget on any page or as a modal.
109
120
  * `explore_navigation_selector` is used to select the navigation element that should be copied and inserted _after_.
@@ -17,6 +17,7 @@ export type ParticipantData = {
17
17
  }
18
18
  gender: string
19
19
  character?: string | null
20
+ popularity?: number | null
20
21
  }
21
22
 
22
23
  export type Job = 'actor' | 'writer' | 'director'
@@ -22,6 +22,7 @@
22
22
  let shown = $state(false)
23
23
  let interval: ReturnType<typeof setInterval> | null = null
24
24
  let railSize = $state(184)
25
+ let participantThreshold = $state(50)
25
26
 
26
27
  onDestroy(() => {
27
28
  if (interval) clearInterval(interval)
@@ -175,6 +176,28 @@
175
176
 
176
177
  <hr />
177
178
 
179
+ <div class="form-group">
180
+ <small>Participant threshold</small>
181
+ <input
182
+ type="range"
183
+ min={0}
184
+ max={100}
185
+ step={1}
186
+ bind:value={participantThreshold}
187
+ oninput={(event) => window.PlayPilotLinkInjections.config!.participant_injection_popularity_threshold = (event.target as HTMLFormElement).value} />
188
+ <small>{participantThreshold}</small>
189
+ </div>
190
+
191
+ <div class="form-group">
192
+ <label for="participant-threshold"><small>Participant use article average</small></label>
193
+ <input
194
+ id="participant-threshold"
195
+ type="checkbox"
196
+ oninput={(event) => window.PlayPilotLinkInjections.config!.participant_injection_popularity_threshold_use_article_average = (event.target as HTMLFormElement).checked} />
197
+ </div>
198
+
199
+ <hr />
200
+
178
201
  <button onclick={onrerender}>Re-inject</button>
179
202
  <button onclick={(event) => {
180
203
  if (confirm('You sure? Rerunning the AI will take some time, refresh whenever')) {
@@ -168,7 +168,7 @@
168
168
  {/each}
169
169
  </Rail>
170
170
  {:then titles}
171
- {@const titlesWithoutDuplicates = titles.filter((a, i, titles) => titles.findIndex(b => (b.sid === a.sid)) === i)}
171
+ {@const titlesWithoutDuplicates = titles?.filter((a, i, titles) => titles.findIndex(b => (b.sid === a.sid)) === i)}
172
172
 
173
173
  {#if titles?.length >= minimumLength}
174
174
  <Rail
@@ -1,7 +1,7 @@
1
1
  import { fireEvent } from '@testing-library/svelte'
2
2
  import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
3
3
 
4
- import { injectLinksInDocument, clearLinkInjections, clearLinkInjection, isAvailableAsManualInjection, filterRemovedAndInactiveInjections, isEquivalentInjection, filterInvalidInTextInjections, filterInvalidAfterArticleInjections, isValidInjection, isValidPlaylinkType, removePlayPilotTitleLinks, hasValidTypeData } from '$lib/injection'
4
+ import { injectLinksInDocument, clearLinkInjections, clearLinkInjection, isAvailableAsManualInjection, filterRemovedAndInactiveInjections, isEquivalentInjection, filterInvalidInTextInjections, filterInvalidAfterArticleInjections, isValidInjection, isValidPlaylinkType, removePlayPilotTitleLinks, hasValidTypeData, doesParticipantMeetThreshold } from '$lib/injection'
5
5
  import { mount, unmount } from 'svelte'
6
6
  import { fakeFetch, generateInjection } from '../helpers'
7
7
  import { openModalForInjectedLink } from '$lib/modal'
@@ -995,6 +995,50 @@ describe('injection.ts', () => {
995
995
 
996
996
  expect(document.querySelector('[data-playpilot-injection-key]')).toBeTruthy()
997
997
  })
998
+
999
+ it('Should ignore participants below a given popularity threshold', () => {
1000
+ window.PlayPilotLinkInjections.config = {
1001
+ participant_injection_popularity_threshold: 50,
1002
+ }
1003
+
1004
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
1005
+
1006
+ const elements = Array.from(document.body.querySelectorAll('p'))
1007
+
1008
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
1009
+ const injections = [
1010
+ { ...generateInjection('This is a sentence with an injection.', 'a sentence'), type: 'participant', participant_details: { ...participants[0], popularity: 20 } },
1011
+ { ...generateInjection('This is a sentence with an injection.', 'an injection'), type: 'participant', participant_details: { ...participants[0], popularity: 70 } },
1012
+ ]
1013
+
1014
+ // @ts-ignore
1015
+ injectLinksInDocument(elements, { aiInjections: injections, manualInjections: [] })
1016
+
1017
+ expect(document.querySelectorAll('[data-playpilot-injection-key]')).toHaveLength(1)
1018
+ })
1019
+
1020
+ it('Should ignore participants popularity for manual injections', () => {
1021
+ window.PlayPilotLinkInjections.config = {
1022
+ participant_injection_popularity_threshold: 50,
1023
+ }
1024
+
1025
+ document.body.innerHTML = '<p>This is a sentence with an injection.</p>'
1026
+
1027
+ const elements = Array.from(document.body.querySelectorAll('p'))
1028
+
1029
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
1030
+ const injections = [
1031
+ { ...generateInjection('This is a sentence with an injection.', 'a sentence'), type: 'participant', manual: true, participant_details: { ...participants[0], popularity: 20 } },
1032
+ { ...generateInjection('This is a sentence with an injection.', 'an injection'), type: 'participant', manual: true, participant_details: { ...participants[0], popularity: 70 } },
1033
+ ]
1034
+
1035
+ // @ts-ignore
1036
+ injectLinksInDocument(elements, { aiInjections: [], manualInjections: injections })
1037
+
1038
+ console.log(document.body.innerHTML)
1039
+
1040
+ expect(document.querySelectorAll('[data-playpilot-injection-key]')).toHaveLength(2)
1041
+ })
998
1042
  })
999
1043
 
1000
1044
  describe('config.open_tpi_links_in_explore', () => {
@@ -1240,6 +1284,7 @@ describe('injection.ts', () => {
1240
1284
  })
1241
1285
  })
1242
1286
 
1287
+
1243
1288
  describe('removePlayPilotTitleLinks', () => {
1244
1289
  it('Should remove existing title links', () => {
1245
1290
  document.body.innerHTML = `
@@ -1285,4 +1330,60 @@ describe('injection.ts', () => {
1285
1330
  expect(document.querySelector('a')).toBeTruthy()
1286
1331
  })
1287
1332
  })
1333
+
1334
+ describe('doesParticipantMeetThreshold', () => {
1335
+ it('Should return true or false if article average is used and participant is above or below average', () => {
1336
+ window.PlayPilotLinkInjections.config = {
1337
+ participant_injection_popularity_threshold: 75,
1338
+ participant_injection_popularity_threshold_use_article_average: true,
1339
+ }
1340
+
1341
+ const truthyParticipant = { ...participants[0], popularity: 70 }
1342
+ const falseyParticipant = { ...participants[0], popularity: 20 }
1343
+
1344
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
1345
+ const injections = [
1346
+ { ...generateInjection('This is a sentence with an injection.', 'an injection'), type: 'participant', participant_details: truthyParticipant },
1347
+ { ...generateInjection('This is a sentence with an injection.', 'an injection'), type: 'participant', participant_details: falseyParticipant },
1348
+ ]
1349
+
1350
+ expect(doesParticipantMeetThreshold(injections, truthyParticipant)).toBe(true)
1351
+ expect(doesParticipantMeetThreshold(injections, falseyParticipant)).toBe(false)
1352
+ })
1353
+
1354
+ it('Should return true or false if article average is not used and participant is above or below threshold', () => {
1355
+ window.PlayPilotLinkInjections.config = {
1356
+ participant_injection_popularity_threshold: 55,
1357
+ }
1358
+
1359
+ const truthyParticipant = { ...participants[0], popularity: 60 }
1360
+ const falseyParticipant = { ...participants[0], popularity: 40 }
1361
+
1362
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
1363
+ const injections = [
1364
+ { ...generateInjection('This is a sentence with an injection.', 'an injection'), type: 'participant', participant_details: truthyParticipant },
1365
+ { ...generateInjection('This is a sentence with an injection.', 'an injection'), type: 'participant', participant_details: falseyParticipant },
1366
+ ]
1367
+
1368
+ expect(doesParticipantMeetThreshold(injections, truthyParticipant)).toBe(true)
1369
+ expect(doesParticipantMeetThreshold(injections, falseyParticipant)).toBe(false)
1370
+ })
1371
+
1372
+ it('Should return true if threshold is 100, regardless of popularity', () => {
1373
+ window.PlayPilotLinkInjections.config = {
1374
+ participant_injection_popularity_threshold: 0,
1375
+ }
1376
+
1377
+ const truthyParticipant = { ...participants[0], popularity: 0 }
1378
+ const truthyParticipant2 = { ...participants[0], popularity: 60 }
1379
+
1380
+ /** @type {import('$lib/types/injection').LinkInjection[]} */
1381
+ const injections = [
1382
+ { ...generateInjection('This is a sentence with an injection.', 'an injection'), type: 'participant', participant_details: truthyParticipant },
1383
+ ]
1384
+
1385
+ expect(doesParticipantMeetThreshold(injections, truthyParticipant)).toBe(true)
1386
+ expect(doesParticipantMeetThreshold(injections, truthyParticipant2)).toBe(true)
1387
+ })
1388
+ })
1288
1389
  })
@@ -36,7 +36,7 @@ describe('TitlesRail.svelte', () => {
36
36
  })
37
37
 
38
38
  it('Should render all titles', async () => {
39
- const { getAllByTestId } = render(TitlesRail, { titles: [title, title] })
39
+ const { getAllByTestId } = render(TitlesRail, { titles: [{ ...title, sid: '1' }, { ...title, sid: '2' }] })
40
40
 
41
41
  expect(getAllByTestId('title')).toHaveLength(2)
42
42
  })
@@ -202,13 +202,13 @@ describe('TitlesRail.svelte', () => {
202
202
  })
203
203
 
204
204
  it('Should not render rails if fewer than given mimimumLength of titles were returned', () => {
205
- const { container } = render(TitlesRail, { titles: [title, title], minimumLength: 3 })
205
+ const { container } = render(TitlesRail, { titles: [{ ...title, sid: '1' }, { ...title, sid: '2' }], minimumLength: 3 })
206
206
 
207
207
  expect(container.querySelector('.titles .slider')).not.toBeTruthy()
208
208
  })
209
209
 
210
210
  it('Should render rails if equal or more than given mimimumLength of titles were returned', () => {
211
- const { container } = render(TitlesRail, { titles: [title, title, title], minimumLength: 3 })
211
+ const { container } = render(TitlesRail, { titles: [{ ...title, sid: '1' }, { ...title, sid: '2' }, { ...title, sid: '3' }], minimumLength: 3 })
212
212
 
213
213
  expect(container.querySelector('.titles .slider')).toBeTruthy()
214
214
  })