@ash-qw/dsh-theme-prts 0.1.91
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/LICENSE +24 -0
- package/README.en.md +103 -0
- package/README.md +107 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +7466 -0
- package/lib/index.js +3 -0
- package/package.json +64 -0
- package/src/assets/faction-emblems/lungmen.png +0 -0
- package/src/assets/faction-emblems/penguin-logistics.png +0 -0
- package/src/assets/faction-emblems/reunion.png +0 -0
- package/src/assets/faction-emblems/rhine-lab.png +0 -0
- package/src/assets/faction-emblems/rhodes-island.png +0 -0
- package/src/assets/prts-original-avatar.png +0 -0
- package/src/assets/rhodes-dsh.svg +6 -0
- package/src/assets/topographic-map.svg +72 -0
- package/src/assets/topographic-tech.svg +19 -0
- package/src/client/assistant-glass-adapter.js +231 -0
- package/src/client/composer-glass-adapter.js +210 -0
- package/src/client/conversation-control-adapter.js +109 -0
- package/src/client/conversation-scale-adapter.js +2243 -0
- package/src/client/facility-geometry.js +60 -0
- package/src/client/facility-vector.js +230 -0
- package/src/client/floating-glass-adapter.js +177 -0
- package/src/client/host-geometry-adapter.js +123 -0
- package/src/client/index.js +359 -0
- package/src/client/layout-stability-diagnostics.js +162 -0
- package/src/client/operations-shell-v2.js +248 -0
- package/src/client/particle-field-adapter.js +1840 -0
- package/src/client/preferences.js +239 -0
- package/src/client/rc7-adapter.js +83 -0
- package/src/client/resize-shield-adapter.js +64 -0
- package/src/client/settings-page.js +62 -0
- package/src/client/sidebar-control-adapter.js +317 -0
- package/src/client/startup-sequence.js +366 -0
- package/src/client/status-projection.js +79 -0
- package/src/client/terrain-motion-adapter.js +188 -0
- package/src/client/theme-controller-v2.js +134 -0
- package/src/client/theme-controller.js +119 -0
- package/src/client/theme-settings-workbench.js +583 -0
- package/src/client/to-bottom-adapter.js +85 -0
- package/src/client/ui-store.js +23 -0
- package/src/index.js +4 -0
- package/src/styles/conversation-scale.css +471 -0
- package/src/styles/facility-vector.css +292 -0
- package/src/styles/prts.css +1223 -0
- package/src/styles/refinement.css +429 -0
- package/src/styles/stabilization.css +161 -0
- package/src/styles/startup-sequence.css +496 -0
- package/src/styles/theme-settings-workbench.css +803 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { DEFAULT_PREFERENCES, isSafeMode, loadPreferences, persistPreferences, resetPreferenceGroup, updatePreferenceValue } from './preferences.js'
|
|
2
|
+
import { createHostGeometryAdapter } from './host-geometry-adapter.js'
|
|
3
|
+
import { createComposerGlassAdapter } from './composer-glass-adapter.js'
|
|
4
|
+
import { createAssistantGlassAdapter } from './assistant-glass-adapter.js'
|
|
5
|
+
import { createRc7Adapter } from './rc7-adapter.js'
|
|
6
|
+
import { createHarnessStatusSource, EMPTY_STATUS, projectHarnessStatus } from './status-projection.js'
|
|
7
|
+
import { createPrtsUiStore } from './ui-store.js'
|
|
8
|
+
import { createSettingsPage } from './settings-page.js'
|
|
9
|
+
import { createPrtsStartupSequence, findNativeHarnessLoader } from './startup-sequence.js'
|
|
10
|
+
import { createOperationsShell } from './operations-shell-v2.js'
|
|
11
|
+
import { createThemeController } from './theme-controller-v2.js'
|
|
12
|
+
import { createParticleFieldAdapter } from './particle-field-adapter.js'
|
|
13
|
+
import { createToBottomAdapter } from './to-bottom-adapter.js'
|
|
14
|
+
import { createConversationControlAdapter } from './conversation-control-adapter.js'
|
|
15
|
+
import { createConversationScaleAdapter } from './conversation-scale-adapter.js'
|
|
16
|
+
import { createLayoutStabilityDiagnostics } from './layout-stability-diagnostics.js'
|
|
17
|
+
import { createSidebarControlAdapter } from './sidebar-control-adapter.js'
|
|
18
|
+
import { createFloatingGlassAdapter } from './floating-glass-adapter.js'
|
|
19
|
+
|
|
20
|
+
export const inject = ['slots', 'theme']
|
|
21
|
+
|
|
22
|
+
function contextService(ctx, name) {
|
|
23
|
+
try {
|
|
24
|
+
const service = ctx?.[name]
|
|
25
|
+
if (service !== undefined && service !== null) return service
|
|
26
|
+
} catch {}
|
|
27
|
+
try {
|
|
28
|
+
return ctx?.get?.(name)
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function applyPrtsPlugin(ctx, environment) {
|
|
35
|
+
const {
|
|
36
|
+
document,
|
|
37
|
+
window,
|
|
38
|
+
React,
|
|
39
|
+
defineStore,
|
|
40
|
+
cssText = '',
|
|
41
|
+
assets = {},
|
|
42
|
+
version = 'development',
|
|
43
|
+
startupTimings,
|
|
44
|
+
} = environment
|
|
45
|
+
const safeMode = isSafeMode(window?.location?.search ?? '')
|
|
46
|
+
const storage = window?.localStorage
|
|
47
|
+
const themeService = contextService(ctx, 'theme')
|
|
48
|
+
const theme = createThemeController({ document, window, cssText, service: themeService })
|
|
49
|
+
const startup = createPrtsStartupSequence({ document, window, prtsEmblem: assets.prtsEmblem, rhodesEmblem: assets.emblem, timings: startupTimings })
|
|
50
|
+
const composerGlass = createComposerGlassAdapter({ document, window })
|
|
51
|
+
const assistantGlass = createAssistantGlassAdapter({ document, window })
|
|
52
|
+
const particleField = createParticleFieldAdapter({
|
|
53
|
+
document,
|
|
54
|
+
window,
|
|
55
|
+
emblems: assets.emblems,
|
|
56
|
+
emblemMasks: assets.emblemMasks,
|
|
57
|
+
heroEmblemMask: assets.heroEmblemMask,
|
|
58
|
+
onStateChange(next) {
|
|
59
|
+
status = { ...status, particle: next }
|
|
60
|
+
sync()
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
const sessions = contextService(ctx, 'sessions')
|
|
64
|
+
const toBottom = createToBottomAdapter({ document, window })
|
|
65
|
+
const conversationControls = createConversationControlAdapter({ document, window })
|
|
66
|
+
const conversationScale = createConversationScaleAdapter({ document, window, sessions })
|
|
67
|
+
const layoutDiagnostics = createLayoutStabilityDiagnostics({ document, window })
|
|
68
|
+
const sidebarControls = createSidebarControlAdapter({ document, window })
|
|
69
|
+
const floatingGlass = createFloatingGlassAdapter({ document, window })
|
|
70
|
+
const adapter = createRc7Adapter({ document })
|
|
71
|
+
const hostGeometry = createHostGeometryAdapter({ document, window })
|
|
72
|
+
const connection = contextService(ctx, 'connection')
|
|
73
|
+
let operations
|
|
74
|
+
let mountObserver
|
|
75
|
+
let mountFrame
|
|
76
|
+
let startupReadyObserver
|
|
77
|
+
let startupSettleFrame
|
|
78
|
+
let preferences = loadPreferences(storage)
|
|
79
|
+
let settingsPersistence = { phase: 'idle', revision: 0 }
|
|
80
|
+
let status = connection ? projectHarnessStatus({ connection }) : EMPTY_STATUS
|
|
81
|
+
const bindings = new Set()
|
|
82
|
+
let revision = 0
|
|
83
|
+
const stopThemeSync = typeof ctx.on === 'function'
|
|
84
|
+
? ctx.on('theme/change', value => value == null ? theme.refresh() : theme.sync(value))
|
|
85
|
+
: undefined
|
|
86
|
+
let mountWarningIssued = false
|
|
87
|
+
|
|
88
|
+
const sync = () => {
|
|
89
|
+
revision += 1
|
|
90
|
+
for (const actions of bindings) actions.sync({ ...preferences }, { ...status }, revision)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const cancelMountFrame = () => {
|
|
94
|
+
if (mountFrame === undefined) return
|
|
95
|
+
if (window?.cancelAnimationFrame) window.cancelAnimationFrame(mountFrame)
|
|
96
|
+
else window?.clearTimeout?.(mountFrame)
|
|
97
|
+
mountFrame = undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const stopMountWatch = () => {
|
|
101
|
+
mountObserver?.disconnect()
|
|
102
|
+
mountObserver = undefined
|
|
103
|
+
cancelMountFrame()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const stopStartupReadyWatch = () => {
|
|
107
|
+
startupReadyObserver?.disconnect()
|
|
108
|
+
startupReadyObserver = undefined
|
|
109
|
+
if (startupSettleFrame === undefined) return
|
|
110
|
+
if (window?.cancelAnimationFrame) window.cancelAnimationFrame(startupSettleFrame)
|
|
111
|
+
else window?.clearTimeout?.(startupSettleFrame)
|
|
112
|
+
startupSettleFrame = undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const disposeOperations = () => {
|
|
116
|
+
operations?.dispose()
|
|
117
|
+
operations = undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const scheduleMountCheck = () => {
|
|
121
|
+
if (mountFrame !== undefined) return
|
|
122
|
+
const run = () => {
|
|
123
|
+
mountFrame = undefined
|
|
124
|
+
if (!preferences.enabled || safeMode || operations) return
|
|
125
|
+
applyVisualState()
|
|
126
|
+
}
|
|
127
|
+
mountFrame = window?.requestAnimationFrame?.(run)
|
|
128
|
+
if (mountFrame === undefined) mountFrame = window?.setTimeout?.(run, 16)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const watchMount = () => {
|
|
132
|
+
if (mountObserver || !window?.MutationObserver || !document?.body) return
|
|
133
|
+
mountObserver = new window.MutationObserver(scheduleMountCheck)
|
|
134
|
+
mountObserver.observe(document.body, { childList: true, subtree: true })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const disposeVisualState = () => {
|
|
138
|
+
startup.stop({ restoreFocus: false })
|
|
139
|
+
stopStartupReadyWatch()
|
|
140
|
+
stopMountWatch()
|
|
141
|
+
mountWarningIssued = false
|
|
142
|
+
disposeOperations()
|
|
143
|
+
composerGlass.dispose()
|
|
144
|
+
assistantGlass.dispose()
|
|
145
|
+
particleField.update({ ...preferences, enabled: false })
|
|
146
|
+
toBottom.dispose()
|
|
147
|
+
conversationControls.dispose()
|
|
148
|
+
conversationScale.dispose()
|
|
149
|
+
layoutDiagnostics.dispose()
|
|
150
|
+
sidebarControls.dispose()
|
|
151
|
+
floatingGlass.dispose()
|
|
152
|
+
theme.apply({ ...preferences, enabled: false })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const applyVisualState = ({ themeAlreadyApplied = false } = {}) => {
|
|
156
|
+
if (safeMode || !preferences.enabled) {
|
|
157
|
+
disposeVisualState()
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (!themeAlreadyApplied) theme.apply(preferences)
|
|
161
|
+
operations ||= createOperationsShell({
|
|
162
|
+
document, window, assets, adapter, hostGeometry,
|
|
163
|
+
onSchemeToggle(next) {
|
|
164
|
+
theme.setTheme(next)
|
|
165
|
+
},
|
|
166
|
+
onThemeDisable() {
|
|
167
|
+
persistAndApply(updatePreferenceValue(preferences, 'enabled', false))
|
|
168
|
+
},
|
|
169
|
+
onPreferenceChange(key, value) {
|
|
170
|
+
persistAndApply(updatePreferenceValue(preferences, key, value))
|
|
171
|
+
},
|
|
172
|
+
onVisualReset() {
|
|
173
|
+
persistAndApply({
|
|
174
|
+
...DEFAULT_PREFERENCES,
|
|
175
|
+
enabled: preferences.enabled,
|
|
176
|
+
bootAnimation: preferences.bootAnimation,
|
|
177
|
+
})
|
|
178
|
+
},
|
|
179
|
+
onRetrySave() {
|
|
180
|
+
retryPersistence()
|
|
181
|
+
},
|
|
182
|
+
getConversationScalePreview(value) {
|
|
183
|
+
return conversationScale.getCalibrationState(value)
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
if (!operations.update(preferences, status, settingsPersistence)) {
|
|
187
|
+
if (!mountWarningIssued) {
|
|
188
|
+
window?.console?.warn?.(
|
|
189
|
+
'[dsh-theme-prts] operations shell mount deferred; base theme remains active',
|
|
190
|
+
)
|
|
191
|
+
mountWarningIssued = true
|
|
192
|
+
}
|
|
193
|
+
disposeOperations()
|
|
194
|
+
watchMount()
|
|
195
|
+
} else {
|
|
196
|
+
mountWarningIssued = false
|
|
197
|
+
stopMountWatch()
|
|
198
|
+
}
|
|
199
|
+
particleField.update(preferences)
|
|
200
|
+
composerGlass.start()
|
|
201
|
+
assistantGlass.start()
|
|
202
|
+
toBottom.start()
|
|
203
|
+
conversationControls.start()
|
|
204
|
+
conversationScale.update(preferences)
|
|
205
|
+
conversationScale.start()
|
|
206
|
+
layoutDiagnostics.start()
|
|
207
|
+
sidebarControls.start()
|
|
208
|
+
floatingGlass.start()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const persistAndApply = next => {
|
|
212
|
+
const wasEnabled = Boolean(preferences.enabled)
|
|
213
|
+
const result = persistPreferences(storage, next)
|
|
214
|
+
preferences = result.preferences
|
|
215
|
+
settingsPersistence = {
|
|
216
|
+
phase: result.persisted ? 'saved' : 'error',
|
|
217
|
+
revision: settingsPersistence.revision + 1,
|
|
218
|
+
}
|
|
219
|
+
applyVisualState()
|
|
220
|
+
sync()
|
|
221
|
+
if (!safeMode && !wasEnabled && preferences.enabled && preferences.bootAnimation) startup.play()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
const retryPersistence = () => {
|
|
226
|
+
const result = persistPreferences(storage, preferences)
|
|
227
|
+
preferences = result.preferences
|
|
228
|
+
settingsPersistence = {
|
|
229
|
+
phase: result.persisted ? 'saved' : 'error',
|
|
230
|
+
revision: settingsPersistence.revision + 1,
|
|
231
|
+
}
|
|
232
|
+
operations?.update(preferences, status, settingsPersistence)
|
|
233
|
+
sync()
|
|
234
|
+
}
|
|
235
|
+
const statusSource = createHarnessStatusSource({
|
|
236
|
+
connection,
|
|
237
|
+
onChange(next) {
|
|
238
|
+
status = next
|
|
239
|
+
operations?.update(preferences, status, settingsPersistence)
|
|
240
|
+
sync()
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
const nativeLoader = !safeMode && preferences.enabled && preferences.bootAnimation
|
|
246
|
+
? findNativeHarnessLoader(document)
|
|
247
|
+
: null
|
|
248
|
+
if (nativeLoader?.isConnected) {
|
|
249
|
+
theme.apply(preferences)
|
|
250
|
+
let visualStatePending = true
|
|
251
|
+
let visualStateSettled = false
|
|
252
|
+
let settleFrames = 0
|
|
253
|
+
const requestSettleFrame = callback => window?.requestAnimationFrame?.(callback)
|
|
254
|
+
?? window?.setTimeout?.(callback, 16)
|
|
255
|
+
const ensureVisualState = () => {
|
|
256
|
+
if (nativeLoader.isConnected) return false
|
|
257
|
+
if (visualStatePending) {
|
|
258
|
+
visualStatePending = false
|
|
259
|
+
applyVisualState({ themeAlreadyApplied: true })
|
|
260
|
+
const settle = () => {
|
|
261
|
+
startupSettleFrame = undefined
|
|
262
|
+
const particleCanvas = document.querySelector('[data-prts-particle-layer]')
|
|
263
|
+
const particleReady = !particleCanvas || particleCanvas.hasAttribute('data-prts-particle-ready')
|
|
264
|
+
settleFrames = particleReady ? settleFrames + 1 : 0
|
|
265
|
+
if (settleFrames >= 2) {
|
|
266
|
+
visualStateSettled = true
|
|
267
|
+
startupReadyObserver?.disconnect()
|
|
268
|
+
startupReadyObserver = undefined
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
startupSettleFrame = requestSettleFrame(settle)
|
|
272
|
+
}
|
|
273
|
+
startupSettleFrame = requestSettleFrame(settle)
|
|
274
|
+
}
|
|
275
|
+
return visualStateSettled
|
|
276
|
+
}
|
|
277
|
+
if (window?.MutationObserver && document?.body) {
|
|
278
|
+
startupReadyObserver = new window.MutationObserver(ensureVisualState)
|
|
279
|
+
startupReadyObserver.observe(document.body, { childList: true, subtree: true })
|
|
280
|
+
}
|
|
281
|
+
const takeoverStarted = startup.play({
|
|
282
|
+
waitForReady: true,
|
|
283
|
+
readyWhen: ensureVisualState,
|
|
284
|
+
})
|
|
285
|
+
if (!takeoverStarted) {
|
|
286
|
+
stopStartupReadyWatch()
|
|
287
|
+
applyVisualState({ themeAlreadyApplied: true })
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
applyVisualState()
|
|
291
|
+
}
|
|
292
|
+
ctx.effect(() => () => {
|
|
293
|
+
statusSource.dispose()
|
|
294
|
+
startup.dispose({ restoreFocus: false })
|
|
295
|
+
stopStartupReadyWatch()
|
|
296
|
+
stopMountWatch()
|
|
297
|
+
disposeOperations()
|
|
298
|
+
composerGlass.dispose()
|
|
299
|
+
assistantGlass.dispose()
|
|
300
|
+
particleField.dispose()
|
|
301
|
+
toBottom.dispose()
|
|
302
|
+
conversationControls.dispose()
|
|
303
|
+
conversationScale.dispose()
|
|
304
|
+
layoutDiagnostics.dispose()
|
|
305
|
+
sidebarControls.dispose()
|
|
306
|
+
floatingGlass.dispose()
|
|
307
|
+
if (typeof stopThemeSync === 'function') stopThemeSync()
|
|
308
|
+
theme.dispose()
|
|
309
|
+
bindings.clear()
|
|
310
|
+
}, 'dsh-theme-prts: visual cleanup')
|
|
311
|
+
|
|
312
|
+
const initialStoreState = { preferences, status, revision }
|
|
313
|
+
const settingsStore = createPrtsUiStore(defineStore, initialStoreState)
|
|
314
|
+
const PrtsSettingsPage = createSettingsPage(React, {
|
|
315
|
+
emblem: assets.emblem,
|
|
316
|
+
version,
|
|
317
|
+
safeMode,
|
|
318
|
+
})
|
|
319
|
+
const injectActions = actions => {
|
|
320
|
+
bindings.add(actions)
|
|
321
|
+
sync()
|
|
322
|
+
return {
|
|
323
|
+
updatePreference(key, value) { persistAndApply(updatePreferenceValue(preferences, key, value)) },
|
|
324
|
+
resetPreferenceGroup(group) {
|
|
325
|
+
persistAndApply(resetPreferenceGroup(preferences, group))
|
|
326
|
+
},
|
|
327
|
+
resetPreferences() {
|
|
328
|
+
persistAndApply({
|
|
329
|
+
...DEFAULT_PREFERENCES,
|
|
330
|
+
enabled: preferences.enabled,
|
|
331
|
+
bootAnimation: preferences.bootAnimation,
|
|
332
|
+
})
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
|
|
338
|
+
name: 'settings.plugins.tab',
|
|
339
|
+
id: 'prts-appearance',
|
|
340
|
+
order: 20,
|
|
341
|
+
label: () => 'P.R.T.S.',
|
|
342
|
+
store: settingsStore,
|
|
343
|
+
inject: injectActions,
|
|
344
|
+
}, PrtsSettingsPage))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function apply(ctx) {
|
|
348
|
+
const React = require('react')
|
|
349
|
+
const { defineStore } = require('@deepseek-ai/dsh-client-runtime/client')
|
|
350
|
+
applyPrtsPlugin(ctx, {
|
|
351
|
+
document,
|
|
352
|
+
window,
|
|
353
|
+
React,
|
|
354
|
+
defineStore,
|
|
355
|
+
cssText: PRTS_CSS,
|
|
356
|
+
assets: PRTS_ASSETS,
|
|
357
|
+
version: PRTS_VERSION,
|
|
358
|
+
})
|
|
359
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const USER_TURN_SELECTOR = [
|
|
2
|
+
'[data-chat-flow-kind="user"]',
|
|
3
|
+
'[data-chat-flow-kind="user-step"]',
|
|
4
|
+
'[data-message-role="user"]',
|
|
5
|
+
].join(',')
|
|
6
|
+
|
|
7
|
+
const RECORD_LIMIT = 240
|
|
8
|
+
|
|
9
|
+
function nodeLabel(node) {
|
|
10
|
+
if (!node?.tagName) return ''
|
|
11
|
+
const role = node.getAttribute?.('data-chat-flow-kind')
|
|
12
|
+
|| node.getAttribute?.('data-message-role')
|
|
13
|
+
|| node.getAttribute?.('data-prts-conversation-control')
|
|
14
|
+
|| node.getAttribute?.('data-prts-to-bottom')
|
|
15
|
+
return `${node.tagName.toLowerCase()}${role ? `[${role}]` : ''}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createLayoutStabilityDiagnostics({ document, window }) {
|
|
19
|
+
let frame
|
|
20
|
+
let performanceObserver
|
|
21
|
+
let mutationObserver
|
|
22
|
+
let started = false
|
|
23
|
+
let lastSample
|
|
24
|
+
let lastUserInput = 0
|
|
25
|
+
const records = []
|
|
26
|
+
|
|
27
|
+
function enabled() {
|
|
28
|
+
if (document?.documentElement?.dataset?.prtsDiagnostics === 'layout') return true
|
|
29
|
+
try {
|
|
30
|
+
return new URLSearchParams(window?.location?.search ?? '').get('prtsDiagnostics') === 'layout'
|
|
31
|
+
} catch {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function push(type, detail = {}) {
|
|
37
|
+
records.push({
|
|
38
|
+
type,
|
|
39
|
+
time: Number((window?.performance?.now?.() ?? Date.now()).toFixed?.(2) ?? Date.now()),
|
|
40
|
+
sinceUserInput: Math.max(0, Date.now() - lastUserInput),
|
|
41
|
+
...detail,
|
|
42
|
+
})
|
|
43
|
+
if (records.length > RECORD_LIMIT) records.splice(0, records.length - RECORD_LIMIT)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function visibleAnchor(scroller) {
|
|
47
|
+
const scrollerBox = scroller?.getBoundingClientRect?.()
|
|
48
|
+
if (!scrollerBox) return {}
|
|
49
|
+
for (const turn of scroller.querySelectorAll?.(USER_TURN_SELECTOR) ?? []) {
|
|
50
|
+
const box = turn.getBoundingClientRect?.()
|
|
51
|
+
if (box && box.bottom > scrollerBox.top && box.top < scrollerBox.bottom) {
|
|
52
|
+
return { node: nodeLabel(turn), top: Number(box.top.toFixed(2)) }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sample() {
|
|
59
|
+
frame = undefined
|
|
60
|
+
if (!started) return
|
|
61
|
+
const scroller = document?.querySelector?.('[data-prts-region="operation"] [data-conversation-scroll]')
|
|
62
|
+
if (scroller) {
|
|
63
|
+
const anchor = visibleAnchor(scroller)
|
|
64
|
+
const next = {
|
|
65
|
+
scrollTop: Number(Number(scroller.scrollTop).toFixed(2)),
|
|
66
|
+
scrollHeight: Number(scroller.scrollHeight),
|
|
67
|
+
clientHeight: Number(scroller.clientHeight),
|
|
68
|
+
anchor: anchor.node,
|
|
69
|
+
anchorTop: anchor.top,
|
|
70
|
+
}
|
|
71
|
+
if (lastSample) {
|
|
72
|
+
const scrollDelta = next.scrollTop - lastSample.scrollTop
|
|
73
|
+
const heightDelta = next.scrollHeight - lastSample.scrollHeight
|
|
74
|
+
const anchorDelta = next.anchor === lastSample.anchor && Number.isFinite(next.anchorTop) && Number.isFinite(lastSample.anchorTop)
|
|
75
|
+
? Number((next.anchorTop - lastSample.anchorTop).toFixed(2))
|
|
76
|
+
: undefined
|
|
77
|
+
if (scrollDelta || heightDelta || anchorDelta) {
|
|
78
|
+
push('geometry', { scrollDelta, heightDelta, anchorDelta, anchor: next.anchor })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
lastSample = next
|
|
82
|
+
} else {
|
|
83
|
+
lastSample = undefined
|
|
84
|
+
}
|
|
85
|
+
frame = window?.requestAnimationFrame?.(sample)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function onUserInput(event) {
|
|
89
|
+
lastUserInput = Date.now()
|
|
90
|
+
push('input', { event: event.type, key: event.key })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function startPerformanceObserver() {
|
|
94
|
+
const PerformanceObserver = window?.PerformanceObserver
|
|
95
|
+
if (typeof PerformanceObserver !== 'function') return
|
|
96
|
+
try {
|
|
97
|
+
performanceObserver = new PerformanceObserver(list => {
|
|
98
|
+
for (const entry of list.getEntries()) {
|
|
99
|
+
if (entry.entryType === 'layout-shift') {
|
|
100
|
+
push('layout-shift', {
|
|
101
|
+
value: entry.value,
|
|
102
|
+
recentInput: entry.hadRecentInput,
|
|
103
|
+
sources: (entry.sources ?? []).map(source => nodeLabel(source.node)).filter(Boolean),
|
|
104
|
+
})
|
|
105
|
+
} else if (entry.entryType === 'longtask') {
|
|
106
|
+
push('long-task', { duration: Number(entry.duration.toFixed(2)) })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
performanceObserver.observe({ type: 'layout-shift', buffered: true })
|
|
111
|
+
performanceObserver.observe({ type: 'longtask', buffered: true })
|
|
112
|
+
} catch {
|
|
113
|
+
performanceObserver?.disconnect?.()
|
|
114
|
+
performanceObserver = undefined
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
start() {
|
|
120
|
+
if (started || !enabled()) return false
|
|
121
|
+
started = true
|
|
122
|
+
window.__PRTS_LAYOUT_DIAGNOSTICS__ = records
|
|
123
|
+
for (const type of ['wheel', 'touchstart', 'pointerdown', 'keydown']) {
|
|
124
|
+
document?.addEventListener?.(type, onUserInput, { capture: true, passive: true })
|
|
125
|
+
}
|
|
126
|
+
if (window?.MutationObserver && document?.body) {
|
|
127
|
+
mutationObserver = new window.MutationObserver(mutations => {
|
|
128
|
+
for (const mutation of mutations) {
|
|
129
|
+
if (mutation.type === 'attributes') {
|
|
130
|
+
push('adapter-style', {
|
|
131
|
+
attribute: mutation.attributeName,
|
|
132
|
+
node: nodeLabel(mutation.target),
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
mutationObserver.observe(document.body, {
|
|
138
|
+
subtree: true,
|
|
139
|
+
attributes: true,
|
|
140
|
+
attributeFilter: ['data-prts-conversation-control', 'data-prts-to-bottom'],
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
startPerformanceObserver()
|
|
144
|
+
frame = window?.requestAnimationFrame?.(sample)
|
|
145
|
+
return true
|
|
146
|
+
},
|
|
147
|
+
dispose() {
|
|
148
|
+
started = false
|
|
149
|
+
if (frame !== undefined) window?.cancelAnimationFrame?.(frame)
|
|
150
|
+
frame = undefined
|
|
151
|
+
performanceObserver?.disconnect?.()
|
|
152
|
+
mutationObserver?.disconnect?.()
|
|
153
|
+
performanceObserver = undefined
|
|
154
|
+
mutationObserver = undefined
|
|
155
|
+
for (const type of ['wheel', 'touchstart', 'pointerdown', 'keydown']) {
|
|
156
|
+
document?.removeEventListener?.(type, onUserInput, { capture: true })
|
|
157
|
+
}
|
|
158
|
+
if (window?.__PRTS_LAYOUT_DIAGNOSTICS__ === records) delete window.__PRTS_LAYOUT_DIAGNOSTICS__
|
|
159
|
+
lastSample = undefined
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
}
|