@playpilot/tpi 3.0.1 → 3.1.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 +7 -7
- package/package.json +1 -1
- package/src/lib/api.ts +21 -1
- package/src/lib/enums/TrackingEvent.ts +2 -0
- package/src/lib/linkInjection.ts +8 -8
- package/src/lib/text.ts +24 -0
- package/src/lib/types/config.d.ts +3 -0
- package/src/routes/+page.svelte +18 -3
- package/src/routes/components/Editorial/AIIndicator.svelte +43 -2
- package/src/routes/components/Editorial/DragHandle.svelte +3 -3
- package/src/routes/components/Editorial/Editor.svelte +58 -28
- package/src/routes/components/Editorial/EditorItem.svelte +52 -24
- package/src/routes/components/Editorial/ManualInjection.svelte +69 -16
- package/src/routes/components/Editorial/ResizeHandle.svelte +111 -0
- package/src/routes/components/Editorial/Search/TitleSearch.svelte +17 -90
- package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +107 -0
- package/src/routes/components/Title.svelte +28 -4
- package/src/tests/helpers.js +2 -1
- package/src/tests/lib/api.test.js +30 -1
- package/src/tests/lib/linkInjection.test.js +39 -11
- package/src/tests/lib/text.test.js +23 -1
- package/src/tests/routes/+page.test.js +52 -5
- package/src/tests/routes/components/Editorial/AiIndicator.test.js +5 -2
- package/src/tests/routes/components/Editorial/DragHandle.test.js +5 -5
- package/src/tests/routes/components/Editorial/Editor.test.js +69 -0
- package/src/tests/routes/components/Editorial/EditorItem.test.js +34 -10
- package/src/tests/routes/components/Editorial/ManualInjection.test.js +5 -1
- package/src/tests/routes/components/Editorial/ResizeHandle.test.js +45 -0
- package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +6 -4
- package/src/tests/routes/components/Editorial/Search/TitleSearchItem.test.js +44 -0
package/package.json
CHANGED
package/src/lib/api.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { apiBaseUrl } from './constants'
|
|
|
3
3
|
import { stringToHash } from './hash'
|
|
4
4
|
import { getLanguage } from './localization'
|
|
5
5
|
import { getPageMetaData } from './meta'
|
|
6
|
+
import type { ConfigResponse } from './types/config'
|
|
6
7
|
import type { LinkInjectionResponse, LinkInjection } from './types/injection'
|
|
7
8
|
import { getFullUrlPath } from './url'
|
|
8
9
|
|
|
@@ -123,6 +124,25 @@ export async function saveLinkInjections(linkInjections: LinkInjection[], html:
|
|
|
123
124
|
return linkInjections
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
export async function fetchConfig(): Promise<ConfigResponse | null> {
|
|
128
|
+
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
129
|
+
const apiToken = getApiToken()
|
|
130
|
+
|
|
131
|
+
if (!apiToken) throw new Error('No token was provided')
|
|
132
|
+
|
|
133
|
+
const response = await fetch(apiBaseUrl + `/domains/config?api-token=${apiToken}`, {
|
|
134
|
+
headers,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
if (!response.ok) throw response
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
return await response.json() || null
|
|
141
|
+
} catch {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
126
146
|
/**
|
|
127
147
|
* Insert random keys into link injections. These are used to identify the links on the page.
|
|
128
148
|
* We can't just use SIDs, as a page might include multiple links of the same title
|
|
@@ -143,6 +163,6 @@ export function generateInjectionKey(sid: string): string {
|
|
|
143
163
|
return sid + '-' + (Math.random() + 1).toString(36).substring(7)
|
|
144
164
|
}
|
|
145
165
|
|
|
146
|
-
export function getApiToken() {
|
|
166
|
+
export function getApiToken(): string | undefined {
|
|
147
167
|
return window.PlayPilotLinkInjections?.token
|
|
148
168
|
}
|
|
@@ -14,4 +14,6 @@ export const TrackingEvent = Object.freeze({
|
|
|
14
14
|
AfterArticleModalButtonClick: 'ali_after_article_modal_button_click',
|
|
15
15
|
|
|
16
16
|
InjectionFailed: 'ali_injection_failed',
|
|
17
|
+
TotalInjectionsCount: 'ali_injection_count',
|
|
18
|
+
FetchingConfigFailed: 'ali_fetch_config_failed',
|
|
17
19
|
})
|
package/src/lib/linkInjection.ts
CHANGED
|
@@ -3,7 +3,6 @@ import TitlePopover from '../routes/components/TitlePopover.svelte'
|
|
|
3
3
|
import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.svelte'
|
|
4
4
|
import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from './text'
|
|
5
5
|
import { getLargestValueInArray } from './array'
|
|
6
|
-
import { decodeHtmlEntities } from './html'
|
|
7
6
|
import type { LinkInjection, LinkInjectionTypes, LinkInjectionRanges } from './types/injection'
|
|
8
7
|
|
|
9
8
|
const keyDataAttribute = 'data-playpilot-injection-key'
|
|
@@ -343,24 +342,25 @@ export function isValidInjection(injection: LinkInjection): boolean {
|
|
|
343
342
|
* Filter links for in-text injections, removing after article, inactive, removed, duplicate, and items without title_details
|
|
344
343
|
*/
|
|
345
344
|
export function filterInvalidInTextInjections(injections: LinkInjection[]): LinkInjection[] {
|
|
346
|
-
return
|
|
345
|
+
return filterRemovedAndInactiveInjections(injections).filter(i => i.in_text !== false && isValidInjection(i))
|
|
347
346
|
}
|
|
348
347
|
|
|
349
348
|
/**
|
|
350
349
|
* Filter links for after article injections, removing in-text only, inactive, removed, duplicate, and items without title_details
|
|
351
350
|
*/
|
|
352
351
|
export function filterInvalidAfterArticleInjections(injections: LinkInjection[]): LinkInjection[] {
|
|
353
|
-
return
|
|
352
|
+
return filterRemovedAndInactiveInjections(injections).filter(i => i.after_article === true && isValidInjection(i))
|
|
354
353
|
}
|
|
355
354
|
|
|
356
355
|
/**
|
|
357
|
-
* Filter injections that were marked as removed or have an equivalent removed manual injections, soley based on the same sentence and title.
|
|
356
|
+
* Filter injections that were marked as removed or inactive or have an equivalent removed or inactive manual injections, soley based on the same sentence and title.
|
|
358
357
|
*/
|
|
359
|
-
export function
|
|
358
|
+
export function filterRemovedAndInactiveInjections(injections: LinkInjection[]): LinkInjection[] {
|
|
360
359
|
return injections.filter(injection => {
|
|
361
|
-
if (injection.removed) return false
|
|
362
|
-
if (injection.manual && !injection.removed) return true
|
|
363
|
-
|
|
360
|
+
if (injection.removed || injection.inactive) return false
|
|
361
|
+
if (injection.manual && (!injection.removed && !injection.inactive)) return true
|
|
362
|
+
|
|
363
|
+
return !injections.some(i => i.manual && (i.removed || i.inactive) && isEquivalentInjection(i, injection))
|
|
364
364
|
})
|
|
365
365
|
}
|
|
366
366
|
|
package/src/lib/text.ts
CHANGED
|
@@ -55,3 +55,27 @@ export function replaceStartingFrom(text: string, search: string, replacement: s
|
|
|
55
55
|
export function cleanPhrase(phrase: string): string {
|
|
56
56
|
return decodeHtmlEntities(phrase).toLowerCase().replace(/\s+/g, '')
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Truncate a string, but leave a given phrase within view, truncating at both the start and end if necessary.
|
|
61
|
+
*/
|
|
62
|
+
export function truncateAroundPhrase(sentence: string, phrase: string, maxLength: number): string {
|
|
63
|
+
const index = sentence.indexOf(phrase)
|
|
64
|
+
const phraseStart = index
|
|
65
|
+
const padding = Math.floor((maxLength - phrase.length) / 2)
|
|
66
|
+
|
|
67
|
+
let start = Math.max(0, phraseStart - padding)
|
|
68
|
+
let end = start + maxLength
|
|
69
|
+
|
|
70
|
+
if (end > sentence.length) {
|
|
71
|
+
end = sentence.length
|
|
72
|
+
start = Math.max(0, end - maxLength)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let result = sentence.slice(start, end).trim()
|
|
76
|
+
|
|
77
|
+
if (start > 0) result = '…' + result
|
|
78
|
+
if (end < sentence.length) result += '…'
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
}
|
package/src/routes/+page.svelte
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte'
|
|
3
|
-
import { pollLinkInjections } from '$lib/api'
|
|
3
|
+
import { fetchConfig, pollLinkInjections } from '$lib/api'
|
|
4
4
|
import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/linkInjection'
|
|
5
5
|
import { track } from '$lib/tracking'
|
|
6
6
|
import { getFullUrlPath } from '$lib/url'
|
|
@@ -41,10 +41,25 @@
|
|
|
41
41
|
async function initialize(): Promise<void> {
|
|
42
42
|
if (isEditorialMode) authorized = await authorize()
|
|
43
43
|
|
|
44
|
+
const url = getFullUrlPath()
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const config = await fetchConfig()
|
|
48
|
+
|
|
49
|
+
// URL was marked as being excluded, we stop injections here unless we're in editorial mode.
|
|
50
|
+
if (!isEditorialMode && config?.exclude_urls_pattern && url.match(config.exclude_urls_pattern)) return
|
|
51
|
+
} catch(error) {
|
|
52
|
+
// We also return if the config did not get fetched properly, as we can't determine what should and should
|
|
53
|
+
// get injected without it.
|
|
54
|
+
track(TrackingEvent.FetchingConfigFailed)
|
|
55
|
+
console.error('TPI Config failed to fetch', error)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
// Only trying once when not in editorial mode to prevent late injections (as well as a ton of requests)
|
|
45
60
|
// by users who are not in the editorial view.
|
|
46
61
|
// [TODO] TEMP: Only try once for editorial as well
|
|
47
|
-
response = await pollLinkInjections(
|
|
62
|
+
response = await pollLinkInjections(url, htmlString, { maxTries: 1 })
|
|
48
63
|
|
|
49
64
|
inject({ aiInjections, manualInjections })
|
|
50
65
|
|
|
@@ -56,7 +71,7 @@
|
|
|
56
71
|
if (!response?.ai_running) return
|
|
57
72
|
if (!isEditorialMode) return
|
|
58
73
|
|
|
59
|
-
const continuedResponse = await pollLinkInjections(
|
|
74
|
+
const continuedResponse = await pollLinkInjections(url, htmlString, { requireCompletedResult: true })
|
|
60
75
|
|
|
61
76
|
// @ts-ignore
|
|
62
77
|
editor.requestNewAIInjections(continuedResponse?.ai_injections || [])
|
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
interface Props {
|
|
6
6
|
// eslint-disable-next-line no-unused-vars
|
|
7
7
|
onadd: (injections: LinkInjection[]) => void
|
|
8
|
+
/** Used to guesstimate the load times. */
|
|
9
|
+
htmlString?: string
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
const { onadd, htmlString = '' }: Props = $props()
|
|
13
|
+
|
|
14
|
+
// Guesstimate AI load times based on the given text length.
|
|
15
|
+
// The value will always be between 1 and 10 minutes.
|
|
16
|
+
const fakeLoadTimes = $derived(Math.min(Math.max(htmlString.length * 30, 60000), 600000))
|
|
11
17
|
|
|
12
18
|
let running = $state(true)
|
|
13
19
|
let injectionsToBeInserted: LinkInjection[] = $state([])
|
|
@@ -33,7 +39,8 @@
|
|
|
33
39
|
|
|
34
40
|
<div>
|
|
35
41
|
{#if running}
|
|
36
|
-
AI links are currently processing.
|
|
42
|
+
AI links are currently processing. This can take several minutes.<br>
|
|
43
|
+
You can add manual links while this is ongoing.
|
|
37
44
|
{:else if injectionsToBeInserted?.length}
|
|
38
45
|
AI links are ready.
|
|
39
46
|
<strong>{injectionsToBeInserted.length} New {injectionsToBeInserted.length > 1 ? 'links were' : 'link was'} found.</strong>
|
|
@@ -48,6 +55,10 @@
|
|
|
48
55
|
<button class="button" onclick={() => dismissed = true}>Dismiss</button>
|
|
49
56
|
{/if}
|
|
50
57
|
</div>
|
|
58
|
+
|
|
59
|
+
{#if running}
|
|
60
|
+
<div class="loading-bar" data-testid="loading-bar" style:animation-duration="{fakeLoadTimes}ms"></div>
|
|
61
|
+
{/if}
|
|
51
62
|
</div>
|
|
52
63
|
|
|
53
64
|
<div class="border">
|
|
@@ -76,6 +87,7 @@
|
|
|
76
87
|
line-height: 1.5;
|
|
77
88
|
z-index: 1;
|
|
78
89
|
color: var(--playpilot-text-color-alt);
|
|
90
|
+
overflow: hidden;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
.icon {
|
|
@@ -133,4 +145,33 @@
|
|
|
133
145
|
color: white;
|
|
134
146
|
}
|
|
135
147
|
}
|
|
148
|
+
|
|
149
|
+
@keyframes fake-load {
|
|
150
|
+
0% {
|
|
151
|
+
width: 0%;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
2% {
|
|
155
|
+
width: 20%;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
70% {
|
|
159
|
+
width: 85%;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
100% {
|
|
163
|
+
width: 90%;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.loading-bar {
|
|
168
|
+
position: absolute;
|
|
169
|
+
bottom: 0;
|
|
170
|
+
left: 0;
|
|
171
|
+
height: 0.15em;
|
|
172
|
+
border-radius: 0 0.25rem 0.25rem 0;
|
|
173
|
+
background: linear-gradient(to right, var(--playpilot-light), var(--playpilot-green));
|
|
174
|
+
animation: fake-load forwards;
|
|
175
|
+
animation-duration: 20000ms;
|
|
176
|
+
}
|
|
136
177
|
</style>
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
z-index: 10;
|
|
89
89
|
appearance: none;
|
|
90
90
|
position: absolute;
|
|
91
|
-
top: 0;
|
|
91
|
+
top: margin(0.5);
|
|
92
92
|
left: 30%;
|
|
93
93
|
width: 40%;
|
|
94
94
|
height: margin(1);
|
|
@@ -114,9 +114,9 @@
|
|
|
114
114
|
display: block;
|
|
115
115
|
content: "";
|
|
116
116
|
position: absolute;
|
|
117
|
-
top:
|
|
117
|
+
top: 20%;
|
|
118
118
|
right: 0;
|
|
119
|
-
bottom:
|
|
119
|
+
bottom: 60%;
|
|
120
120
|
left: 0;
|
|
121
121
|
border-radius: margin(1);
|
|
122
122
|
background: var(--playpilot-text-color);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { fade, fly, slide } from 'svelte/transition'
|
|
3
|
-
import { flip } from 'svelte/animate'
|
|
4
3
|
import EditorItem from './EditorItem.svelte'
|
|
5
4
|
import DragHandle from './DragHandle.svelte'
|
|
5
|
+
import ResizeHandle from './ResizeHandle.svelte'
|
|
6
6
|
import Alert from './Alert.svelte'
|
|
7
7
|
import ManualInjection from './ManualInjection.svelte'
|
|
8
8
|
import RoundButton from '../RoundButton.svelte'
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import { isEquivalentInjection, isValidInjection } from '$lib/linkInjection'
|
|
13
13
|
import type { Position } from '$lib/types/position'
|
|
14
14
|
import type { LinkInjection } from '$lib/types/injection'
|
|
15
|
+
import { track } from '$lib/tracking'
|
|
16
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
15
17
|
|
|
16
18
|
interface Props {
|
|
17
19
|
linkInjections: LinkInjection[],
|
|
@@ -23,9 +25,11 @@
|
|
|
23
25
|
let { linkInjections = $bindable(), htmlString = '', loading = false, aiRunning = false }: Props = $props()
|
|
24
26
|
|
|
25
27
|
const editorPositionKey = 'editor-position'
|
|
28
|
+
const editorHeightKey = 'editor-height'
|
|
26
29
|
|
|
27
30
|
let editorElement: HTMLElement | null = $state(null)
|
|
28
31
|
let position: Position = $state(JSON.parse(localStorage.getItem(editorPositionKey) || '{ "x": 0, "y": 0 }'))
|
|
32
|
+
let height: number = $state(parseInt(localStorage.getItem(editorHeightKey) || '0'))
|
|
29
33
|
let manualInjectionActive = $state(false)
|
|
30
34
|
let saving = $state(false)
|
|
31
35
|
let hasError = $state(false)
|
|
@@ -35,12 +39,26 @@
|
|
|
35
39
|
|
|
36
40
|
const linkInjectionsString = $derived(JSON.stringify(linkInjections))
|
|
37
41
|
const hasChanged = $derived(initialStateString !== linkInjectionsString)
|
|
38
|
-
|
|
42
|
+
// Filter out injections without title_details, injections that are removed, duplicate, or are AI injections that failed to inject
|
|
43
|
+
const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
|
|
44
|
+
const sortedInjections = $derived(sortInjections(filteredInjections))
|
|
39
45
|
|
|
40
46
|
$effect(() => {
|
|
41
|
-
if (
|
|
47
|
+
if (loading) return
|
|
48
|
+
|
|
49
|
+
untrack(() => {
|
|
50
|
+
initialStateString = linkInjectionsString
|
|
51
|
+
trackInjectionsCount()
|
|
52
|
+
})
|
|
42
53
|
})
|
|
43
54
|
|
|
55
|
+
function sortInjections(injections: LinkInjection[]): LinkInjection[] {
|
|
56
|
+
return injections.sort((a, b) => {
|
|
57
|
+
if (a.failed !== b.failed) return a.failed ? 1 : -1
|
|
58
|
+
return a.title_details!.title.localeCompare(b.title_details!.title)
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
/** Save the given injections in their current state and rewrite object to returned value */
|
|
45
63
|
async function save(): Promise<void> {
|
|
46
64
|
try {
|
|
@@ -56,8 +74,8 @@
|
|
|
56
74
|
}
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
function
|
|
60
|
-
localStorage.setItem(
|
|
77
|
+
function saveLocalStorage(key: string, value: string): void {
|
|
78
|
+
localStorage.setItem(key, value)
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
function removeInjection(key: string): void {
|
|
@@ -69,9 +87,6 @@
|
|
|
69
87
|
linkInjections = [...linkInjections]
|
|
70
88
|
}
|
|
71
89
|
|
|
72
|
-
/**
|
|
73
|
-
* @param {HTMLElement} itemElement
|
|
74
|
-
*/
|
|
75
90
|
function scrollToItem(itemElement: HTMLElement): void {
|
|
76
91
|
if (!editorElement) return
|
|
77
92
|
|
|
@@ -90,18 +105,25 @@
|
|
|
90
105
|
}
|
|
91
106
|
}
|
|
92
107
|
|
|
93
|
-
/**
|
|
94
|
-
* @param {Event} event
|
|
95
|
-
*/
|
|
96
108
|
function onscroll(event: Event): void {
|
|
97
109
|
const target = event.target as HTMLElement
|
|
98
110
|
scrollDistance = target.scrollTop
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
function trackInjectionsCount() {
|
|
114
|
+
const payload = {
|
|
115
|
+
total: linkInjections.length.toString(),
|
|
116
|
+
failed_automatic: linkInjections.filter(i => i.failed && !i.manual).length.toString(),
|
|
117
|
+
failed_manual: linkInjections.filter(i => i.failed && i.manual).length.toString(),
|
|
118
|
+
final_injected: filteredInjections.length.toString(),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
track(TrackingEvent.TotalInjectionsCount, null, payload)
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
/**
|
|
102
125
|
* This is called from outside when new AI links are ready.
|
|
103
126
|
* We only pass on links that do not already exist.
|
|
104
|
-
* @param {LinkInjection[]} injections
|
|
105
127
|
*/
|
|
106
128
|
export function requestNewAIInjections(injections: LinkInjection[]): void {
|
|
107
129
|
const newInjections = injections.filter(injection => {
|
|
@@ -115,20 +137,26 @@
|
|
|
115
137
|
</script>
|
|
116
138
|
|
|
117
139
|
<section class="editor playpilot-styled-scrollbar" class:panel-open={manualInjectionActive} class:loading bind:this={editorElement} {onscroll}>
|
|
118
|
-
{#if editorElement}
|
|
119
|
-
<div class="drag-handle">
|
|
120
|
-
<DragHandle element={editorElement} {position} limit={{ x: 16, y: 16 }} onchange={savePosition} />
|
|
121
|
-
</div>
|
|
122
|
-
{/if}
|
|
123
|
-
|
|
124
140
|
<header class="header">
|
|
141
|
+
{#if editorElement}
|
|
142
|
+
{#if !loading}
|
|
143
|
+
<div class="handle">
|
|
144
|
+
<ResizeHandle element={editorElement} {height} onchange={(height) => saveLocalStorage(editorHeightKey, JSON.stringify(height))} />
|
|
145
|
+
</div>
|
|
146
|
+
{/if}
|
|
147
|
+
|
|
148
|
+
<div class="handle">
|
|
149
|
+
<DragHandle element={editorElement} {position} limit={{ x: 16, y: 16 }} onchange={(position) => saveLocalStorage(editorPositionKey, JSON.stringify(position))} />
|
|
150
|
+
</div>
|
|
151
|
+
{/if}
|
|
152
|
+
|
|
125
153
|
<h1>Playlinks</h1>
|
|
126
154
|
|
|
127
155
|
{#if loading}
|
|
128
156
|
<div class="loading">Loading...</div>
|
|
129
157
|
{:else}
|
|
130
|
-
<div class="bubble" aria-label="{
|
|
131
|
-
{
|
|
158
|
+
<div class="bubble" aria-label="{filteredInjections.length} found playlinks">
|
|
159
|
+
{filteredInjections.length}
|
|
132
160
|
</div>
|
|
133
161
|
|
|
134
162
|
<RoundButton onclick={() => manualInjectionActive = true} size="24px" aria-label="Add manual injection">
|
|
@@ -138,7 +166,7 @@
|
|
|
138
166
|
</header>
|
|
139
167
|
|
|
140
168
|
{#if !loading && aiRunning}
|
|
141
|
-
<AIIndicator bind:this={aIIndicator} onadd={(newInjections) => newInjections.forEach(i => linkInjections.push(i))} />
|
|
169
|
+
<AIIndicator {htmlString} bind:this={aIIndicator} onadd={(newInjections) => newInjections.forEach(i => linkInjections.push(i))} />
|
|
142
170
|
{/if}
|
|
143
171
|
|
|
144
172
|
{#if !loading}
|
|
@@ -149,13 +177,11 @@
|
|
|
149
177
|
{/if}
|
|
150
178
|
|
|
151
179
|
<div class="items">
|
|
152
|
-
{#each
|
|
180
|
+
{#each sortedInjections as linkInjection (linkInjection.key)}
|
|
153
181
|
<!-- 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 -->
|
|
154
182
|
{@const index = linkInjections.findIndex((i) => i.key === linkInjection.key)}
|
|
155
183
|
|
|
156
|
-
<
|
|
157
|
-
<EditorItem bind:linkInjection={linkInjections[index]} onremove={() => removeInjection(linkInjection.key)} onhighlight={scrollToItem} />
|
|
158
|
-
</div>
|
|
184
|
+
<EditorItem bind:linkInjection={linkInjections[index]} onremove={() => removeInjection(linkInjection.key)} onhighlight={scrollToItem} />
|
|
159
185
|
{:else}
|
|
160
186
|
<div class="empty">No links available. Add links manually by clicking the + button and select text to add a link to.</div>
|
|
161
187
|
{/each}
|
|
@@ -170,7 +196,7 @@
|
|
|
170
196
|
|
|
171
197
|
{#if manualInjectionActive}
|
|
172
198
|
<div
|
|
173
|
-
class="panel"
|
|
199
|
+
class="panel playpilot-styled-scrollbar"
|
|
174
200
|
style:top="{scrollDistance}px"
|
|
175
201
|
transition:fly={{ x: Math.min(window.innerWidth, 320), duration: 200, opacity: 1 }}>
|
|
176
202
|
<ManualInjection
|
|
@@ -198,8 +224,9 @@
|
|
|
198
224
|
bottom: margin(1);
|
|
199
225
|
right: margin(1);
|
|
200
226
|
width: 100%;
|
|
201
|
-
max-width: margin(
|
|
227
|
+
max-width: margin(22);
|
|
202
228
|
height: min(50vh, margin(50));
|
|
229
|
+
min-height: 10rem;
|
|
203
230
|
padding: margin(1);
|
|
204
231
|
border-radius: margin(1.5);
|
|
205
232
|
background: var(--playpilot-dark);
|
|
@@ -218,6 +245,7 @@
|
|
|
218
245
|
|
|
219
246
|
.loading {
|
|
220
247
|
height: auto;
|
|
248
|
+
min-height: 0;
|
|
221
249
|
border-radius: margin(2);
|
|
222
250
|
margin-left: auto;
|
|
223
251
|
padding-right: margin(0.5);
|
|
@@ -225,7 +253,7 @@
|
|
|
225
253
|
font-size: margin(0.85);
|
|
226
254
|
}
|
|
227
255
|
|
|
228
|
-
.
|
|
256
|
+
.handle {
|
|
229
257
|
opacity: 0;
|
|
230
258
|
transition: opacity 150ms;
|
|
231
259
|
|
|
@@ -240,6 +268,7 @@
|
|
|
240
268
|
top: margin(-1);
|
|
241
269
|
margin: margin(-1) margin(-1) 0;
|
|
242
270
|
padding: margin(1) margin(1) margin(1) margin(1.5);
|
|
271
|
+
border: 0;
|
|
243
272
|
background: var(--playpilot-dark);
|
|
244
273
|
display: flex;
|
|
245
274
|
align-items: center;
|
|
@@ -291,6 +320,7 @@
|
|
|
291
320
|
transition: opacity 100ms;
|
|
292
321
|
font-family: inherit;
|
|
293
322
|
color: var(--playpilot-text-color-alt);
|
|
323
|
+
font-size: 0.85rem;
|
|
294
324
|
cursor: pointer;
|
|
295
325
|
|
|
296
326
|
&:hover {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { slide } from 'svelte/transition'
|
|
3
3
|
import IconChevron from '../Icons/IconChevron.svelte'
|
|
4
|
-
import IconIMDb from '../Icons/IconIMDb.svelte'
|
|
5
4
|
import Switch from './Switch.svelte'
|
|
6
5
|
import TextInput from './TextInput.svelte'
|
|
7
6
|
import PlaylinkTypeSelect from './PlaylinkTypeSelect.svelte'
|
|
@@ -12,6 +11,7 @@
|
|
|
12
11
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
13
12
|
import type { LinkInjection } from '$lib/types/injection'
|
|
14
13
|
import type { TitleData } from '$lib/types/title'
|
|
14
|
+
import { truncateAroundPhrase } from '$lib/text'
|
|
15
15
|
|
|
16
16
|
interface Props {
|
|
17
17
|
linkInjection: LinkInjection,
|
|
@@ -22,17 +22,19 @@
|
|
|
22
22
|
|
|
23
23
|
const { linkInjection = $bindable(), onremove = () => null, onhighlight = () => null }: Props = $props()
|
|
24
24
|
|
|
25
|
-
const { key, sentence, title_details, failed } = $derived(linkInjection || {})
|
|
25
|
+
const { key, sentence, title_details, failed, inactive } = $derived(linkInjection || {})
|
|
26
26
|
|
|
27
27
|
// @ts-ignore Definitely not null
|
|
28
28
|
const title: TitleData = $derived(title_details)
|
|
29
|
+
const truncatedSentence = $derived(truncateAroundPhrase(linkInjection.sentence, linkInjection.title, 60))
|
|
29
30
|
|
|
30
31
|
let expanded = $state(false)
|
|
32
|
+
let expandedSentence = $state(false)
|
|
31
33
|
let highlighted = $state(false)
|
|
32
34
|
let element: HTMLElement | null = $state(null)
|
|
33
35
|
|
|
34
36
|
onMount(() => {
|
|
35
|
-
if (failed) track(TrackingEvent.InjectionFailed, title, { phrase: linkInjection.title, sentence
|
|
37
|
+
if (failed) track(TrackingEvent.InjectionFailed, title, { phrase: linkInjection.title, sentence})
|
|
36
38
|
})
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -85,24 +87,26 @@
|
|
|
85
87
|
<div
|
|
86
88
|
class="item"
|
|
87
89
|
class:highlighted
|
|
90
|
+
class:inactive
|
|
88
91
|
onmouseenter={() => toggleOnPageResultHighlight(true)}
|
|
89
92
|
onmouseleave={() => toggleOnPageResultHighlight(false)}
|
|
90
93
|
onclick={scrollLinkIntoView}
|
|
91
94
|
bind:this={element}
|
|
92
95
|
out:slide|global={{ duration: 200 }}>
|
|
93
96
|
<div class="header">
|
|
94
|
-
<img class="poster" src={title.standing_poster} alt="" />
|
|
97
|
+
<img class="poster" src={title.standing_poster} alt="" width="32" height="48" />
|
|
95
98
|
|
|
96
99
|
<div class="info">
|
|
97
100
|
<div class="title">{title.title}</div>
|
|
98
101
|
|
|
99
|
-
<div class="
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<span>{title.type}</span>
|
|
102
|
+
<div class="sentence">
|
|
103
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
104
|
+
{@html (expandedSentence ? sentence : truncatedSentence).replace(linkInjection.title, `<em>${linkInjection.title}</em>`)}
|
|
103
105
|
|
|
104
|
-
{#if
|
|
105
|
-
<
|
|
106
|
+
{#if truncatedSentence.length < sentence.length}
|
|
107
|
+
<button class="expand-sentence" onclick={() => expandedSentence = !expandedSentence}>
|
|
108
|
+
{expandedSentence ? 'Less' : 'More'}
|
|
109
|
+
</button>
|
|
106
110
|
{/if}
|
|
107
111
|
</div>
|
|
108
112
|
</div>
|
|
@@ -123,8 +127,8 @@
|
|
|
123
127
|
<IconChevron {expanded} />
|
|
124
128
|
</button>
|
|
125
129
|
|
|
126
|
-
<Switch label=
|
|
127
|
-
Visible
|
|
130
|
+
<Switch label={inactive ? 'Inactive' : 'Visible'} active={!linkInjection.inactive} onclick={(active) => { linkInjection.inactive = !active; linkInjection.manual = true }}>
|
|
131
|
+
{inactive ? 'Inactive' : 'Visible'}
|
|
128
132
|
</Switch>
|
|
129
133
|
</div>
|
|
130
134
|
{/if}
|
|
@@ -174,6 +178,10 @@
|
|
|
174
178
|
height: margin(3);
|
|
175
179
|
border-radius: margin(0.25);
|
|
176
180
|
background: var(--playpilot-content);
|
|
181
|
+
|
|
182
|
+
.inactive & {
|
|
183
|
+
opacity: 0.35;
|
|
184
|
+
}
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
.title {
|
|
@@ -181,26 +189,46 @@
|
|
|
181
189
|
word-break: break-word;
|
|
182
190
|
}
|
|
183
191
|
|
|
184
|
-
.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
192
|
+
.sentence {
|
|
193
|
+
font-size: 0.7em;
|
|
194
|
+
line-height: 150%;
|
|
195
|
+
margin-right: margin(-0.5);
|
|
188
196
|
color: var(--playpilot-text-color-alt);
|
|
197
|
+
opacity: 0.75;
|
|
189
198
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
199
|
+
:global(em) {
|
|
200
|
+
color: var(--playpilot-text-color);
|
|
201
|
+
font-weight: bold;
|
|
193
202
|
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.expand-sentence {
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
appearance: none;
|
|
208
|
+
border: 0;
|
|
209
|
+
padding: 0;
|
|
210
|
+
margin: 0;
|
|
211
|
+
background: transparent;
|
|
212
|
+
font-family: inherit;
|
|
213
|
+
color: inherit;
|
|
214
|
+
font-size: inherit;
|
|
215
|
+
line-height: inherit;
|
|
216
|
+
font-style: italic;
|
|
217
|
+
text-decoration: underline;
|
|
194
218
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
margin: margin(-0.125) margin(0.125) 0 0;
|
|
198
|
-
height: 1em;
|
|
219
|
+
&:hover {
|
|
220
|
+
color: var(--playpilot-text-color);
|
|
199
221
|
}
|
|
200
222
|
}
|
|
201
223
|
|
|
202
224
|
.content {
|
|
203
|
-
padding-top: margin(
|
|
225
|
+
padding-top: margin(0.5);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.info {
|
|
229
|
+
.inactive & {
|
|
230
|
+
opacity: 0.35;
|
|
231
|
+
}
|
|
204
232
|
}
|
|
205
233
|
|
|
206
234
|
.context-menu {
|