@playpilot/tpi 8.38.3 → 8.39.0-beta.2

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.3",
3
+ "version": "8.39.0-beta.2",
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 useArticleAverage = !!window.PlayPilotLinkInjections?.config?.participant_injection_popularity_threshold_use_article_average
491
+ const defaultThreshold = useArticleAverage ? 50 : 1000
492
+ const configThreshold = window.PlayPilotLinkInjections?.config?.participant_injection_popularity_threshold
493
+ const threshold = configThreshold ?? defaultThreshold
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,8 @@
22
22
  let shown = $state(false)
23
23
  let interval: ReturnType<typeof setInterval> | null = null
24
24
  let railSize = $state(184)
25
+ let participantsUseAverage = $state(!!window.PlayPilotLinkInjections!.config?.participant_injection_popularity_threshold_use_article_average)
26
+ let participantThreshold = $derived(window.PlayPilotLinkInjections!.config?.participant_injection_popularity_threshold || (participantsUseAverage ? 50 : 1_000))
25
27
 
26
28
  onDestroy(() => {
27
29
  if (interval) clearInterval(interval)
@@ -32,6 +34,7 @@
32
34
 
33
35
  const succesfulInjections = data.evaluated_link_injections?.filter(injection => !injection.failed) || []
34
36
  const failedInjections = data.evaluated_link_injections?.filter(injection => injection.failed) || []
37
+ const participantsInjections = succesfulInjections.filter(injection => injection.type === 'participant')
35
38
 
36
39
  const visiblePixels = Array.from(document.querySelectorAll<HTMLImageElement>('[data-playpilot-pixel]'))
37
40
 
@@ -44,6 +47,7 @@
44
47
  'API Config': Object.entries(window.PlayPilotLinkInjections.config || {}).map(([label, data]) => ({ label, data })),
45
48
  [`Succesful injections (${succesfulInjections.length})`]: succesfulInjections.map(injection => ({ label: injection.title, data: injection.sentence })),
46
49
  [`Failed injections (${failedInjections.length})`]: failedInjections.map(injection => ({ label: injection.title, data: `Reason: ${injection.failed_message} | Sentence: ${injection.sentence}` })),
50
+ [`Participant popularity (${participantsInjections.length})`]: participantsInjections.map(injection => ({ label: injection.participant_details!.name, data: injection.participant_details!.popularity || 'Not set' })),
47
51
  [`Fetched ads (${data.ads?.length || 0})`]: data.ads?.map(ad => ({ label: ad.campaign_name, data: ad })),
48
52
  [`Tracking events (${data.tracked_events?.length || 0})`]: data.tracked_events?.map(event => ({ label: event.event, data: event.payload })),
49
53
  [`Visible pixels (${visiblePixels.length})`]: visiblePixels.map((pixel, index) => ({ label: index + 1, data: pixel.src })),
@@ -175,6 +179,29 @@
175
179
 
176
180
  <hr />
177
181
 
182
+ <div class="form-group">
183
+ <label for="participant-threshold"><small>Participant use article average</small></label>
184
+ <input
185
+ id="participant-threshold"
186
+ type="checkbox"
187
+ bind:checked={participantsUseAverage}
188
+ oninput={(event) => window.PlayPilotLinkInjections.config!.participant_injection_popularity_threshold_use_article_average = (event.target as HTMLFormElement).checked} />
189
+ </div>
190
+
191
+ <div class="form-group">
192
+ <small>Participant threshold</small>
193
+ <input
194
+ type="range"
195
+ min={0}
196
+ max={participantsUseAverage ? 100 : 10_000}
197
+ step={1}
198
+ bind:value={participantThreshold}
199
+ oninput={(event) => window.PlayPilotLinkInjections.config!.participant_injection_popularity_threshold = (event.target as HTMLFormElement).value} />
200
+ <small>{participantThreshold}</small>
201
+ </div>
202
+
203
+ <hr />
204
+
178
205
  <button onclick={onrerender}>Re-inject</button>
179
206
  <button onclick={(event) => {
180
207
  if (confirm('You sure? Rerunning the AI will take some time, refresh whenever')) {
@@ -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
  })