@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.
Files changed (42) hide show
  1. package/dist/link-injections.js +8 -7
  2. package/package.json +1 -1
  3. package/src/lib/actions/heading.ts +11 -0
  4. package/src/lib/api.ts +1 -3
  5. package/src/lib/auth.ts +13 -1
  6. package/src/lib/constants.ts +2 -0
  7. package/src/lib/enums/TrackingEvent.ts +1 -0
  8. package/src/lib/linkInjection.ts +43 -32
  9. package/src/lib/scss/_mixins.scss +8 -0
  10. package/src/lib/scss/global.scss +6 -6
  11. package/src/lib/scss/variables.scss +2 -0
  12. package/src/lib/stores/organization.ts +4 -0
  13. package/src/lib/tracking.ts +14 -1
  14. package/src/lib/types/injection.d.ts +5 -0
  15. package/src/routes/+layout.svelte +6 -2
  16. package/src/routes/+page.svelte +31 -13
  17. package/src/routes/components/Description.svelte +2 -3
  18. package/src/routes/components/Editorial/AIIndicator.svelte +85 -91
  19. package/src/routes/components/Editorial/Alert.svelte +12 -2
  20. package/src/routes/components/Editorial/Editor.svelte +82 -61
  21. package/src/routes/components/Editorial/EditorItem.svelte +32 -7
  22. package/src/routes/components/Editorial/ManualInjection.svelte +10 -9
  23. package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +14 -0
  24. package/src/routes/components/Editorial/ResizeHandle.svelte +1 -1
  25. package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +7 -5
  26. package/src/routes/components/Icons/IconWarning.svelte +5 -0
  27. package/src/routes/components/Modal.svelte +2 -0
  28. package/src/routes/components/Playlinks.svelte +12 -11
  29. package/src/routes/components/Popover.svelte +2 -0
  30. package/src/routes/components/Title.svelte +3 -2
  31. package/src/routes/components/TitlePopover.svelte +1 -1
  32. package/src/tests/lib/auth.test.js +31 -1
  33. package/src/tests/lib/linkInjection.test.js +87 -48
  34. package/src/tests/lib/tracking.test.js +61 -1
  35. package/src/tests/routes/+page.test.js +94 -4
  36. package/src/tests/routes/components/Editorial/AiIndicator.test.js +28 -42
  37. package/src/tests/routes/components/Editorial/Alert.test.js +10 -3
  38. package/src/tests/routes/components/Editorial/Editor.test.js +15 -0
  39. package/src/tests/routes/components/Editorial/EditorItem.test.js +32 -7
  40. package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +13 -1
  41. package/src/tests/routes/components/Title.test.js +2 -2
  42. package/svelte.config.js +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "3.1.0",
3
+ "version": "3.2.0-beta.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -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 = {} }: { automation?: boolean; hash?: string; params?: object } = {}): Promise<LinkInjectionResponse> {
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
+ }
@@ -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='
@@ -17,4 +17,5 @@ export const TrackingEvent = Object.freeze({
17
17
  InjectionFailed: 'ali_injection_failed',
18
18
  TotalInjectionsCount: 'ali_injection_count',
19
19
  FetchingConfigFailed: 'ali_fetch_config_failed',
20
+ AuthFailed: 'ali_auth_failed',
20
21
  })
@@ -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 cleanPhrase(fullText).includes(cleanPhrase(i.sentence))
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)) continue
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
- element.innerHTML = replaceStartingFrom(element.innerHTML, injection.title, linkWrapperElement.outerHTML, startingIndex)
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
- const sortedInjections = sortLinkInjectionsByRange(mergedInjections, ranges)
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 [...aiInjections, ...manualInjections.map(i => ({ ...i, manual: true }))]
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
+ }
@@ -2,13 +2,13 @@
2
2
 
3
3
  [data-playpilot-injection-key] {
4
4
  position: relative;
5
+ }
5
6
 
6
- &.injection-highlight {
7
- outline: margin(0.25) solid var(--playpilot-primary) !important;
8
- outline-offset: margin(0.5) !important;
9
- border-radius: margin(0.05);
10
- scroll-margin: margin(5);
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 {
@@ -19,4 +19,6 @@
19
19
  #{margin(0.1) margin(0.1) margin(0.75)} rgba(0, 0, 0, 0.25);
20
20
  --playpilot-error: #ea5a5a;
21
21
  --playpilot-error-dark: #442533;
22
+ --playpilot-warning: #f7c74e;
23
+ --playpilot-warning-dark: #413c23;
22
24
  }
@@ -0,0 +1,4 @@
1
+ import { writable, type Writable } from "svelte/store"
2
+
3
+ export const currentOrganizationSid: Writable<string | null> = writable(null)
4
+ export const currentDomainSid: Writable<string | null> = writable(null)
@@ -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 &amp; 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 &amp; 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>
@@ -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
- track(TrackingEvent.ArticlePageView)
40
-
41
- initialize()
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
- const continuedResponse = await pollLinkInjections(url, htmlString, { requireCompletedResult: true })
89
+ response = await pollLinkInjections(url, htmlString, { requireCompletedResult: true })
82
90
 
83
- // @ts-ignore
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 bind:linkInjections bind:this={editor} {htmlString} {loading} aiRunning={response?.ai_running && response?.automation_enabled} />
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
- <p>{blurb}</p>
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;