@playpilot/tpi 3.3.4 → 3.4.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 -8
- package/package.json +1 -1
- package/src/lib/api.ts +6 -9
- package/src/lib/hash.ts +4 -0
- package/src/lib/linkInjection.ts +3 -12
- package/src/lib/localization.ts +7 -7
- package/src/lib/scss/global.scss +0 -5
- package/src/lib/scss/variables.scss +1 -1
- package/src/lib/session.ts +78 -0
- package/src/lib/tracking.ts +1 -2
- package/src/lib/types/injection.d.ts +2 -0
- package/src/lib/types/session.d.ts +14 -0
- package/src/routes/+page.svelte +14 -6
- package/src/routes/components/Editorial/Editor.svelte +36 -25
- package/src/routes/components/Editorial/Session.svelte +97 -0
- package/src/routes/components/Modal.svelte +1 -4
- package/src/routes/components/Playlinks.svelte +4 -5
- package/src/routes/components/Popover.svelte +1 -2
- package/src/routes/components/Title.svelte +0 -5
- package/src/tests/lib/api.test.js +14 -14
- package/src/tests/lib/localization.test.js +0 -7
- package/src/tests/lib/session.test.js +95 -0
- package/src/tests/lib/tracking.test.js +0 -16
- package/src/tests/routes/+page.test.js +14 -4
- package/src/tests/routes/components/Editorial/Editor.test.js +17 -1
- package/src/tests/routes/components/Editorial/Session.test.js +80 -0
- package/src/tests/setup.js +23 -5
- package/src/lib/event.ts +0 -6
- package/src/lib/viewTransition.ts +0 -24
- package/src/tests/lib/event.test.js +0 -22
- package/src/tests/lib/viewTransition.test.js +0 -13
package/package.json
CHANGED
package/src/lib/api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { authorize, getAuthToken, isEditorialModeEnabled } from './auth'
|
|
2
2
|
import { apiBaseUrl } from './constants'
|
|
3
|
-
import { stringToHash } from './hash'
|
|
3
|
+
import { generateRandomHash, stringToHash } from './hash'
|
|
4
4
|
import { getLanguage } from './localization'
|
|
5
5
|
import { getPageMetaData } from './meta'
|
|
6
6
|
import type { ConfigResponse } from './types/config'
|
|
@@ -11,13 +11,13 @@ let pollTimeout: ReturnType<typeof setTimeout> | null = null
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Fetch link injections for a URL.
|
|
14
|
-
* @param url URL of the given article
|
|
15
14
|
* @param html HTML to be crawled
|
|
16
15
|
* @param options
|
|
16
|
+
* @param [options.url] URL of the given article
|
|
17
17
|
* @param [options.hash] unique key to identify the HTML
|
|
18
18
|
* @param [options.params] Any rest params to include in the request body
|
|
19
19
|
*/
|
|
20
|
-
export async function fetchLinkInjections(
|
|
20
|
+
export async function fetchLinkInjections(html: string, { url = getFullUrlPath(), hash = stringToHash(html), params = {} }: { url?: string, hash?: string; params?: object } = {}): Promise<LinkInjectionResponse> {
|
|
21
21
|
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
22
22
|
const apiToken = getApiToken()
|
|
23
23
|
const isEditorialMode = isEditorialModeEnabled() ? await authorize() : false
|
|
@@ -48,16 +48,13 @@ export async function fetchLinkInjections(url: string, html: string, { hash = st
|
|
|
48
48
|
/**
|
|
49
49
|
* Link injections take a while to be ready. During this time we poll the endpoint until it returns the result we want.
|
|
50
50
|
* The results return `injections_ready=false` while the injections are not yet ready.
|
|
51
|
-
* @param url URL of the given article
|
|
52
51
|
* @param html HTML to be crawled
|
|
53
52
|
*/
|
|
54
53
|
export async function pollLinkInjections(
|
|
55
|
-
url: string,
|
|
56
54
|
html: string,
|
|
57
55
|
{ requireCompletedResult = false, pollInterval = 3000, maxTries = 600, onpoll = () => null }:
|
|
58
56
|
{ requireCompletedResult?: boolean, pollInterval?: number, maxTries?: number, onpoll?: (_response: LinkInjectionResponse) => void } = {}
|
|
59
57
|
): Promise<LinkInjectionResponse> {
|
|
60
|
-
let hash = stringToHash(html)
|
|
61
58
|
let currentTry = 0
|
|
62
59
|
|
|
63
60
|
// Clear pollTimeout if it is already running to prevent multiple timeouts from running at the same time
|
|
@@ -72,7 +69,7 @@ export async function pollLinkInjections(
|
|
|
72
69
|
const poll = async (resolve: Function, reject: Function): Promise<void> => {
|
|
73
70
|
let response
|
|
74
71
|
try {
|
|
75
|
-
response = await fetchLinkInjections(
|
|
72
|
+
response = await fetchLinkInjections(html)
|
|
76
73
|
|
|
77
74
|
if (requireCompletedResult && (response.automation_enabled && response.ai_running)) throw new Error
|
|
78
75
|
|
|
@@ -117,7 +114,7 @@ export async function saveLinkInjections(linkInjections: LinkInjection[], html:
|
|
|
117
114
|
removed: !!linkInjection.removed
|
|
118
115
|
}))
|
|
119
116
|
|
|
120
|
-
const response = await fetchLinkInjections(
|
|
117
|
+
const response = await fetchLinkInjections(html, {
|
|
121
118
|
params: {
|
|
122
119
|
private_token: getAuthToken(),
|
|
123
120
|
link_injections: newLinkInjections,
|
|
@@ -166,7 +163,7 @@ function insertRandomKeys(linkInjections: LinkInjection[]): LinkInjection[] {
|
|
|
166
163
|
* @returns Random string preprending with title sid.
|
|
167
164
|
*/
|
|
168
165
|
export function generateInjectionKey(sid: string): string {
|
|
169
|
-
return sid + '-' + (
|
|
166
|
+
return sid + '-' + generateRandomHash()
|
|
170
167
|
}
|
|
171
168
|
|
|
172
169
|
export function getApiToken(): string | undefined {
|
package/src/lib/hash.ts
CHANGED
package/src/lib/linkInjection.ts
CHANGED
|
@@ -5,8 +5,6 @@ import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.sv
|
|
|
5
5
|
import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from './text'
|
|
6
6
|
import { getLargestValueInArray } from './array'
|
|
7
7
|
import type { LinkInjection, LinkInjectionTypes, LinkInjectionRanges } from './types/injection'
|
|
8
|
-
import { isHoldingSpecialKey } from './event'
|
|
9
|
-
import { playFallbackViewTransition } from './viewTransition'
|
|
10
8
|
|
|
11
9
|
const keyDataAttribute = 'data-playpilot-injection-key'
|
|
12
10
|
const keySelector = `[${keyDataAttribute}]`
|
|
@@ -227,8 +225,6 @@ function addCSSVariablesToLinks(): void {
|
|
|
227
225
|
function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
228
226
|
// Open modal on click
|
|
229
227
|
window.addEventListener('click', (event) => {
|
|
230
|
-
if (isHoldingSpecialKey(event)) return
|
|
231
|
-
|
|
232
228
|
const target = event.target as HTMLElement | null
|
|
233
229
|
if (!target?.parentElement) return
|
|
234
230
|
|
|
@@ -238,12 +234,7 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
238
234
|
const injection = injections.find(injection => key === injection.key)
|
|
239
235
|
if (!injection) return
|
|
240
236
|
|
|
241
|
-
event
|
|
242
|
-
|
|
243
|
-
playFallbackViewTransition(() => {
|
|
244
|
-
destroyLinkPopover(injection, false)
|
|
245
|
-
openLinkModal(event, injection)
|
|
246
|
-
}, window.innerWidth >= 600 && !window.matchMedia("(pointer: coarse)").matches)
|
|
237
|
+
openLinkModal(event, injection)
|
|
247
238
|
})
|
|
248
239
|
|
|
249
240
|
const createdInjectionElements = Array.from(document.querySelectorAll(keySelector)) as HTMLElement[]
|
|
@@ -266,11 +257,12 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
266
257
|
* Ignore clicks that used modifier keys or that were not left click.
|
|
267
258
|
*/
|
|
268
259
|
function openLinkModal(event: MouseEvent, injection: LinkInjection): void {
|
|
269
|
-
if (
|
|
260
|
+
if (event.ctrlKey || event.metaKey || event.button !== 0) return
|
|
270
261
|
if (activeModalInsertedComponent) return
|
|
271
262
|
|
|
272
263
|
event.preventDefault()
|
|
273
264
|
|
|
265
|
+
destroyLinkPopover(injection)
|
|
274
266
|
activeModalInsertedComponent = mount(TitleModal, { target: document.body, props: { title: injection.title_details!, onclose: destroyLinkModal } })
|
|
275
267
|
}
|
|
276
268
|
|
|
@@ -446,4 +438,3 @@ export function isAvailableAsManualInjection(injection: LinkInjection, injection
|
|
|
446
438
|
export function isEquivalentInjection(injection1: LinkInjection, injection2: LinkInjection): boolean {
|
|
447
439
|
return injection1.title === injection2.title && cleanPhrase(injection1.sentence) === cleanPhrase(injection2.sentence)
|
|
448
440
|
}
|
|
449
|
-
|
package/src/lib/localization.ts
CHANGED
|
@@ -10,7 +10,11 @@ export function t(key: string, language: LanguageCode = getLanguage()): string {
|
|
|
10
10
|
return translations[key]?.[language] || key
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @returns {LanguageCode}
|
|
15
|
+
*/
|
|
13
16
|
export function getLanguage(): LanguageCode {
|
|
17
|
+
// @ts-ignore
|
|
14
18
|
const configLanguage = window.PlayPilotLinkInjections?.language as LanguageCode | undefined
|
|
15
19
|
const languageCodes = Object.values(Language) as LanguageCode[]
|
|
16
20
|
|
|
@@ -20,14 +24,10 @@ export function getLanguage(): LanguageCode {
|
|
|
20
24
|
console.warn(`PlayPilot Link Injections: ${configLanguage} is not an accepted language`)
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
const documentLanguage = document.querySelector('html')?.getAttribute('lang')
|
|
24
|
-
|
|
25
|
-
if (documentLanguage) {
|
|
26
|
-
if (languageCodes.includes(documentLanguage as LanguageCode)) return documentLanguage as LanguageCode
|
|
27
|
+
const documentLanguage = document.querySelector('html')?.getAttribute('lang') as LanguageCode | undefined
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (matched) return matched
|
|
29
|
+
if (documentLanguage && languageCodes.includes(documentLanguage)) {
|
|
30
|
+
return documentLanguage
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
return Language.English
|
package/src/lib/scss/global.scss
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
--playpilot-content: #354367;
|
|
9
9
|
--playpilot-content-light: #4b5b82;
|
|
10
10
|
--playpilot-green: #53bca0;
|
|
11
|
-
--playpilot-font-family: "Poppins",
|
|
11
|
+
--playpilot-font-family: "Poppins", sans-serif;
|
|
12
12
|
--playpilot-text-color: #fff;
|
|
13
13
|
--playpilot-text-color-alt: #c8d4de;
|
|
14
14
|
--playpilot-shadow:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { fetchLinkInjections } from "./api"
|
|
2
|
+
import { generateRandomHash } from "./hash"
|
|
3
|
+
import type { SessionResponse } from "./types/session"
|
|
4
|
+
|
|
5
|
+
export const sessionKey = 'PlayPilotEditorialSessionId'
|
|
6
|
+
export const sessionPollPeriodMilliseconds = 5000
|
|
7
|
+
export const sessionPeriodMilliseconds = sessionPollPeriodMilliseconds * 2
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch link injections and infer if the current session is allowed to edit. If they are, we make an additional request
|
|
11
|
+
* where we save the session.
|
|
12
|
+
* Along with that, we also return automation_enabled and injections_enabled as they are used to update the state of
|
|
13
|
+
* the current editing session.
|
|
14
|
+
* @param html Despite not being used, the request still requires a valid html string
|
|
15
|
+
*/
|
|
16
|
+
export async function fetchAsSession(html: string): Promise<SessionResponse> {
|
|
17
|
+
const { automation_enabled, injections_enabled, session_id, session_last_ping } = await fetchLinkInjections(html)
|
|
18
|
+
|
|
19
|
+
const isCurrentlyAllowToEdit = isAllowedToEdit(session_id || null, session_last_ping || null)
|
|
20
|
+
|
|
21
|
+
const response = {
|
|
22
|
+
automation_enabled,
|
|
23
|
+
injections_enabled,
|
|
24
|
+
session_id,
|
|
25
|
+
session_last_ping,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!isCurrentlyAllowToEdit) return response
|
|
29
|
+
|
|
30
|
+
return await saveCurrentSession(html)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save the current users session id as the currently active session. This is always completed regardless of if the user
|
|
35
|
+
* is the current owner of the session, so check that first if necessary!
|
|
36
|
+
* @param html Despite not being used, the request still requires a valid html string
|
|
37
|
+
*/
|
|
38
|
+
export async function saveCurrentSession(html: string): Promise<SessionResponse> {
|
|
39
|
+
const sessionId = getSessionId()
|
|
40
|
+
const now = new Date(Date.now()).toISOString()
|
|
41
|
+
|
|
42
|
+
const params = {
|
|
43
|
+
session_id: sessionId,
|
|
44
|
+
session_last_ping: now.toString(),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { automation_enabled, injections_enabled } = await fetchLinkInjections(html, { params })
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
automation_enabled,
|
|
51
|
+
injections_enabled,
|
|
52
|
+
...params,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isAllowedToEdit(responseSessionId: string | null, responseSessionLastPing: string | null): boolean {
|
|
57
|
+
if (!responseSessionId || !responseSessionLastPing) return true
|
|
58
|
+
|
|
59
|
+
const now = Date.now()
|
|
60
|
+
const responseSessionLastPingDate = new Date(responseSessionLastPing)
|
|
61
|
+
const currentSessionId = getSessionId()
|
|
62
|
+
|
|
63
|
+
// Last session ping was older than given period, so the previous session has likely ended
|
|
64
|
+
if (now - responseSessionLastPingDate.getTime() > sessionPeriodMilliseconds) return true
|
|
65
|
+
|
|
66
|
+
return currentSessionId === responseSessionId
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getSessionId(): string {
|
|
70
|
+
return sessionStorage.getItem(sessionKey) || setSessionId()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function setSessionId(): string {
|
|
74
|
+
const id = sessionStorage.getItem(sessionKey) || generateRandomHash()
|
|
75
|
+
sessionStorage.setItem(sessionKey, id)
|
|
76
|
+
|
|
77
|
+
return id
|
|
78
|
+
}
|
package/src/lib/tracking.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { get } from "svelte/store"
|
|
2
2
|
import { currentDomainSid, currentOrganizationSid } from "./stores/organization"
|
|
3
3
|
import type { TitleData } from "./types/title"
|
|
4
|
-
import { getFullUrlPath } from "./url"
|
|
5
4
|
|
|
6
5
|
const baseUrl = 'https://insights.playpilot.net'
|
|
7
6
|
|
|
@@ -23,7 +22,7 @@ export async function track(event: string, title: TitleData | null = null, paylo
|
|
|
23
22
|
}
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
payload.url =
|
|
25
|
+
payload.url = window.location.href
|
|
27
26
|
payload.organization_sid = get(currentOrganizationSid)
|
|
28
27
|
payload.domain_sid = get(currentDomainSid)
|
|
29
28
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LinkInjectionResponse } from "./injection"
|
|
2
|
+
|
|
3
|
+
export type SessionResponse = Omit<
|
|
4
|
+
LinkInjectionResponse,
|
|
5
|
+
'injections_ready' |
|
|
6
|
+
'page_updated' |
|
|
7
|
+
'ai_running' |
|
|
8
|
+
'ai_progress_message' |
|
|
9
|
+
'ai_progress_percentage' |
|
|
10
|
+
'ai_injections' |
|
|
11
|
+
'organization_sid' |
|
|
12
|
+
'domain_sid' |
|
|
13
|
+
'link_injections'
|
|
14
|
+
>
|
package/src/routes/+page.svelte
CHANGED
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
let authorized = $state(false)
|
|
22
22
|
let loading = $state(true)
|
|
23
23
|
let linkInjections: LinkInjection[] = $state([])
|
|
24
|
-
let editor = $state()
|
|
25
24
|
|
|
26
25
|
// @ts-ignore It's ok if the response is empty
|
|
27
26
|
const { ai_injections: aiInjections = [], link_injections: manualInjections = [] } = $derived(response || {})
|
|
@@ -47,10 +46,9 @@
|
|
|
47
46
|
|
|
48
47
|
if (isEditorialMode) authorized = await authorize()
|
|
49
48
|
|
|
50
|
-
const url = getFullUrlPath()
|
|
51
|
-
|
|
52
49
|
try {
|
|
53
50
|
const config = await fetchConfig()
|
|
51
|
+
const url = getFullUrlPath()
|
|
54
52
|
|
|
55
53
|
// URL was marked as being excluded, we stop injections here unless we're in editorial mode.
|
|
56
54
|
if (!isEditorialMode && config?.exclude_urls_pattern && url.match(config.exclude_urls_pattern)) return
|
|
@@ -64,7 +62,7 @@
|
|
|
64
62
|
|
|
65
63
|
// Only trying once when not in editorial mode to prevent late injections (as well as a ton of requests)
|
|
66
64
|
// by users who are not in the editorial view.
|
|
67
|
-
response = await pollLinkInjections(
|
|
65
|
+
response = await pollLinkInjections(htmlString, { maxTries: 1 })
|
|
68
66
|
|
|
69
67
|
loading = false
|
|
70
68
|
|
|
@@ -84,7 +82,7 @@
|
|
|
84
82
|
// so as not to suddenly insert new links while a user is reading the article.
|
|
85
83
|
if (!isEditorialMode) return
|
|
86
84
|
|
|
87
|
-
response = await pollLinkInjections(
|
|
85
|
+
response = await pollLinkInjections(htmlString, { requireCompletedResult: true, onpoll: (update) => response = update })
|
|
88
86
|
inject({ aiInjections, manualInjections })
|
|
89
87
|
}
|
|
90
88
|
|
|
@@ -92,6 +90,15 @@
|
|
|
92
90
|
inject(separateLinkInjectionTypes(linkInjections))
|
|
93
91
|
}
|
|
94
92
|
|
|
93
|
+
function reinitializeEditor(): void {
|
|
94
|
+
isEditorialMode = false
|
|
95
|
+
|
|
96
|
+
requestAnimationFrame(() => {
|
|
97
|
+
isEditorialMode = true
|
|
98
|
+
initialize()
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
95
102
|
function inject(injections: LinkInjectionTypes = { aiInjections, manualInjections }): void {
|
|
96
103
|
// Get filtered injections as they are shown on the page.
|
|
97
104
|
// Only update state if it they are different from current injections.
|
|
@@ -127,9 +134,9 @@
|
|
|
127
134
|
{#if isEditorialMode && authorized}
|
|
128
135
|
<Editor
|
|
129
136
|
bind:linkInjections
|
|
130
|
-
bind:this={editor}
|
|
131
137
|
{htmlString}
|
|
132
138
|
{loading}
|
|
139
|
+
onreinitialize={reinitializeEditor}
|
|
133
140
|
injectionsEnabled={response?.injections_enabled}
|
|
134
141
|
aiStatus={{
|
|
135
142
|
automationEnabled: response?.automation_enabled,
|
|
@@ -141,6 +148,7 @@
|
|
|
141
148
|
</div>
|
|
142
149
|
|
|
143
150
|
<style lang="scss">
|
|
151
|
+
@import url('https://fonts.googleapis.com/css?family=Poppins:400,600,700');
|
|
144
152
|
@import url('$lib/scss/variables.scss');
|
|
145
153
|
@import url('$lib/scss/global.scss');
|
|
146
154
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import Alert from './Alert.svelte'
|
|
7
7
|
import ManualInjection from './ManualInjection.svelte'
|
|
8
8
|
import RoundButton from '../RoundButton.svelte'
|
|
9
|
+
import Session from './Session.svelte'
|
|
9
10
|
import { saveLinkInjections } from '$lib/api'
|
|
10
11
|
import { untrack } from 'svelte'
|
|
11
12
|
import AIIndicator from './AIIndicator.svelte'
|
|
@@ -21,12 +22,13 @@
|
|
|
21
22
|
htmlString?: string,
|
|
22
23
|
loading?: boolean,
|
|
23
24
|
injectionsEnabled?: boolean,
|
|
24
|
-
aiStatus
|
|
25
|
+
aiStatus?: {
|
|
25
26
|
automationEnabled?: boolean,
|
|
26
27
|
aiRunning?: boolean,
|
|
27
28
|
message?: string,
|
|
28
29
|
percentage?: number
|
|
29
30
|
}
|
|
31
|
+
onreinitialize?: () => void,
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
let {
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
loading = false,
|
|
36
38
|
injectionsEnabled = false,
|
|
37
39
|
aiStatus = {},
|
|
40
|
+
onreinitialize = () => null,
|
|
38
41
|
}: Props = $props()
|
|
39
42
|
|
|
40
43
|
const editorPositionKey = 'editor-position'
|
|
@@ -46,15 +49,17 @@
|
|
|
46
49
|
let manualInjectionActive = $state(false)
|
|
47
50
|
let saving = $state(false)
|
|
48
51
|
let hasError = $state(false)
|
|
52
|
+
let allowEditing = $state(true)
|
|
49
53
|
let scrollDistance = $state(0)
|
|
50
54
|
let initialStateString = $state('')
|
|
51
55
|
|
|
56
|
+
const { automationEnabled = false, aiRunning = false } = $derived(aiStatus)
|
|
52
57
|
const linkInjectionsString = $derived(JSON.stringify(linkInjections))
|
|
58
|
+
const showControls = $derived(!aiRunning && allowEditing)
|
|
53
59
|
const hasChanged = $derived(initialStateString && initialStateString !== linkInjectionsString)
|
|
54
60
|
// Filter out injections without title_details, injections that are removed, duplicate, or are AI injections that failed to inject
|
|
55
61
|
const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
|
|
56
62
|
const sortedInjections = $derived(sortInjections(filteredInjections))
|
|
57
|
-
const { automationEnabled = false, aiRunning = false } = $derived(aiStatus)
|
|
58
63
|
const initialAiRunning = $derived(!loading && untrack(() => aiStatus.aiRunning))
|
|
59
64
|
|
|
60
65
|
$effect(() => {
|
|
@@ -136,10 +141,6 @@
|
|
|
136
141
|
}
|
|
137
142
|
</script>
|
|
138
143
|
|
|
139
|
-
<svelte:head>
|
|
140
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Poppins:400,600,700">
|
|
141
|
-
</svelte:head>
|
|
142
|
-
|
|
143
144
|
<section class="editor playpilot-styled-scrollbar" class:panel-open={manualInjectionActive} class:loading bind:this={editorElement} {onscroll}>
|
|
144
145
|
{#if editorElement && !loading}
|
|
145
146
|
<div class="handles">
|
|
@@ -158,7 +159,7 @@
|
|
|
158
159
|
|
|
159
160
|
{#if loading}
|
|
160
161
|
<div class="loading">Loading...</div>
|
|
161
|
-
{:else if
|
|
162
|
+
{:else if showControls}
|
|
162
163
|
<div class="bubble" aria-label="{filteredInjections.length} found playlinks">
|
|
163
164
|
{filteredInjections.length}
|
|
164
165
|
</div>
|
|
@@ -170,26 +171,36 @@
|
|
|
170
171
|
</header>
|
|
171
172
|
|
|
172
173
|
{#if !loading}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
174
|
+
<Session
|
|
175
|
+
{htmlString}
|
|
176
|
+
onallow={() => allowEditing = true}
|
|
177
|
+
ondisallow={() => allowEditing = false}
|
|
178
|
+
ontakeover={onreinitialize}
|
|
179
|
+
onpoll={(updated) => {
|
|
180
|
+
injectionsEnabled = updated.injectionsEnabled
|
|
181
|
+
aiStatus.automationEnabled = updated.automationEnabled
|
|
182
|
+
}} />
|
|
183
|
+
|
|
184
|
+
{#if showControls}
|
|
185
|
+
{#if !injectionsEnabled}
|
|
186
|
+
<div class="alert">
|
|
187
|
+
<Alert type="warning">
|
|
188
|
+
<strong>Playlinks are currently not published.</strong> Visitors to this page will not see any of the injected links.
|
|
189
|
+
Publish playlinks from the <a href="https://partner.playpilot.net">Partner Portal.</a>
|
|
190
|
+
</Alert>
|
|
191
|
+
</div>
|
|
192
|
+
{/if}
|
|
181
193
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
{#if initialAiRunning || !automationEnabled}
|
|
195
|
+
<AIIndicator {...aiStatus} aiInjectionsCount={separateLinkInjectionTypes(linkInjections).aiInjections.length} />
|
|
196
|
+
{/if}
|
|
185
197
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
{#if hasError}
|
|
199
|
+
<div class="error" transition:slide|global={{ duration: 150 }}>
|
|
200
|
+
<Alert>Something went wrong, check your links below.</Alert>
|
|
201
|
+
</div>
|
|
202
|
+
{/if}
|
|
191
203
|
|
|
192
|
-
{#if !aiRunning}
|
|
193
204
|
<div class="items">
|
|
194
205
|
{#each sortedInjections as linkInjection (linkInjection.key)}
|
|
195
206
|
<!-- We want to bind to the original object, not the derived object, so we get the index of the injection in the original object by it's key -->
|
|
@@ -201,7 +212,7 @@
|
|
|
201
212
|
{/each}
|
|
202
213
|
</div>
|
|
203
214
|
|
|
204
|
-
{#if hasChanged && linkInjections.length}
|
|
215
|
+
{#if hasChanged && linkInjections.length && allowEditing}
|
|
205
216
|
<button class="save" disabled={saving} onclick={save} in:fade={{ duration: 100 }}>
|
|
206
217
|
{saving ? 'Saving...' : 'Save links'}
|
|
207
218
|
</button>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fetchAsSession, isAllowedToEdit, saveCurrentSession, sessionPollPeriodMilliseconds } from '$lib/session'
|
|
3
|
+
import { onMount } from 'svelte'
|
|
4
|
+
import Alert from './Alert.svelte'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
htmlString?: string,
|
|
8
|
+
// eslint-disable-next-line no-unused-vars
|
|
9
|
+
onpoll?: ({ injectionsEnabled, automationEnabled }: { injectionsEnabled: boolean, automationEnabled: boolean }) => void,
|
|
10
|
+
onallow?: () => void,
|
|
11
|
+
ondisallow?: () => void,
|
|
12
|
+
ontakeover?: () => void,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
htmlString = '',
|
|
17
|
+
onpoll = () => null,
|
|
18
|
+
onallow = () => null,
|
|
19
|
+
ondisallow = () => null,
|
|
20
|
+
ontakeover = () => null,
|
|
21
|
+
}: Props = $props()
|
|
22
|
+
|
|
23
|
+
let pollInterval: ReturnType<typeof setInterval> | null = null
|
|
24
|
+
let isEditingAllowed = $state(true)
|
|
25
|
+
|
|
26
|
+
onMount(() => {
|
|
27
|
+
startPolling()
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
if (pollInterval) clearInterval(pollInterval)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function startPolling(): void {
|
|
35
|
+
setSession()
|
|
36
|
+
pollInterval = setInterval(setSession, sessionPollPeriodMilliseconds)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function setSession(): Promise<void> {
|
|
40
|
+
const result = await fetchAsSession(htmlString)
|
|
41
|
+
|
|
42
|
+
if (!result) return
|
|
43
|
+
|
|
44
|
+
isEditingAllowed = isAllowedToEdit(result.session_id || null, result.session_last_ping || null)
|
|
45
|
+
|
|
46
|
+
if (isEditingAllowed) onallow()
|
|
47
|
+
else ondisallow()
|
|
48
|
+
|
|
49
|
+
onpoll({ injectionsEnabled: !!result.injections_enabled, automationEnabled: !!result.automation_enabled })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function takeOverEditing(): void {
|
|
53
|
+
saveCurrentSession(htmlString)
|
|
54
|
+
ontakeover()
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
{#if !isEditingAllowed}
|
|
59
|
+
<div class="alert">
|
|
60
|
+
<Alert type="warning">
|
|
61
|
+
<p>
|
|
62
|
+
<strong>Someone else is currently editing this document.</strong>
|
|
63
|
+
Wait for them to exit the page or take over edit permissions, preventing them from making more changes.
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
<button class="button" onclick={takeOverEditing}>Take over editing</button>
|
|
67
|
+
</Alert>
|
|
68
|
+
</div>
|
|
69
|
+
{/if}
|
|
70
|
+
|
|
71
|
+
<style lang="scss">
|
|
72
|
+
p {
|
|
73
|
+
margin: 0 0 margin(0.5);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.alert {
|
|
77
|
+
margin: 0 margin(0.5);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.button {
|
|
81
|
+
appearance: none;
|
|
82
|
+
width: 100%;
|
|
83
|
+
padding: margin(0.25);
|
|
84
|
+
border: 0;
|
|
85
|
+
border-radius: margin(0.25);
|
|
86
|
+
background: var(--playpilot-warning);
|
|
87
|
+
transition: opacity 100ms;
|
|
88
|
+
font-family: inherit;
|
|
89
|
+
color: var(--playpilot-dark);
|
|
90
|
+
font-size: 0.85rem;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
|
|
93
|
+
&:hover {
|
|
94
|
+
background: white;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</style>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<svelte:window on:keydown={({ key }) => { if (key === 'Escape') onclose() }} />
|
|
33
33
|
|
|
34
34
|
<div class="modal" transition:fade={{ duration: 150 }}>
|
|
35
|
-
<div class="dialog" {onscroll} role="dialog" transition:scaleOrFly
|
|
35
|
+
<div class="dialog" {onscroll} role="dialog" transition:scaleOrFly>
|
|
36
36
|
<div class="close">
|
|
37
37
|
<RoundButton onclick={() => onclose()}>
|
|
38
38
|
<IconClose />
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
<style lang="scss">
|
|
51
51
|
.modal {
|
|
52
52
|
z-index: 2147483647; // As high as she goes
|
|
53
|
-
box-sizing: border-box;
|
|
54
53
|
position: fixed;
|
|
55
54
|
display: flex;
|
|
56
55
|
justify-content: center;
|
|
@@ -71,7 +70,6 @@
|
|
|
71
70
|
|
|
72
71
|
|
|
73
72
|
.dialog {
|
|
74
|
-
z-index: 1;
|
|
75
73
|
position: relative;
|
|
76
74
|
width: 100%;
|
|
77
75
|
max-width: 600px;
|
|
@@ -90,7 +88,6 @@
|
|
|
90
88
|
|
|
91
89
|
|
|
92
90
|
.backdrop {
|
|
93
|
-
z-index: 0;
|
|
94
91
|
position: absolute;
|
|
95
92
|
top: 0;
|
|
96
93
|
right: 0;
|
|
@@ -65,14 +65,12 @@
|
|
|
65
65
|
|
|
66
66
|
<style lang="scss">
|
|
67
67
|
img {
|
|
68
|
-
--size: #{margin(2)};
|
|
69
|
-
height: var(--size);
|
|
70
|
-
width: var(--size);
|
|
71
68
|
border-radius: margin(0.5);
|
|
72
69
|
background: rgba(0, 0, 0, 0.25);
|
|
73
70
|
|
|
74
71
|
@media (min-width: 640px) {
|
|
75
|
-
|
|
72
|
+
height: margin(2.5);
|
|
73
|
+
width: margin(2.5);
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
|
|
@@ -106,7 +104,8 @@
|
|
|
106
104
|
|
|
107
105
|
img {
|
|
108
106
|
@media (min-width: 640px) {
|
|
109
|
-
|
|
107
|
+
height: margin(2);
|
|
108
|
+
width: margin(2);
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
111
|
}
|