@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/dist/editorial.mount.js +7 -7
- package/dist/link-injections.js +2 -2
- package/dist/mount.js +5 -5
- package/package.json +1 -1
- package/src/lib/injection.ts +25 -0
- package/src/lib/types/config.d.ts +11 -0
- package/src/lib/types/participant.d.ts +1 -0
- package/src/routes/components/Debugger.svelte +27 -0
- package/src/tests/lib/injection.test.js +102 -1
package/package.json
CHANGED
package/src/lib/injection.ts
CHANGED
|
@@ -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_.
|
|
@@ -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
|
})
|