@playpilot/tpi 3.1.0 → 3.2.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/link-injections.js +8 -7
- package/package.json +1 -1
- package/src/lib/actions/heading.ts +11 -0
- package/src/lib/api.ts +1 -3
- package/src/lib/auth.ts +13 -1
- package/src/lib/constants.ts +2 -0
- package/src/lib/enums/TrackingEvent.ts +1 -0
- package/src/lib/linkInjection.ts +43 -32
- package/src/lib/scss/_mixins.scss +8 -0
- package/src/lib/scss/global.scss +6 -6
- package/src/lib/scss/variables.scss +2 -0
- package/src/lib/stores/organization.ts +4 -0
- package/src/lib/tracking.ts +14 -1
- package/src/lib/types/injection.d.ts +5 -0
- package/src/routes/+layout.svelte +6 -2
- package/src/routes/+page.svelte +31 -13
- package/src/routes/components/Description.svelte +2 -3
- package/src/routes/components/Editorial/AIIndicator.svelte +85 -91
- package/src/routes/components/Editorial/Alert.svelte +12 -2
- package/src/routes/components/Editorial/Editor.svelte +82 -61
- package/src/routes/components/Editorial/EditorItem.svelte +32 -7
- package/src/routes/components/Editorial/ManualInjection.svelte +10 -9
- package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +14 -0
- package/src/routes/components/Editorial/ResizeHandle.svelte +1 -1
- package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +7 -5
- package/src/routes/components/Icons/IconWarning.svelte +5 -0
- package/src/routes/components/Modal.svelte +2 -0
- package/src/routes/components/Playlinks.svelte +12 -11
- package/src/routes/components/Popover.svelte +2 -0
- package/src/routes/components/Title.svelte +3 -2
- package/src/routes/components/TitlePopover.svelte +1 -1
- package/src/tests/lib/auth.test.js +31 -1
- package/src/tests/lib/linkInjection.test.js +87 -48
- package/src/tests/lib/tracking.test.js +61 -1
- package/src/tests/routes/+page.test.js +94 -4
- package/src/tests/routes/components/Editorial/AiIndicator.test.js +28 -42
- package/src/tests/routes/components/Editorial/Alert.test.js +10 -3
- package/src/tests/routes/components/Editorial/Editor.test.js +15 -0
- package/src/tests/routes/components/Editorial/EditorItem.test.js +32 -7
- package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +13 -1
- package/src/tests/routes/components/Title.test.js +2 -2
- package/svelte.config.js +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heading styling of actual `<h1>`, `<h2>`, etc, tags is often styled pretty specifically on pages.
|
|
3
|
+
* Our elements are affected by the pages styling and this styling can be hard to override.
|
|
4
|
+
* By always using a <div> we lower the chance of any unexpected styling hitting us.
|
|
5
|
+
* We still want headings to be semantically correct, which is all this action does.
|
|
6
|
+
* @example `use:heading{3}`
|
|
7
|
+
*/
|
|
8
|
+
export function heading(node: HTMLElement, level = 1) {
|
|
9
|
+
node.role = "heading"
|
|
10
|
+
node.ariaLevel = level.toString()
|
|
11
|
+
}
|
package/src/lib/api.ts
CHANGED
|
@@ -14,11 +14,10 @@ let pollTimeout: ReturnType<typeof setTimeout> | null = null
|
|
|
14
14
|
* @param url URL of the given article
|
|
15
15
|
* @param html HTML to be crawled
|
|
16
16
|
* @param options
|
|
17
|
-
* @param [options.automation] Enable automation, disable when inserting into editorial
|
|
18
17
|
* @param [options.hash] unique key to identify the HTML
|
|
19
18
|
* @param [options.params] Any rest params to include in the request body
|
|
20
19
|
*/
|
|
21
|
-
export async function fetchLinkInjections(url: string, html: string, { hash = stringToHash(html), params = {} }: {
|
|
20
|
+
export async function fetchLinkInjections(url: string, html: string, { hash = stringToHash(html), params = {} }: { hash?: string; params?: object } = {}): Promise<LinkInjectionResponse> {
|
|
22
21
|
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
23
22
|
const apiToken = getApiToken()
|
|
24
23
|
const isEditorialMode = isEditorialModeEnabled() ? await authorize() : false
|
|
@@ -113,7 +112,6 @@ export async function saveLinkInjections(linkInjections: LinkInjection[], html:
|
|
|
113
112
|
const response = await fetchLinkInjections(getFullUrlPath(), html, {
|
|
114
113
|
params: {
|
|
115
114
|
private_token: getAuthToken(),
|
|
116
|
-
automation_enabled: false,
|
|
117
115
|
link_injections: newLinkInjections,
|
|
118
116
|
text_selector: selector,
|
|
119
117
|
},
|
package/src/lib/auth.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getApiToken } from './api'
|
|
2
2
|
import { apiBaseUrl } from './constants'
|
|
3
|
+
import { TrackingEvent } from './enums/TrackingEvent'
|
|
4
|
+
import { track } from './tracking'
|
|
3
5
|
|
|
4
6
|
const cookieName = 'EncryptedToken'
|
|
5
7
|
const urlParam = 'articleReplacementEditToken'
|
|
@@ -32,8 +34,11 @@ export async function authorize(href: string = window.location.href): Promise<bo
|
|
|
32
34
|
setAuthCookie(authToken)
|
|
33
35
|
|
|
34
36
|
return true
|
|
35
|
-
} catch(error) {
|
|
37
|
+
} catch(error: any) {
|
|
36
38
|
console.error(error)
|
|
39
|
+
track(TrackingEvent.AuthFailed)
|
|
40
|
+
} finally {
|
|
41
|
+
removeAuthParamFromUrl()
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
return false
|
|
@@ -84,3 +89,10 @@ export function isEditorialModeEnabled(): boolean {
|
|
|
84
89
|
const windowToken = window?.PlayPilotLinkInjections?.editorial_token
|
|
85
90
|
return new URLSearchParams(window.location.search).get('playpilot-editorial-mode') === 'true' || !!windowToken
|
|
86
91
|
}
|
|
92
|
+
|
|
93
|
+
export function removeAuthParamFromUrl(): void {
|
|
94
|
+
const url = new URL(window.location.href)
|
|
95
|
+
url.searchParams.delete(urlParam)
|
|
96
|
+
|
|
97
|
+
window.history.replaceState({}, '', url)
|
|
98
|
+
}
|
package/src/lib/constants.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export const playPilotBaseUrl = 'https://www.playpilot.com'
|
|
2
2
|
export const apiBaseUrl = 'https://partner-api.playpilot.tech/1.0.0'
|
|
3
|
+
|
|
4
|
+
export const imagePlaceholderDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFQAAAB+CAMAAACNgsajAAAAb1BMVEU1Q2fI1N5YZoNVYoFndI9qd5JBT3FFU3S8yNS3xNC4xNFBTnDG0t2lscJJV3e9ytaptcWToLOOm6/BzdiZprh3hJ1aaIWJlatte5VPXHx7iJ9zgZlfbYk3RWmNmq5jcY1XZIO2wtCjsMB+i6I9S25IprTeAAABqUlEQVRo3u3W2ZKCMBCF4T5ExRmRVXBfZnn/Z5wKiU5pz1AJ7ZXV/x03HxVCB0jTNE3TNE3TNO0l2rbPNxcFdk8334AINTUD5eSaWbPoQs0qw0CVN98BzNNgE4NVv+ZHsJliuNVt7UgotATAafp/5mZiG4waAB0N5k0kUeg0wMykKDfLvRTl5pxyCFFupjQVo9ykiRTlphzl5nNQNu8C9Hv2lylDT0W2NMyUoeXdLC68KUNLuM7O9HskQ0uLLAEUR2aOQjfORE5LzHP2PMehxpl2k6otM8eh2aQGkBlieyRBYbs3y5Rk6IPZn8mT65XJR6LcROGErwaoxqLm4XvkiTVsy1FoYe5n06zcjazp1Wk0umHz3k9BT6+bXjXR6PnRtKpr755PfRjx4WPz7tXW/26gGYnOvOmrM7xtiK4qYtDOrpGZtnR7JFcSi+Z1XZt7k5d4NCZmcrWxqMzkbRgqN+nAULHpx1RiylFvftJwlxjUz2bWdpPB7NnSxZih5RFrD20Vai4izGOgeenPukMSUE6hte5denI7NMyU1xrSNE3TNE3TNE17hX4ADHsS0j0OCOoAAAAASUVORK5CYII='
|
package/src/lib/linkInjection.ts
CHANGED
|
@@ -97,14 +97,15 @@ export function injectLinksInDocument(elements: HTMLElement[], onclick: (LinkInj
|
|
|
97
97
|
|
|
98
98
|
// Find injection in text content of all elements together, ignore potential HTML elements.
|
|
99
99
|
// This is to filter out injections that can't be injected anyway.
|
|
100
|
-
const fullText = elements.map(element => element.innerText).join(' ')
|
|
100
|
+
const fullText = cleanPhrase(elements.map(element => element.innerText).join(' '))
|
|
101
101
|
|
|
102
102
|
const validInjections = filterInvalidInTextInjections(mergedInjections)
|
|
103
103
|
const foundInjections = validInjections.filter(i => {
|
|
104
|
-
return
|
|
104
|
+
return fullText.includes(cleanPhrase(i.sentence))
|
|
105
105
|
})
|
|
106
106
|
|
|
107
107
|
const ranges: LinkInjectionRanges = {}
|
|
108
|
+
const failedMessages: Record<string, string> = {}
|
|
108
109
|
|
|
109
110
|
for (const injection of foundInjections) {
|
|
110
111
|
const elementIndex = elements.findIndex(element => cleanPhrase(element.innerText).includes(cleanPhrase(injection.sentence)))
|
|
@@ -113,8 +114,18 @@ export function injectLinksInDocument(elements: HTMLElement[], onclick: (LinkInj
|
|
|
113
114
|
if (!element) continue
|
|
114
115
|
|
|
115
116
|
const nodeContainingText = findTextNodeContaining(injection.title, element, ['A'])
|
|
117
|
+
|
|
116
118
|
// Ignore if the found injection has no node or if it is inside a link.
|
|
117
|
-
if (!nodeContainingText || isNodeInLink(nodeContainingText))
|
|
119
|
+
if (!nodeContainingText?.nodeValue || isNodeInLink(nodeContainingText)) {
|
|
120
|
+
// We check once more where the text was found, this time without ignoring links
|
|
121
|
+
// so we can determine if the failure was due to it being in a link
|
|
122
|
+
const linkNodeContainingText = findTextNodeContaining(injection.title, element)
|
|
123
|
+
if (linkNodeContainingText && isNodeInLink(linkNodeContainingText)) {
|
|
124
|
+
failedMessages[injection.key] = 'Given text is already inside of a link.'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
118
129
|
|
|
119
130
|
// Create a wrapper in which the link will be placed. This wrapper exists as a parent for the popover
|
|
120
131
|
// so that it is not directly inside of the link.
|
|
@@ -131,9 +142,16 @@ export function injectLinksInDocument(elements: HTMLElement[], onclick: (LinkInj
|
|
|
131
142
|
linkWrapperElement.insertAdjacentElement('beforeend', linkElement)
|
|
132
143
|
|
|
133
144
|
// Replace starting from the end of the previously injected link, making sure injections with the same or overlapping
|
|
134
|
-
// phrases can still be injected.
|
|
145
|
+
// phrases can still be injected. Additionally we check the index of where the node value starts. This can differentiate from
|
|
146
|
+
// the actual start if it was partially matched with manual injections. If that martial match contains a phrase that also
|
|
147
|
+
// occurs in the full sentence before it, it would incorrectly match against that. If that happens to be a link already,
|
|
148
|
+
// it would break that link with newly inserted HTML.
|
|
135
149
|
const startingIndex = getLargestValueInArray(Object.values(ranges).filter(r => r.elementIndex === elementIndex).map(r => r.to))
|
|
136
|
-
|
|
150
|
+
const valueIndex = element.innerHTML.indexOf(nodeContainingText.nodeValue)
|
|
151
|
+
const sentenceIndex = element.innerHTML.indexOf(injection.sentence)
|
|
152
|
+
const highestIndex = Math.max(startingIndex, valueIndex, sentenceIndex, 0)
|
|
153
|
+
|
|
154
|
+
element.innerHTML = replaceStartingFrom(element.innerHTML, injection.title, linkWrapperElement.outerHTML, highestIndex)
|
|
137
155
|
|
|
138
156
|
const from = element.innerHTML.indexOf(linkWrapperElement.outerHTML)
|
|
139
157
|
|
|
@@ -150,18 +168,23 @@ export function injectLinksInDocument(elements: HTMLElement[], onclick: (LinkInj
|
|
|
150
168
|
const afterArticleInjections = filterInvalidAfterArticleInjections(mergedInjections)
|
|
151
169
|
if (afterArticleInjections.length) insertAfterArticlePlaylinks(elements, afterArticleInjections, onclick)
|
|
152
170
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return sortedInjections.filter(i => i.title_details).map((injection, index) => {
|
|
156
|
-
const failed = !injection.inactive && !injection.after_article && !document.querySelector(`[${keyDataAttribute}="${injection.key}"]`)
|
|
171
|
+
return mergedInjections.filter(i => i.title_details).map((injection, index) => {
|
|
157
172
|
// Favour manual injections over AI injections
|
|
158
173
|
const duplicate = injection.duplicate ?? (!injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections))
|
|
159
174
|
|
|
175
|
+
const matchingElement = document.querySelector(`[${keyDataAttribute}="${injection.key}"]`)
|
|
176
|
+
const failed = isValidPlaylinkType(injection) && !injection.inactive && !injection.after_article && !matchingElement
|
|
177
|
+
const failedMessage =
|
|
178
|
+
!failed ? '' :
|
|
179
|
+
failedMessages[injection.key] ||
|
|
180
|
+
(!fullText.includes(cleanPhrase(injection.sentence)) ? 'Given sentence was not found in the article.' : 'The link failed to inject for unknown reasons.')
|
|
181
|
+
|
|
160
182
|
return {
|
|
161
183
|
...injection,
|
|
162
184
|
inactive: injection.inactive ?? false,
|
|
163
185
|
duplicate,
|
|
164
186
|
failed,
|
|
187
|
+
failed_message: failedMessage
|
|
165
188
|
}
|
|
166
189
|
})
|
|
167
190
|
}
|
|
@@ -325,32 +348,11 @@ export function clearLinkInjection(key: string): void {
|
|
|
325
348
|
if (element) element.outerHTML = element.textContent || ''
|
|
326
349
|
}
|
|
327
350
|
|
|
328
|
-
/**
|
|
329
|
-
* Sort injections by where they were inserted. First by their element index, second by where in the element the
|
|
330
|
-
* injection was injected. Injections without range (after article injections or failed injection) go last.
|
|
331
|
-
*/
|
|
332
|
-
export function sortLinkInjectionsByRange(injections: LinkInjection[], ranges: LinkInjectionRanges): LinkInjection[] {
|
|
333
|
-
return injections.sort((a, b) => {
|
|
334
|
-
const rangeA = ranges[a.key]
|
|
335
|
-
const rangeB = ranges[b.key]
|
|
336
|
-
|
|
337
|
-
if (!rangeA && rangeB) return 1
|
|
338
|
-
if (rangeA && !rangeB) return -1
|
|
339
|
-
if (!rangeA && !rangeB) return 0
|
|
340
|
-
|
|
341
|
-
if (rangeA?.elementIndex !== rangeB?.elementIndex) {
|
|
342
|
-
return (rangeA?.elementIndex || 0) - (rangeB?.elementIndex || 0)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return (rangeA?.from || 0) - (rangeB?.from || 0)
|
|
346
|
-
})
|
|
347
|
-
}
|
|
348
|
-
|
|
349
351
|
/**
|
|
350
352
|
* Merge different injection types
|
|
351
353
|
*/
|
|
352
354
|
export function mergeInjectionTypes({ aiInjections, manualInjections }: LinkInjectionTypes): LinkInjection[] {
|
|
353
|
-
return [...
|
|
355
|
+
return [...manualInjections.map(i => ({ ...i, manual: true })), ...aiInjections]
|
|
354
356
|
}
|
|
355
357
|
|
|
356
358
|
/**
|
|
@@ -367,7 +369,16 @@ export function separateLinkInjectionTypes(injections: LinkInjection[]): LinkInj
|
|
|
367
369
|
* Returns whether or not an injection would be valid for any sort of injection, text or after_article
|
|
368
370
|
*/
|
|
369
371
|
export function isValidInjection(injection: LinkInjection): boolean {
|
|
370
|
-
return !injection.inactive && !injection.removed && !injection.duplicate && !!injection.title_details
|
|
372
|
+
return !injection.inactive && !injection.removed && !injection.duplicate && !!injection.title_details && isValidPlaylinkType(injection)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* An injection can have be of various playlink types, when all are false equivalent, the link is not valid.
|
|
377
|
+
* It should be treated similar to an inactive playlink in this case.
|
|
378
|
+
*/
|
|
379
|
+
export function isValidPlaylinkType(injection: LinkInjection): boolean {
|
|
380
|
+
if (injection.in_text || injection.in_text === undefined) return true
|
|
381
|
+
return !!injection.after_article
|
|
371
382
|
}
|
|
372
383
|
|
|
373
384
|
/**
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Annoyingly, some pages include global styling to add a fill to every svg.
|
|
2
|
+
// We don't want that, but we can't just apply global styling either, as that would affect their page.
|
|
3
|
+
// So this mixins is to be used inside of other containers.
|
|
4
|
+
@mixin reset-svg() {
|
|
5
|
+
:global(svg) {
|
|
6
|
+
fill: none;
|
|
7
|
+
}
|
|
8
|
+
}
|
package/src/lib/scss/global.scss
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[data-playpilot-injection-key] {
|
|
4
4
|
position: relative;
|
|
5
|
+
}
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
7
|
+
.playpilot-injection-highlight {
|
|
8
|
+
outline: margin(0.25) solid var(--playpilot-primary) !important;
|
|
9
|
+
outline-offset: margin(0.5) !important;
|
|
10
|
+
border-radius: margin(0.05);
|
|
11
|
+
scroll-margin: margin(5);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
.playpilot-styled-scrollbar {
|
package/src/lib/tracking.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { get } from "svelte/store"
|
|
2
|
+
import { currentDomainSid, currentOrganizationSid } from "./stores/organization"
|
|
1
3
|
import type { TitleData } from "./types/title"
|
|
2
4
|
|
|
3
5
|
const baseUrl = 'https://insights.playpilot.net'
|
|
@@ -9,7 +11,7 @@ const baseUrl = 'https://insights.playpilot.net'
|
|
|
9
11
|
* @param [title] Title related to the event
|
|
10
12
|
* @param [payload] Any data that will be included with the event
|
|
11
13
|
*/
|
|
12
|
-
export async function track(event: string, title: TitleData | null = null, payload: Record<string, string | number> = {}): Promise<void> {
|
|
14
|
+
export async function track(event: string, title: TitleData | null = null, payload: Record<string, string | number | null> = {}): Promise<void> {
|
|
13
15
|
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
14
16
|
|
|
15
17
|
if (title) {
|
|
@@ -21,6 +23,8 @@ export async function track(event: string, title: TitleData | null = null, paylo
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
payload.url = window.location.href
|
|
26
|
+
payload.organization_sid = get(currentOrganizationSid)
|
|
27
|
+
payload.domain_sid = get(currentDomainSid)
|
|
24
28
|
|
|
25
29
|
fetch(baseUrl, {
|
|
26
30
|
headers,
|
|
@@ -32,3 +36,12 @@ export async function track(event: string, title: TitleData | null = null, paylo
|
|
|
32
36
|
})),
|
|
33
37
|
})
|
|
34
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set the sid of the organization and domain to a store..
|
|
42
|
+
* These are saved for tracking purposes and currently serve no other function.
|
|
43
|
+
*/
|
|
44
|
+
export function setTrackingSids({ organizationSid = null, domainSid = null }: { organizationSid?: string | null, domainSid?: string | null }): void {
|
|
45
|
+
currentOrganizationSid.set(organizationSid || null)
|
|
46
|
+
currentDomainSid.set(domainSid || null)
|
|
47
|
+
}
|
|
@@ -9,6 +9,7 @@ export type LinkInjection = {
|
|
|
9
9
|
title_details?: TitleData
|
|
10
10
|
inactive?: boolean
|
|
11
11
|
failed?: boolean
|
|
12
|
+
failed_message?: string
|
|
12
13
|
in_text?: boolean
|
|
13
14
|
after_article?: boolean
|
|
14
15
|
after_article_style?: 'modal_button' | 'playlinks' | null
|
|
@@ -26,7 +27,11 @@ export type LinkInjectionResponse = {
|
|
|
26
27
|
injections_enabled: boolean
|
|
27
28
|
ai_running: boolean
|
|
28
29
|
ai_injections: LinkInjection[] | null
|
|
30
|
+
ai_progress_message: string
|
|
31
|
+
ai_progress_percentage: number
|
|
29
32
|
link_injections: LinkInjection[] | null
|
|
33
|
+
organization_sid?: string
|
|
34
|
+
domain_sid?: string
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export type LinkInjectionTypes = {
|
|
@@ -27,12 +27,16 @@
|
|
|
27
27
|
<h1 use:noClass>Some heading</h1>
|
|
28
28
|
<time datetime="14:00">1 hour ago</time>
|
|
29
29
|
<p use:noClass>Following the success of John M. Chu's 2018 romantic-comedy Crazy Rich Asians, Quan was inspired to return to acting. He first scored a supporting role in the Netflix movie Finding 'Ohana, before securing a starring role in the absurdist comedy-drama Everything Everywhere all At Once. A critical and commercial success, the film earned $143 million against a budget of $14-25 million, and saw Quan win the Academy Award for Best Supporting Actor. Following his win, Quan struggled to choose projects he was satisfied with, passing on an action-comedy three times, before finally taking his first leading role in it, following advice from Spielberg.</p>
|
|
30
|
-
<p use:noClass>In an interview with Epire & Magazine, Quan reveals he quested starring in Love Hurts, which sees him in the leading role of a former assassin turned successful realtor, whose past returns when his brother attempts to hunt him down. The movie is in a similar vein to successful films such as The Long Kiss Goodnight and Nobody, and Quan discussed how he was reluctant to take the part due to his conditioned beliefs about how an action hero should look. But he reveals that he changed his mind following a meeting with Spielberg, who convinced him to do it.</p>
|
|
30
|
+
<p use:noClass>In an interview with Epire & Magazine, Quan reveals he quested starring in Love Hurts, which sees him Love Hurts in the leading role of a former assassin turned successful realtor, whose past returns when his brother attempts to hunt him down. The movie is in a similar vein to successful films such as The Long Kiss Goodnight and Nobody, and Quan discussed how he was reluctant to take the part due to his conditioned beliefs about how an action hero should look. But he reveals that he changed his mind following a meeting with Spielberg, who convinced him to do it.</p>
|
|
31
31
|
<p use:noClass><strong use:noClass>Jason Momoa</strong> (”Aquaman”), <strong use:noClass>Jack Black</strong> (”Nacho Libre”) och <strong use:noClass>Jennifer Coolidge</strong> (”The White Lotus”) medverkar i den <strong use:noClass>Jared Hess</strong>-regisserade (”Napolen Dynamite”) filmen. Filmen följer fyra utbölingar som via en magisk portal sugs in i en värld där allt är kubformat. För att komma hem igen måste de övervinna den färgstarka världen.</p>
|
|
32
32
|
|
|
33
|
+
<p use:noClass>
|
|
34
|
+
Following their post-credits scene in <a use:noClass href="/">John Wick</a>, in a new John Wick spinoff.
|
|
35
|
+
</p>
|
|
36
|
+
|
|
33
37
|
<ul use:noClass>
|
|
34
38
|
<li use:noClass><strong use:noClass>Winner:</strong> The Zone of Interest</li>
|
|
35
|
-
<li use:noClass>Oppenheimer</li>
|
|
39
|
+
<li use:noClass>Oppenheimer and Oppenheimer and Oppenheimer, and Oppenheimer</li>
|
|
36
40
|
<li use:noClass>Past Lives</li>
|
|
37
41
|
<li use:noClass>Anatomy of a Fall</li>
|
|
38
42
|
<li use:noClass>Killers of the Flower Moon</li>
|
package/src/routes/+page.svelte
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { onMount } from 'svelte'
|
|
3
3
|
import { fetchConfig, pollLinkInjections } from '$lib/api'
|
|
4
4
|
import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/linkInjection'
|
|
5
|
-
import { track } from '$lib/tracking'
|
|
5
|
+
import { setTrackingSids, track } from '$lib/tracking'
|
|
6
6
|
import { getFullUrlPath } from '$lib/url'
|
|
7
7
|
import { isCrawler } from '$lib/crawler'
|
|
8
8
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
@@ -30,15 +30,16 @@
|
|
|
30
30
|
|
|
31
31
|
// Rerender link injections when linkInjections change. This is only relevant for editiorial mode.
|
|
32
32
|
$effect(() => {
|
|
33
|
-
if (isEditorialMode) rerender()
|
|
33
|
+
if (isEditorialMode && !loading) rerender()
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
onMount(() => {
|
|
37
37
|
if (isCrawler()) return
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
(async () => {
|
|
40
|
+
await initialize()
|
|
41
|
+
track(TrackingEvent.ArticlePageView)
|
|
42
|
+
})()
|
|
42
43
|
|
|
43
44
|
return () => clearLinkInjections()
|
|
44
45
|
})
|
|
@@ -65,23 +66,29 @@
|
|
|
65
66
|
|
|
66
67
|
// Only trying once when not in editorial mode to prevent late injections (as well as a ton of requests)
|
|
67
68
|
// by users who are not in the editorial view.
|
|
68
|
-
// [TODO] TEMP: Only try once for editorial as well
|
|
69
69
|
response = await pollLinkInjections(url, htmlString, { maxTries: 1 })
|
|
70
70
|
|
|
71
|
-
inject({ aiInjections, manualInjections })
|
|
72
|
-
|
|
73
71
|
loading = false
|
|
74
72
|
|
|
73
|
+
if (!response) return
|
|
74
|
+
|
|
75
|
+
setTrackingSids({ organizationSid: response.organization_sid, domainSid: response.domain_sid })
|
|
76
|
+
|
|
77
|
+
// We only show results once they are fully processed. We only inject if AI is no longer running, or isn't
|
|
78
|
+
// meant to run to begin with.
|
|
79
|
+
if (!response.ai_running || !response.automation_enabled) {
|
|
80
|
+
inject({ aiInjections, manualInjections })
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
// A response was previous returned, but injections were still being generated in the backend.
|
|
76
85
|
// With this second request we wait until AI links are ready. We only do this in editorial
|
|
77
86
|
// so as not to suddenly insert new links while a user is reading the article.
|
|
78
|
-
if (!response?.ai_running) return
|
|
79
87
|
if (!isEditorialMode) return
|
|
80
88
|
|
|
81
|
-
|
|
89
|
+
response = await pollLinkInjections(url, htmlString, { requireCompletedResult: true })
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
editor.requestNewAIInjections(continuedResponse?.ai_injections || [])
|
|
91
|
+
inject({ aiInjections, manualInjections })
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
function rerender(): void {
|
|
@@ -119,7 +126,18 @@
|
|
|
119
126
|
{/if}
|
|
120
127
|
|
|
121
128
|
{#if isEditorialMode && authorized}
|
|
122
|
-
<Editor
|
|
129
|
+
<Editor
|
|
130
|
+
bind:linkInjections
|
|
131
|
+
bind:this={editor}
|
|
132
|
+
{htmlString}
|
|
133
|
+
{loading}
|
|
134
|
+
injectionsEnabled={response?.injections_enabled}
|
|
135
|
+
aiStatus={{
|
|
136
|
+
automationEnabled: response?.automation_enabled,
|
|
137
|
+
aiRunning: response?.ai_running && response?.automation_enabled,
|
|
138
|
+
message: response?.ai_progress_message,
|
|
139
|
+
percentage: response?.ai_progress_percentage,
|
|
140
|
+
}} />
|
|
123
141
|
{/if}
|
|
124
142
|
|
|
125
143
|
{#if activeInjection && activeInjection.title_details}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
14
|
<div>
|
|
15
|
-
<span class="paragraph">
|
|
15
|
+
<span class="paragraph" role="paragraph">
|
|
16
16
|
{expanded ? text : limitedText}{#if !expanded && text.length > limit}...{/if}
|
|
17
17
|
|
|
18
18
|
{#if !expanded && (text.length > limit || blurb)}
|
|
@@ -21,12 +21,11 @@
|
|
|
21
21
|
</span>
|
|
22
22
|
|
|
23
23
|
{#if expanded}
|
|
24
|
-
<
|
|
24
|
+
<div class="paragraph" role="paragraph">{blurb}</div>
|
|
25
25
|
{/if}
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
28
|
<style lang="scss">
|
|
29
|
-
p,
|
|
30
29
|
.paragraph {
|
|
31
30
|
display: block;
|
|
32
31
|
margin: margin(1) 0 0;
|