@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.
Files changed (50) hide show
  1. package/LICENSE +24 -0
  2. package/README.en.md +103 -0
  3. package/README.md +107 -0
  4. package/THIRD_PARTY_NOTICES.md +48 -0
  5. package/cordis.patch.yml +4 -0
  6. package/lib/client.js +7466 -0
  7. package/lib/index.js +3 -0
  8. package/package.json +64 -0
  9. package/src/assets/faction-emblems/lungmen.png +0 -0
  10. package/src/assets/faction-emblems/penguin-logistics.png +0 -0
  11. package/src/assets/faction-emblems/reunion.png +0 -0
  12. package/src/assets/faction-emblems/rhine-lab.png +0 -0
  13. package/src/assets/faction-emblems/rhodes-island.png +0 -0
  14. package/src/assets/prts-original-avatar.png +0 -0
  15. package/src/assets/rhodes-dsh.svg +6 -0
  16. package/src/assets/topographic-map.svg +72 -0
  17. package/src/assets/topographic-tech.svg +19 -0
  18. package/src/client/assistant-glass-adapter.js +231 -0
  19. package/src/client/composer-glass-adapter.js +210 -0
  20. package/src/client/conversation-control-adapter.js +109 -0
  21. package/src/client/conversation-scale-adapter.js +2243 -0
  22. package/src/client/facility-geometry.js +60 -0
  23. package/src/client/facility-vector.js +230 -0
  24. package/src/client/floating-glass-adapter.js +177 -0
  25. package/src/client/host-geometry-adapter.js +123 -0
  26. package/src/client/index.js +359 -0
  27. package/src/client/layout-stability-diagnostics.js +162 -0
  28. package/src/client/operations-shell-v2.js +248 -0
  29. package/src/client/particle-field-adapter.js +1840 -0
  30. package/src/client/preferences.js +239 -0
  31. package/src/client/rc7-adapter.js +83 -0
  32. package/src/client/resize-shield-adapter.js +64 -0
  33. package/src/client/settings-page.js +62 -0
  34. package/src/client/sidebar-control-adapter.js +317 -0
  35. package/src/client/startup-sequence.js +366 -0
  36. package/src/client/status-projection.js +79 -0
  37. package/src/client/terrain-motion-adapter.js +188 -0
  38. package/src/client/theme-controller-v2.js +134 -0
  39. package/src/client/theme-controller.js +119 -0
  40. package/src/client/theme-settings-workbench.js +583 -0
  41. package/src/client/to-bottom-adapter.js +85 -0
  42. package/src/client/ui-store.js +23 -0
  43. package/src/index.js +4 -0
  44. package/src/styles/conversation-scale.css +471 -0
  45. package/src/styles/facility-vector.css +292 -0
  46. package/src/styles/prts.css +1223 -0
  47. package/src/styles/refinement.css +429 -0
  48. package/src/styles/stabilization.css +161 -0
  49. package/src/styles/startup-sequence.css +496 -0
  50. package/src/styles/theme-settings-workbench.css +803 -0
@@ -0,0 +1,2243 @@
1
+ import { CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT, CONVERSATION_SCALE_FOCUS_CONTRAST_MAX, CONVERSATION_SCALE_FOCUS_CONTRAST_MIN } from './preferences.js'
2
+
3
+ export const CONVERSATION_USER_TURN_SELECTOR = [
4
+ '[data-chat-flow-kind="user"]',
5
+ '[data-chat-flow-kind="user-step"]',
6
+ '[data-message-role="user"]',
7
+ ].join(',')
8
+
9
+ export const CONVERSATION_SCALE_SPACING = 8
10
+ export const CONVERSATION_SCALE_BASE_LENGTHS = Object.freeze([24, 18, 14, 10, 6])
11
+ export const CONVERSATION_SCALE_CONTRAST_DELTAS = Object.freeze([5, -3, -4, -4, -3])
12
+
13
+ export function resolveConversationScaleLengths(value = CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT) {
14
+ const numeric = Number(value)
15
+ const contrast = Number.isFinite(numeric)
16
+ ? Math.min(CONVERSATION_SCALE_FOCUS_CONTRAST_MAX, Math.max(CONVERSATION_SCALE_FOCUS_CONTRAST_MIN, numeric))
17
+ : CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT
18
+ const ratio = contrast / CONVERSATION_SCALE_FOCUS_CONTRAST_MAX
19
+ return CONVERSATION_SCALE_BASE_LENGTHS.map((length, index) =>
20
+ Math.round(length + CONVERSATION_SCALE_CONTRAST_DELTAS[index] * ratio))
21
+ }
22
+
23
+ export const CONVERSATION_SCALE_LENGTHS = Object.freeze(resolveConversationScaleLengths())
24
+ export const CONVERSATION_SCALE_WAVE_DURATION = 140
25
+
26
+ export const CONVERSATION_JUMP_BOTTOM_THRESHOLD = 25
27
+ export const CONVERSATION_JUMP_SMOOTH_VIEWPORT_LIMIT = 1.5
28
+ export const CONVERSATION_JUMP_SETTLE_MAX_DISTANCE = 88
29
+ export const CONVERSATION_JUMP_SETTLE_VIEWPORT_RATIO = .12
30
+ export const CONVERSATION_JUMP_SETTLE_DURATION = 180
31
+ export const CONVERSATION_JUMP_VERIFY_DURATION = 900
32
+ export const CONVERSATION_HISTORY_PAGE_LIMIT = 10
33
+ export const CONVERSATION_HISTORY_TIME_LIMIT = 8000
34
+
35
+ const ASSISTANT_SELECTOR = '[data-chat-flow-kind="assistant-step"], [data-message-role="assistant"]'
36
+ const FORMAL_ANSWER_SELECTOR = '[data-prts-ai-surface], [data-markdown], [data-slot*="markdown" i], .markdown-body'
37
+ const AUXILIARY_SELECTOR = '[data-chat-flow-kind*="reason" i], [data-chat-flow-kind*="tool" i], [data-slot*="reason" i], [data-slot*="tool" i], [data-role*="reason" i], [data-role*="tool" i]'
38
+ const STREAMING_SELECTOR = '[aria-busy="true"], [data-streaming], [data-status*="stream" i], [data-state*="stream" i]'
39
+ const CONVERSATION_THIRD_PARTY_OVERLAY_SELECTOR = [
40
+ '[data-shell-overlay] [data-plugin]:not([data-plugin="dsh-theme-prts"])',
41
+ '[data-slot="shell.overlay"] [data-plugin]:not([data-plugin="dsh-theme-prts"])',
42
+ ].join(', ')
43
+ const CONVERSATION_BLOCKING_OVERLAY_SELECTOR = [
44
+ '[role="dialog"]',
45
+ '[aria-modal="true"]',
46
+ 'dialog[open]',
47
+ '[data-prts-floating-glass="dialog"]',
48
+ '[data-prts-floating-scrim]',
49
+ '[data-slot*="dialog" i]',
50
+ '[data-slot*="modal" i]',
51
+ '[class*="_dialog"]',
52
+ '[class*="_modal"]',
53
+ '[class*="_scrim"]',
54
+ '[class*="_backdrop"]',
55
+ ].join(', ')
56
+ const CONVERSATION_OVERLAY_SELECTOR = [
57
+ CONVERSATION_THIRD_PARTY_OVERLAY_SELECTOR,
58
+ '[role="dialog"]',
59
+ '[role="menu"]',
60
+ '[role="listbox"]',
61
+ '[popover]',
62
+ '[data-prts-floating-glass]',
63
+ '[aria-modal="true"]',
64
+ 'dialog[open]',
65
+ '[data-slot*="dialog" i]',
66
+ '[data-slot*="modal" i]',
67
+ '[data-prts-floating-scrim]',
68
+ '[class*="_dialog"]',
69
+ '[class*="_modal"]',
70
+ '[class*="_scrim"]',
71
+ '[class*="_backdrop"]',
72
+ ].join(', ')
73
+ const CONVERSATION_PORTAL_ROOT_SELECTOR = [
74
+ '[role="presentation"]',
75
+ '[class*="_portal_"]',
76
+ '[data-floating-ui-portal]',
77
+ '[data-radix-portal]',
78
+ ].join(', ')
79
+ const PREVIEW_REMOVE_SELECTOR = [
80
+ '[hidden]',
81
+ '[aria-hidden="true"]',
82
+ 'button',
83
+ '[role="toolbar"]',
84
+ '[data-slot*="toolbar" i]',
85
+ '[data-slot*="action" i]',
86
+ '[data-slot*="sentinel" i]',
87
+ 'script',
88
+ 'style',
89
+ ].join(',')
90
+ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
91
+ const MINIMUM_TURNS = 2
92
+ export const CONVERSATION_SCALE_WIDTH = 32
93
+ export const CONVERSATION_SCALE_CONTENT_GAP = 12
94
+ export const CONVERSATION_SCALE_PREVIEW_WIDTH = 360
95
+ export const CONVERSATION_SCALE_PREVIEW_GAP = 8
96
+ export const CONVERSATION_SCALE_RIGHT_GAP = 12
97
+ export const CONVERSATION_SCALE_HYSTERESIS = 16
98
+ export const CONVERSATION_SCALE_DEFAULT_MAX_DISTANCE = 96
99
+ const SCALE_WIDTH = CONVERSATION_SCALE_WIDTH
100
+ const PREVIEW_WIDTH = CONVERSATION_SCALE_PREVIEW_WIDTH
101
+ const PREVIEW_GAP = CONVERSATION_SCALE_PREVIEW_GAP
102
+ const PREVIEW_FALLBACK_HEIGHT = 128
103
+ const PREVIEW_DELAY = 100
104
+ const HISTORY_TRIGGER_HEIGHT = CONVERSATION_SCALE_SPACING
105
+ const PREVIEW_REFRESH_INTERVAL = 100
106
+ const PREVIEW_TOUCH_DURATION = 1200
107
+ const SNAP_DELAY = 110
108
+ const TOUCH_WAVE_DURATION = 620
109
+ const HISTORY_PAGE_SETTLE_LIMIT = 4000
110
+ let scaleInstanceSeed = 0
111
+
112
+ function clamp(value, minimum, maximum) {
113
+ return Math.min(maximum, Math.max(minimum, value))
114
+ }
115
+
116
+ export function resolveConversationScaleGeometry({
117
+ operationWidth,
118
+ contentLeft,
119
+ maximumDistance = CONVERSATION_SCALE_DEFAULT_MAX_DISTANCE,
120
+ previouslyHidden = false,
121
+ } = {}) {
122
+ const width = Number(operationWidth)
123
+ const content = Number(contentLeft)
124
+ const maximum = Number(maximumDistance)
125
+ if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(content) || content <= 0) {
126
+ return { visible: false, reason: 'unmeasured' }
127
+ }
128
+ const recovery = previouslyHidden ? CONVERSATION_SCALE_HYSTERESIS : 0
129
+ const minimumCorridor = CONVERSATION_SCALE_WIDTH + CONVERSATION_SCALE_CONTENT_GAP * 2 + recovery
130
+ if (content < minimumCorridor) return { visible: false, reason: 'corridor' }
131
+ const configuredLeft = clamp(
132
+ Number.isFinite(maximum) ? maximum : CONVERSATION_SCALE_DEFAULT_MAX_DISTANCE,
133
+ CONVERSATION_SCALE_CONTENT_GAP,
134
+ Math.max(CONVERSATION_SCALE_CONTENT_GAP, width - CONVERSATION_SCALE_WIDTH - CONVERSATION_SCALE_RIGHT_GAP),
135
+ )
136
+ const centered = configuredLeft + CONVERSATION_SCALE_WIDTH + CONVERSATION_SCALE_CONTENT_GAP > content
137
+ const left = centered ? (content - CONVERSATION_SCALE_WIDTH) / 2 : configuredLeft
138
+ const previewRight = left
139
+ + CONVERSATION_SCALE_WIDTH
140
+ + CONVERSATION_SCALE_PREVIEW_GAP
141
+ + CONVERSATION_SCALE_PREVIEW_WIDTH
142
+ + CONVERSATION_SCALE_RIGHT_GAP
143
+ + recovery
144
+ if (previewRight > width) return { visible: false, reason: 'preview', left, centered }
145
+ return { visible: true, reason: 'ready', left, centered }
146
+ }
147
+
148
+
149
+ export function resolveConversationScaleCalibrationState({
150
+ operationWidth,
151
+ contentLeft,
152
+ maximumDistance = CONVERSATION_SCALE_DEFAULT_MAX_DISTANCE,
153
+ turnCount = 0,
154
+ drawerMode,
155
+ viewportWidth,
156
+ previouslyHidden = false,
157
+ } = {}) {
158
+ const numeric = Number(maximumDistance)
159
+ const normalizedMaximum = Number.isFinite(numeric)
160
+ ? clamp(Math.round(numeric / 8) * 8, 16, 240)
161
+ : CONVERSATION_SCALE_DEFAULT_MAX_DISTANCE
162
+ const resolution = drawerMode === 'overlay'
163
+ ? { visible: false, reason: 'drawer' }
164
+ : Number(viewportWidth) <= 640
165
+ ? { visible: false, reason: 'phone' }
166
+ : resolveConversationScaleGeometry({
167
+ operationWidth,
168
+ contentLeft,
169
+ maximumDistance: normalizedMaximum,
170
+ previouslyHidden,
171
+ })
172
+ if (!resolution.visible) return {
173
+ ...resolution,
174
+ mode: 'hidden',
175
+ maximumDistance: normalizedMaximum,
176
+ }
177
+ if (Number(turnCount) < MINIMUM_TURNS) return {
178
+ visible: false,
179
+ reason: 'turns',
180
+ mode: 'hidden',
181
+ maximumDistance: normalizedMaximum,
182
+ left: resolution.left,
183
+ centered: resolution.centered,
184
+ }
185
+ return {
186
+ ...resolution,
187
+ mode: resolution.centered ? 'centered' : 'configured',
188
+ maximumDistance: normalizedMaximum,
189
+ }
190
+ }
191
+
192
+ function visible(node) {
193
+ return node && !node.hidden && node.getAttribute('aria-hidden') !== 'true'
194
+ }
195
+
196
+ function follows(first, second) {
197
+ return Boolean(first?.compareDocumentPosition?.(second) & 4)
198
+ }
199
+
200
+ function truncateText(value, limit) {
201
+ const characters = Array.from(value)
202
+ return characters.length > limit ? `${characters.slice(0, limit).join('')}…` : value
203
+ }
204
+
205
+ function attachmentSummary(node) {
206
+ const named = node?.querySelector?.('[data-file-name], [download], img[alt]')
207
+ const name = named?.getAttribute('data-file-name')
208
+ || named?.getAttribute('download')
209
+ || named?.getAttribute('alt')
210
+ if (name?.trim()) return `${named?.matches('img') ? '图片' : '附件'} · ${name.trim()}`
211
+ if (node?.querySelector?.('img, [data-file], [data-attachment], input[type="file"]')) return '附件消息'
212
+ return ''
213
+ }
214
+
215
+ export function sanitizeConversationPreviewNode(node, limit = 360) {
216
+ if (!node) return ''
217
+ const clone = node.cloneNode(true)
218
+ for (const removable of clone.querySelectorAll?.(PREVIEW_REMOVE_SELECTOR) || []) removable.remove()
219
+ for (const block of clone.querySelectorAll?.('pre') || []) {
220
+ const firstLine = block.textContent?.split(/\r?\n/).map(line => line.trim()).find(Boolean) || ''
221
+ block.textContent = firstLine ? `代码:${firstLine}` : '代码'
222
+ }
223
+ const text = clone.textContent?.replace(/\s+/g, ' ').trim() || attachmentSummary(node)
224
+ return truncateText(text, limit)
225
+ }
226
+
227
+ export function collectConversationTurns(scroller) {
228
+ if (!scroller) return []
229
+ return Array.from(scroller.querySelectorAll(CONVERSATION_USER_TURN_SELECTOR)).filter(node => {
230
+ if (!visible(node) || node.closest('[data-prts-conversation-scale], [data-prts-conversation-preview]')) return false
231
+ const parent = node.parentElement?.closest?.(CONVERSATION_USER_TURN_SELECTOR)
232
+ return !parent || !scroller.contains(parent)
233
+ })
234
+ }
235
+
236
+ function hasClassSuffix(node, suffix) {
237
+ return [...(node?.classList ?? [])].some(token => token.endsWith(suffix))
238
+ }
239
+
240
+ export function findConversationOlderButton(root) {
241
+ for (const button of root?.querySelectorAll?.('button') ?? []) {
242
+ for (let current = button.parentElement; current && root.contains(current); current = current.parentElement) {
243
+ if (hasClassSuffix(current, '_older')) return button
244
+ }
245
+ }
246
+ return null
247
+ }
248
+
249
+ export function resolveConversationHistorySnapshot(sessions) {
250
+ try {
251
+ const listSnapshot = sessions?.list?.getSnapshot?.()
252
+ const current = listSnapshot?.current
253
+ ?? listSnapshot?.currentId
254
+ ?? listSnapshot?.currentSessionId
255
+ if (current === undefined || current === null) return {}
256
+ const binding = sessions?.binding?.(current)
257
+ const session = binding?.session
258
+ const snapshot = session?.getSnapshot?.() ?? binding?.getSnapshot?.()
259
+ return { key: current, hasMore: snapshot?.hasMore === true, loading: snapshot?.loadingOlder === true }
260
+ } catch {
261
+ return {}
262
+ }
263
+ }
264
+
265
+ function collectAssistantCandidates(scroller, turn, nextTurn) {
266
+ return Array.from(scroller?.querySelectorAll?.(ASSISTANT_SELECTOR) || []).filter(node => {
267
+ if (!visible(node)) return false
268
+ const parent = node.parentElement?.closest?.(ASSISTANT_SELECTOR)
269
+ if (parent && scroller.contains(parent)) return false
270
+ if (!follows(turn, node)) return false
271
+ return !nextTurn || follows(node, nextTurn)
272
+ })
273
+ }
274
+
275
+ function formalBodies(candidate) {
276
+ const bodies = []
277
+ if (candidate.matches?.(FORMAL_ANSWER_SELECTOR)) bodies.push(candidate)
278
+ bodies.push(...candidate.querySelectorAll?.(FORMAL_ANSWER_SELECTOR) || [])
279
+ return bodies.filter(body => !body.closest?.(AUXILIARY_SELECTOR))
280
+ }
281
+
282
+ export function getConversationTurnPreview(scroller, turns, index) {
283
+ const turn = turns?.[index]
284
+ if (!turn) return { question: '', answer: '尚无有效回复', state: 'empty', answerSource: undefined }
285
+ const nextTurn = turns[index + 1]
286
+ const questionSource = turn.querySelector?.('[class$="_bubble"], [data-message-role="user"]') || turn
287
+ const question = sanitizeConversationPreviewNode(questionSource, 180) || attachmentSummary(turn) || '附件消息'
288
+ const candidates = collectAssistantCandidates(scroller, turn, nextTurn)
289
+
290
+ for (const candidate of candidates) {
291
+ for (const body of formalBodies(candidate)) {
292
+ const answer = sanitizeConversationPreviewNode(body, 420)
293
+ if (answer) return { question, answer, state: 'answer', answerSource: body }
294
+ }
295
+ }
296
+
297
+ for (const candidate of candidates) {
298
+ const answer = sanitizeConversationPreviewNode(candidate, 420)
299
+ if (answer) return { question, answer, state: 'fallback', answerSource: candidate }
300
+ }
301
+
302
+ const streaming = candidates.some(candidate => candidate.matches?.(STREAMING_SELECTOR) || candidate.querySelector?.(STREAMING_SELECTOR))
303
+ return {
304
+ question,
305
+ answer: streaming ? '正在生成…' : '尚无有效回复',
306
+ state: streaming ? 'streaming' : 'empty',
307
+ answerSource: candidates[0],
308
+ }
309
+ }
310
+
311
+ export function getConversationScaleLayout(count, height, requestedOffset = 0) {
312
+ const safeCount = Math.max(0, Math.floor(Number(count) || 0))
313
+ const safeHeight = Math.max(1, Number(height) || 1)
314
+ const span = Math.max(0, safeCount - 1) * CONVERSATION_SCALE_SPACING
315
+ const usableHeight = Math.max(0, safeHeight - 1)
316
+ const maxOffset = Math.max(0, span - usableHeight)
317
+ const offset = clamp(Number(requestedOffset) || 0, 0, maxOffset)
318
+ const start = .5 + (maxOffset === 0 ? (usableHeight - span) / 2 : 0)
319
+ const positions = Array.from({ length: safeCount }, (_, index) =>
320
+ start + index * CONVERSATION_SCALE_SPACING - offset)
321
+ return { count: safeCount, height: safeHeight, span, maxOffset, offset, start, positions }
322
+ }
323
+
324
+ export function conversationScaleWaveLength(distance, lengths = CONVERSATION_SCALE_LENGTHS) {
325
+ const safeDistance = Math.abs(Number(distance) || 0)
326
+ const lower = Math.floor(safeDistance)
327
+ if (lower >= lengths.length - 1) return lengths.at(-1)
328
+ const progress = safeDistance - lower
329
+ const from = lengths[lower]
330
+ const to = lengths[lower + 1]
331
+ return from + (to - from) * progress
332
+ }
333
+
334
+ export function visibleConversationTickRange(layout, buffer = 2) {
335
+ if (!layout?.count) return { minimum: 0, maximum: -1 }
336
+ const padding = Math.max(0, Math.floor(Number(buffer) || 0))
337
+ const minimum = Math.max(
338
+ 0,
339
+ Math.ceil((layout.offset - layout.start) / CONVERSATION_SCALE_SPACING) - padding,
340
+ )
341
+ const maximum = Math.min(
342
+ layout.count - 1,
343
+ Math.floor((layout.height - .5 + layout.offset - layout.start) / CONVERSATION_SCALE_SPACING) + padding,
344
+ )
345
+ return { minimum, maximum }
346
+ }
347
+
348
+ export function nearestVisibleConversationTick(layout, localY) {
349
+ if (!layout?.count) return -1
350
+ const minimum = Math.max(0, Math.ceil((layout.offset - layout.start) / CONVERSATION_SCALE_SPACING))
351
+ const maximum = Math.min(
352
+ layout.count - 1,
353
+ Math.floor((layout.height - .5 + layout.offset - layout.start) / CONVERSATION_SCALE_SPACING),
354
+ )
355
+ if (maximum < minimum) return -1
356
+ const raw = Math.round((Number(localY) + layout.offset - layout.start) / CONVERSATION_SCALE_SPACING)
357
+ return clamp(raw, minimum, maximum)
358
+ }
359
+
360
+ export function snapConversationScaleOffset(offset, maximum) {
361
+ return clamp(
362
+ Math.round((Number(offset) || 0) / CONVERSATION_SCALE_SPACING) * CONVERSATION_SCALE_SPACING,
363
+ 0,
364
+ Math.max(0, Number(maximum) || 0),
365
+ )
366
+ }
367
+
368
+ export function shouldFollowConversationScaleBottom(offset, maximum) {
369
+ return Math.max(0, Number(maximum) || 0) - (Number(offset) || 0) <= 1
370
+ }
371
+
372
+ export function calculateConversationJumpTop({
373
+ scrollTop,
374
+ scrollHeight,
375
+ clientHeight,
376
+ turnTop,
377
+ visibleTop,
378
+ visibleHeight,
379
+ }) {
380
+ const target = (Number(scrollTop) || 0)
381
+ + (Number(turnTop) || 0)
382
+ - (Number(visibleTop) || 0)
383
+ - Math.max(0, Number(visibleHeight) || 0) * .28
384
+ return clamp(target, 0, Math.max(0, (Number(scrollHeight) || 0) - (Number(clientHeight) || 0)))
385
+ }
386
+
387
+ export function resolveConversationJumpBehavior(distance, viewportHeight, prefersReducedMotion = false) {
388
+ if (prefersReducedMotion) return 'auto'
389
+ const height = Math.max(0, Number(viewportHeight) || 0)
390
+ const delta = Math.abs(Number(distance) || 0)
391
+ return height > 0 && delta <= height * CONVERSATION_JUMP_SMOOTH_VIEWPORT_LIMIT ? 'smooth' : 'auto'
392
+ }
393
+
394
+ export function resolveConversationJumpSettle(distance, viewportHeight, prefersReducedMotion = false) {
395
+ if (prefersReducedMotion || resolveConversationJumpBehavior(distance, viewportHeight) === 'smooth') {
396
+ return { offset: 0, duration: 0 }
397
+ }
398
+ const height = Math.max(0, Number(viewportHeight) || 0)
399
+ const delta = Number(distance) || 0
400
+ if (height <= 0 || Math.abs(delta) <= 2) return { offset: 0, duration: 0 }
401
+ const offset = Math.sign(delta) * Math.min(
402
+ Math.abs(delta),
403
+ CONVERSATION_JUMP_SETTLE_MAX_DISTANCE,
404
+ height * CONVERSATION_JUMP_SETTLE_VIEWPORT_RATIO,
405
+ )
406
+ return { offset, duration: CONVERSATION_JUMP_SETTLE_DURATION }
407
+ }
408
+
409
+ function scrollConversationTo(scroller, top, behavior) {
410
+ if (typeof scroller?.scrollTo === 'function') scroller.scrollTo({ top, behavior })
411
+ else if (scroller) scroller.scrollTop = top
412
+ }
413
+
414
+ export async function performConversationJump({
415
+ scroller,
416
+ getTargetTop,
417
+ prefersReducedMotion = false,
418
+ requestFrame = callback => globalThis.requestAnimationFrame?.(callback) ?? globalThis.setTimeout?.(callback, 16),
419
+ now = () => globalThis.performance?.now?.() ?? Date.now(),
420
+ cancelled = () => false,
421
+ onFrame = () => {},
422
+ } = {}) {
423
+ if (!scroller || typeof getTargetTop !== 'function') return false
424
+ const nextFrame = () => new Promise(resolve => requestFrame(timestamp => {
425
+ resolve(Number.isFinite(timestamp) ? timestamp : now())
426
+ }))
427
+ const targetTop = () => {
428
+ const raw = getTargetTop()
429
+ if (raw === null || raw === undefined) return null
430
+ const value = Number(raw)
431
+ return Number.isFinite(value) ? value : null
432
+ }
433
+
434
+ let top = targetTop()
435
+ if (top === null) return false
436
+ const floor = Math.max(0, Number(scroller.scrollHeight) - Number(scroller.clientHeight))
437
+ const bottomDistance = floor - Number(scroller.scrollTop)
438
+ if (bottomDistance <= CONVERSATION_JUMP_BOTTOM_THRESHOLD && top < Number(scroller.scrollTop)) {
439
+ scroller.scrollTop = Math.max(0, Number(scroller.scrollTop) - 1)
440
+ await nextFrame()
441
+ if (cancelled()) return null
442
+ top = targetTop()
443
+ if (top === null) return false
444
+ }
445
+
446
+ const distance = top - Number(scroller.scrollTop)
447
+ const behavior = resolveConversationJumpBehavior(distance, scroller.clientHeight, prefersReducedMotion)
448
+ const settle = resolveConversationJumpSettle(distance, scroller.clientHeight, prefersReducedMotion)
449
+ if (settle.duration > 0) {
450
+ const limit = () => Math.max(0, Number(scroller.scrollHeight) - Number(scroller.clientHeight))
451
+ const beforeTarget = clamp(top - settle.offset, 0, limit())
452
+ scrollConversationTo(scroller, beforeTarget, 'auto')
453
+ await nextFrame()
454
+ if (cancelled()) return null
455
+ const from = Number(scroller.scrollTop)
456
+ const startedAt = now()
457
+ let progress = 0
458
+ while (progress < 1) {
459
+ const timestamp = await nextFrame()
460
+ if (cancelled()) return null
461
+ progress = Math.min(1, (timestamp - startedAt) / settle.duration)
462
+ const eased = 1 - (1 - progress) ** 3
463
+ const liveTarget = targetTop()
464
+ if (liveTarget === null) return false
465
+ scroller.scrollTop = from + (liveTarget - from) * eased
466
+ onFrame()
467
+ }
468
+ } else {
469
+ scrollConversationTo(scroller, top, behavior)
470
+ onFrame()
471
+ }
472
+
473
+ let stableFrames = 0
474
+ let verificationFrames = 0
475
+ const verificationStartedAt = now()
476
+ while (now() - verificationStartedAt < CONVERSATION_JUMP_VERIFY_DURATION && verificationFrames < 72) {
477
+ await nextFrame()
478
+ verificationFrames += 1
479
+ if (cancelled()) return null
480
+ const liveTarget = targetTop()
481
+ if (liveTarget === null) return false
482
+ stableFrames = Math.abs(Number(scroller.scrollTop) - liveTarget) <= 2 ? stableFrames + 1 : 0
483
+ if (stableFrames >= 2) return true
484
+ }
485
+
486
+ top = targetTop()
487
+ if (top === null) return false
488
+ scrollConversationTo(scroller, top, 'auto')
489
+ await nextFrame()
490
+ await nextFrame()
491
+ if (cancelled()) return null
492
+ const correctedTarget = targetTop()
493
+ return correctedTarget !== null && Math.abs(Number(scroller.scrollTop) - correctedTarget) <= 2
494
+ }
495
+
496
+ export function buildConversationScalePaths(layout, interactionIndex = -1, animation, lengths = CONVERSATION_SCALE_LENGTHS) {
497
+ const gray = []
498
+ const yellow = []
499
+ const history = []
500
+ const hovering = interactionIndex >= 0 && interactionIndex < layout.count
501
+ const animatedCenter = Number.isFinite(animation?.center) ? animation.center : interactionIndex
502
+ const animatedIntensity = animation
503
+ ? clamp(Number(animation.intensity) || 0, 0, 1)
504
+ : hovering ? 1 : 0
505
+ const restingLength = lengths.at(-1)
506
+ const { minimum, maximum } = visibleConversationTickRange(layout)
507
+ for (let index = minimum; index <= maximum; index += 1) {
508
+ const rawY = layout.start + index * CONVERSATION_SCALE_SPACING - layout.offset
509
+ if (rawY < .5 || rawY > layout.height - .5) continue
510
+ const y = Number(rawY.toFixed(2))
511
+ const waveLength = conversationScaleWaveLength(Math.abs(index - animatedCenter), lengths)
512
+ if (animation?.historyAvailable && index === 0) {
513
+ const selected = interactionIndex === 0
514
+ const intensity = selected ? animatedIntensity : 0
515
+ const restingFirstEnd = 2 + restingLength
516
+ const restingSecondStart = restingFirstEnd + 4
517
+ const restingSecondEnd = restingSecondStart + restingLength
518
+ const firstEnd = Number((
519
+ restingFirstEnd + (14 - restingFirstEnd) * intensity
520
+ ).toFixed(2))
521
+ const secondStart = Number((
522
+ restingSecondStart + (16 - restingSecondStart) * intensity
523
+ ).toFixed(2))
524
+ const secondEnd = Number((
525
+ restingSecondEnd + (30 - restingSecondEnd) * intensity
526
+ ).toFixed(2))
527
+ history.push(`M2 ${y}H${firstEnd}M${secondStart} ${y}H${secondEnd}`)
528
+ continue
529
+ }
530
+ const length = restingLength + (waveLength - restingLength) * animatedIntensity
531
+ const command = `M2 ${y}H${Number((2 + length).toFixed(2))}`
532
+ const accented = hovering ? index === interactionIndex : index === layout.count - 1
533
+ ;(accented ? yellow : gray).push(command)
534
+ }
535
+ return { gray: gray.join(''), yellow: yellow.join(''), history: history.join('') }
536
+ }
537
+
538
+ function createPreview(document, id) {
539
+ const preview = document.createElement('aside')
540
+ preview.id = id
541
+ preview.setAttribute('data-prts-conversation-preview', '')
542
+ preview.setAttribute('data-prts-owned-conversation-preview', '')
543
+ preview.setAttribute('role', 'tooltip')
544
+ preview.setAttribute('aria-hidden', 'true')
545
+ const connector = document.createElement('span')
546
+ connector.setAttribute('data-prts-conversation-preview-connector', '')
547
+ connector.setAttribute('aria-hidden', 'true')
548
+ const body = document.createElement('div')
549
+ body.setAttribute('data-prts-conversation-preview-body', '')
550
+ const question = document.createElement('div')
551
+ question.setAttribute('data-prts-conversation-preview-row', 'question')
552
+ const questionLabel = document.createElement('b')
553
+ questionLabel.textContent = 'Q'
554
+ const questionText = document.createElement('span')
555
+ questionText.setAttribute('data-prts-conversation-preview-question', '')
556
+ question.append(questionLabel, questionText)
557
+ const answer = document.createElement('div')
558
+ answer.setAttribute('data-prts-conversation-preview-row', 'answer')
559
+ const answerLabel = document.createElement('b')
560
+ answerLabel.textContent = 'A'
561
+ const answerText = document.createElement('span')
562
+ answerText.setAttribute('data-prts-conversation-preview-answer', '')
563
+ answer.append(answerLabel, answerText)
564
+ body.append(question, answer)
565
+ preview.append(connector, body)
566
+ preview._prtsBody = body
567
+ preview._prtsQuestion = questionText
568
+ preview._prtsAnswer = answerText
569
+ return preview
570
+ }
571
+
572
+ function createScale(document) {
573
+ const instance = ++scaleInstanceSeed
574
+ const maskId = `prts-conversation-scale-fade-${instance}`
575
+ const previewId = `prts-conversation-preview-${instance}`
576
+ const historyTooltipId = `prts-conversation-history-tooltip-${instance}`
577
+ const scale = document.createElement('div')
578
+ scale.setAttribute('data-prts-conversation-scale', '')
579
+ scale.setAttribute('data-prts-owned-conversation-scale', '')
580
+ scale.setAttribute('role', 'slider')
581
+ scale.setAttribute('aria-label', '会话刻度导航')
582
+ scale.setAttribute('aria-orientation', 'vertical')
583
+ scale.tabIndex = 0
584
+
585
+ const svg = document.createElementNS(SVG_NAMESPACE, 'svg')
586
+ svg.setAttribute('data-prts-conversation-scale-svg', '')
587
+ svg.setAttribute('aria-hidden', 'true')
588
+ svg.setAttribute('preserveAspectRatio', 'none')
589
+ const defs = document.createElementNS(SVG_NAMESPACE, 'defs')
590
+ const gradient = document.createElementNS(SVG_NAMESPACE, 'linearGradient')
591
+ gradient.id = `${maskId}-gradient`
592
+ gradient.setAttribute('gradientUnits', 'userSpaceOnUse')
593
+ gradient.setAttribute('x1', '0')
594
+ gradient.setAttribute('y1', '0')
595
+ gradient.setAttribute('x2', '0')
596
+ gradient.setAttribute('y2', '24')
597
+ const transparent = document.createElementNS(SVG_NAMESPACE, 'stop')
598
+ transparent.setAttribute('offset', '0')
599
+ transparent.setAttribute('stop-color', 'white')
600
+ transparent.setAttribute('stop-opacity', '0')
601
+ const opaque = document.createElementNS(SVG_NAMESPACE, 'stop')
602
+ opaque.setAttribute('offset', '1')
603
+ opaque.setAttribute('stop-color', 'white')
604
+ gradient.append(transparent, opaque)
605
+ const mask = document.createElementNS(SVG_NAMESPACE, 'mask')
606
+ mask.id = maskId
607
+ mask.setAttribute('maskUnits', 'userSpaceOnUse')
608
+ mask.setAttribute('x', '0')
609
+ mask.setAttribute('y', '0')
610
+ mask.setAttribute('width', String(SCALE_WIDTH))
611
+ const maskRect = document.createElementNS(SVG_NAMESPACE, 'rect')
612
+ maskRect.setAttribute('x', '0')
613
+ maskRect.setAttribute('y', '0')
614
+ maskRect.setAttribute('width', String(SCALE_WIDTH))
615
+ maskRect.setAttribute('fill', `url(#${gradient.id})`)
616
+ mask.append(maskRect)
617
+ defs.append(gradient, mask)
618
+ const marks = document.createElementNS(SVG_NAMESPACE, 'g')
619
+ marks.setAttribute('data-prts-conversation-scale-marks', '')
620
+ const gray = document.createElementNS(SVG_NAMESPACE, 'path')
621
+ gray.setAttribute('data-prts-conversation-scale-gray', '')
622
+ const yellow = document.createElementNS(SVG_NAMESPACE, 'path')
623
+ yellow.setAttribute('data-prts-conversation-scale-accent', '')
624
+ const history = document.createElementNS(SVG_NAMESPACE, 'path')
625
+ history.setAttribute('data-prts-conversation-scale-history', '')
626
+ marks.append(gray, yellow, history)
627
+ const hit = document.createElementNS(SVG_NAMESPACE, 'rect')
628
+ hit.setAttribute('data-prts-conversation-scale-hit', '')
629
+ hit.setAttribute('x', '0')
630
+ hit.setAttribute('y', '0')
631
+ hit.setAttribute('width', String(SCALE_WIDTH))
632
+ hit.setAttribute('fill', 'transparent')
633
+ svg.append(defs, marks, hit)
634
+ scale.append(svg)
635
+ scale._prtsSvg = svg
636
+ scale._prtsMarks = marks
637
+ scale._prtsMask = mask
638
+ scale._prtsMaskRect = maskRect
639
+ scale._prtsMaskId = maskId
640
+ scale._prtsGray = gray
641
+ scale._prtsAccent = yellow
642
+ scale._prtsHistory = history
643
+ scale._prtsHit = hit
644
+ scale._prtsPreview = createPreview(document, previewId)
645
+ const historyStatus = document.createElement('div')
646
+ historyStatus.setAttribute('data-prts-conversation-history-status', '')
647
+ historyStatus.setAttribute('role', 'status')
648
+ historyStatus.setAttribute('aria-live', 'polite')
649
+ historyStatus.hidden = true
650
+ const historyText = document.createElement('span')
651
+ historyText.setAttribute('data-prts-conversation-history-text', '')
652
+ const historyAction = document.createElement('button')
653
+ historyAction.type = 'button'
654
+ historyAction.setAttribute('data-prts-conversation-history-action', '')
655
+ historyStatus.append(historyText, historyAction)
656
+ const historyHint = document.createElement('div')
657
+ historyHint.setAttribute('data-prts-conversation-history-hint', '')
658
+ historyHint.setAttribute('data-prts-owned-conversation-scale', '')
659
+ historyHint.hidden = true
660
+ const historyTrigger = document.createElement('button')
661
+ historyTrigger.type = 'button'
662
+ historyTrigger.tabIndex = -1
663
+ historyTrigger.setAttribute('data-prts-conversation-history-trigger', '')
664
+ historyTrigger.setAttribute('aria-label', '载入更早内容并前往会话开头')
665
+ const historyTooltip = document.createElement('div')
666
+ historyTooltip.id = historyTooltipId
667
+ historyTooltip.setAttribute('data-prts-conversation-history-tooltip', '')
668
+ historyTooltip.setAttribute('role', 'tooltip')
669
+ historyTooltip.setAttribute('aria-hidden', 'true')
670
+ historyTooltip.hidden = true
671
+ const historyTooltipConnector = document.createElement('span')
672
+ historyTooltipConnector.setAttribute('data-prts-conversation-history-tooltip-connector', '')
673
+ historyTooltipConnector.setAttribute('aria-hidden', 'true')
674
+ const historyTooltipBody = document.createElement('div')
675
+ historyTooltipBody.setAttribute('data-prts-conversation-preview-body', '')
676
+ historyTooltipBody.setAttribute('data-prts-conversation-history-tooltip-body', '')
677
+ const historyTooltipHeader = document.createElement('div')
678
+ historyTooltipHeader.setAttribute('data-prts-conversation-preview-row', 'question')
679
+ const historyTooltipHeaderLabel = document.createElement('b')
680
+ historyTooltipHeaderLabel.textContent = 'H'
681
+ const historyTooltipHeaderText = document.createElement('span')
682
+ historyTooltipHeaderText.setAttribute('data-prts-conversation-history-tooltip-label', '')
683
+ historyTooltipHeaderText.textContent = '历史断点'
684
+ historyTooltipHeader.append(historyTooltipHeaderLabel, historyTooltipHeaderText)
685
+ const historyTooltipDetail = document.createElement('div')
686
+ historyTooltipDetail.setAttribute('data-prts-conversation-preview-row', 'answer')
687
+ const historyTooltipDetailLabel = document.createElement('b')
688
+ historyTooltipDetailLabel.textContent = '↥'
689
+ const historyTooltipContent = document.createElement('div')
690
+ historyTooltipContent.setAttribute('data-prts-conversation-history-tooltip-content', '')
691
+ const historyTooltipTitle = document.createElement('strong')
692
+ historyTooltipTitle.textContent = '更早内容尚未加载'
693
+ const historyTooltipDescription = document.createElement('span')
694
+ historyTooltipDescription.textContent = '点击断点,载入历史并前往会话开头'
695
+ historyTooltipContent.append(historyTooltipTitle, historyTooltipDescription)
696
+ historyTooltipDetail.append(historyTooltipDetailLabel, historyTooltipContent)
697
+ historyTooltipBody.append(historyTooltipHeader, historyTooltipDetail)
698
+ historyTooltip.append(historyTooltipConnector, historyTooltipBody)
699
+ historyHint.append(historyTrigger, historyTooltip)
700
+ scale._prtsHistoryStatus = historyStatus
701
+ scale._prtsHistoryText = historyText
702
+ scale._prtsHistoryAction = historyAction
703
+ scale._prtsHistoryHint = historyHint
704
+ scale._prtsHistoryTrigger = historyTrigger
705
+ scale._prtsHistoryTooltip = historyTooltip
706
+ return scale
707
+ }
708
+
709
+ export function createConversationScaleAdapter({ document, window, sessions }) {
710
+ let operation
711
+ let scroller
712
+ let composer
713
+ let scale
714
+ let preview
715
+ let historyStatus
716
+ let historyHint
717
+ let mutationObserver
718
+ let resizeObserver
719
+ let previewObserver
720
+ let previewSource
721
+ let frame
722
+ let waveFrame
723
+ let snapTimer
724
+ let touchTimer
725
+ let previewDelayTimer
726
+ let previewHideTimer
727
+ let previewRefreshTimer
728
+ let turns = []
729
+ let resizeSettleTimer
730
+ let resizeActive = false
731
+ let layout
732
+ let geometry
733
+ let scaleOffset = 0
734
+ let previousMaximum = 0
735
+ let followBottom = true
736
+ let cursorIndex = 0
737
+ let hoverIndex = -1
738
+ let previewIndex = -1
739
+ let pendingPreviewIndex = -1
740
+ let keyboardActive = false
741
+ let pointerInside = false
742
+ let pointerStart
743
+ let lastDirectJump = 0
744
+ let jumpRequest = 0
745
+ let historyRequest = 0
746
+ let historyMutationRevision = 0
747
+ let historyAvailable = false
748
+ let historyPhase = 'idle'
749
+ let historyPages = 0
750
+ let historyMessage = ''
751
+ let historySessionKey
752
+ let historyHintHovered = false
753
+ let scanPending = false
754
+ let fullScanPending = false
755
+ let pendingMutations = []
756
+ let obstructionDirty = true
757
+ const overlayCandidates = new Set()
758
+ let overlayScanPending = false
759
+ let scaleObstructed = false
760
+ let scaleBlockingObstructed = false
761
+ let scaleSpaceObstructed = true
762
+ let maximumDistance = CONVERSATION_SCALE_DEFAULT_MAX_DISTANCE
763
+ let focusContrast = CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT
764
+ let scaleLengths = resolveConversationScaleLengths(focusContrast)
765
+ let contentLeft
766
+ let geometryDirty = true
767
+ let observedOperationWidth
768
+ let observedOperationHeight
769
+ let started = false
770
+ let failed = false
771
+ let waveCenter = 0
772
+ let waveIntensity = 0
773
+ let waveStartCenter = 0
774
+ let waveStartIntensity = 0
775
+ let waveTargetCenter = 0
776
+ let waveTargetIntensity = 0
777
+ let waveStartedAt = 0
778
+
779
+ const requestFrame = callback => window?.requestAnimationFrame?.(callback) ?? window?.setTimeout?.(callback, 0)
780
+ const cancelFrame = id => {
781
+ if (id === undefined) return
782
+ if (window?.cancelAnimationFrame) window.cancelAnimationFrame(id)
783
+ else window?.clearTimeout?.(id)
784
+ }
785
+ const currentTime = timestamp => Number.isFinite(timestamp)
786
+ ? timestamp
787
+ : window?.performance?.now?.() ?? Date.now()
788
+
789
+ function reducedMotion() {
790
+ return window?.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches
791
+ || document.documentElement.dataset.prtsMotion === 'reduced'
792
+ }
793
+
794
+ function sampleWave(timestamp = currentTime()) {
795
+ if (waveFrame === undefined) return
796
+ const progress = clamp((currentTime(timestamp) - waveStartedAt) / CONVERSATION_SCALE_WAVE_DURATION, 0, 1)
797
+ const eased = 1 - (1 - progress) ** 3
798
+ waveCenter = waveStartCenter + (waveTargetCenter - waveStartCenter) * eased
799
+ waveIntensity = waveStartIntensity + (waveTargetIntensity - waveStartIntensity) * eased
800
+ }
801
+
802
+ function animateWave(timestamp) {
803
+ waveFrame = undefined
804
+ if (!started || failed) return
805
+ const time = currentTime(timestamp)
806
+ const progress = clamp((time - waveStartedAt) / CONVERSATION_SCALE_WAVE_DURATION, 0, 1)
807
+ const eased = 1 - (1 - progress) ** 3
808
+ waveCenter = waveStartCenter + (waveTargetCenter - waveStartCenter) * eased
809
+ waveIntensity = waveStartIntensity + (waveTargetIntensity - waveStartIntensity) * eased
810
+ schedule()
811
+ if (progress < 1) waveFrame = requestFrame(animateWave)
812
+ }
813
+
814
+ function retargetWave(index) {
815
+ const time = currentTime()
816
+ sampleWave(time)
817
+ cancelFrame(waveFrame)
818
+ waveFrame = undefined
819
+ waveStartCenter = waveCenter
820
+ waveStartIntensity = waveIntensity
821
+ waveTargetCenter = index >= 0 ? index : waveCenter
822
+ waveTargetIntensity = index >= 0 ? 1 : 0
823
+ waveStartedAt = time
824
+ if (reducedMotion()) {
825
+ waveCenter = waveTargetCenter
826
+ waveIntensity = waveTargetIntensity
827
+ schedule()
828
+ return
829
+ }
830
+ waveFrame = requestFrame(animateWave)
831
+ }
832
+
833
+ function resetWave() {
834
+ cancelFrame(waveFrame)
835
+ waveFrame = undefined
836
+ waveCenter = 0
837
+ waveIntensity = 0
838
+ waveStartCenter = 0
839
+ waveStartIntensity = 0
840
+ waveTargetCenter = 0
841
+ waveTargetIntensity = 0
842
+ waveStartedAt = 0
843
+ }
844
+
845
+ function cancelJump() {
846
+ jumpRequest += 1
847
+ }
848
+
849
+ function historyPrefix() {
850
+ return historyAvailable ? 1 : 0
851
+ }
852
+
853
+ function scaleItemCount() {
854
+ return turns.length + historyPrefix()
855
+ }
856
+
857
+ function scaleToTurnIndex(index) {
858
+ return index - historyPrefix()
859
+ }
860
+
861
+ function turnToScaleIndex(index) {
862
+ return index + historyPrefix()
863
+ }
864
+
865
+ function syncHistoryStatus() {
866
+ if (!historyStatus || !scale) return
867
+ const visible = historyPhase === 'loading' || historyPhase === 'paused' || historyPhase === 'error'
868
+ historyStatus.hidden = !visible
869
+ historyStatus.dataset.prtsHistoryPhase = historyPhase
870
+ scale.dataset.prtsHistoryPhase = historyPhase
871
+ scale.toggleAttribute('data-prts-conversation-history-available', historyAvailable)
872
+ if (!visible) {
873
+ syncHistoryHint()
874
+ return
875
+ }
876
+ const page = historyPhase === 'loading' ? historyPages + 1 : historyPages
877
+ const fallback = historyPhase === 'loading'
878
+ ? `正在载入更早内容 · 第 ${page} 页`
879
+ : historyPhase === 'error'
880
+ ? historyMessage || '更早内容载入失败'
881
+ : historyPages > 0
882
+ ? `已载入 ${historyPages} 页 · 尚未到达开头`
883
+ : '已载入部分内容 · 尚未到达开头'
884
+ scale._prtsHistoryText.textContent = fallback
885
+ scale._prtsHistoryAction.textContent = historyPhase === 'loading' ? '取消' : '继续'
886
+ scale._prtsHistoryAction.setAttribute(
887
+ 'aria-label',
888
+ historyPhase === 'loading' ? '取消前往会话开头' : '继续前往会话开头',
889
+ )
890
+ syncHistoryHint()
891
+ }
892
+
893
+ function syncHistoryHint() {
894
+ if (!historyHint || !scale) return
895
+ const railUnavailable = scaleSpaceObstructed || scaleBlockingObstructed || scaleObstructed
896
+ || scaleItemCount() < MINIMUM_TURNS
897
+ const visible = historyAvailable && historyPhase === 'idle' && !railUnavailable
898
+ if (!visible) historyHintHovered = false
899
+ const selected = visible && (
900
+ historyHintHovered
901
+ || (pointerInside && hoverIndex === 0)
902
+ )
903
+ historyHint.hidden = !visible
904
+ historyHint.toggleAttribute('data-prts-history-selected', selected)
905
+ scale.toggleAttribute('data-prts-conversation-history-selected', selected)
906
+ const tooltip = scale._prtsHistoryTooltip
907
+ tooltip.hidden = !selected
908
+ tooltip.setAttribute('aria-hidden', String(!selected))
909
+ const tooltipId = tooltip.id
910
+ for (const target of [scale, scale._prtsHistoryTrigger]) {
911
+ if (selected) target.setAttribute('aria-describedby', tooltipId)
912
+ else if (target.getAttribute('aria-describedby') === tooltipId) target.removeAttribute('aria-describedby')
913
+ }
914
+ }
915
+
916
+ function selectHistoryHint({ keyboard = false } = {}) {
917
+ if (!historyAvailable || historyPhase !== 'idle') return
918
+ keyboardActive = keyboard
919
+ hoverIndex = 0
920
+ cursorIndex = 0
921
+ ensureVisible(0)
922
+ retargetWave(0)
923
+ syncAccessibility()
924
+ schedule()
925
+ }
926
+
927
+ function setHistoryAvailable(next) {
928
+ const available = Boolean(next)
929
+ if (available === historyAvailable) return
930
+ const previousPrefix = historyPrefix()
931
+ historyAvailable = available
932
+ const delta = historyPrefix() - previousPrefix
933
+ if (cursorIndex >= previousPrefix) cursorIndex = Math.max(0, cursorIndex + delta)
934
+ if (hoverIndex >= previousPrefix) hoverIndex = Math.max(0, hoverIndex + delta)
935
+ if (!followBottom && scaleOffset > 0) scaleOffset = Math.max(0, scaleOffset + delta * CONVERSATION_SCALE_SPACING)
936
+ geometryDirty = true
937
+ syncHistoryStatus()
938
+ }
939
+
940
+ function readHistoryState() {
941
+ const snapshot = resolveConversationHistorySnapshot(sessions)
942
+ const olderButton = findConversationOlderButton(operation)
943
+ if (snapshot.key !== undefined && snapshot.key !== historySessionKey) {
944
+ historyRequest += 1
945
+ historySessionKey = snapshot.key
946
+ historyPages = 0
947
+ historyPhase = 'idle'
948
+ historyMessage = ''
949
+ }
950
+ const available = snapshot.hasMore || Boolean(olderButton) || historyPhase === 'loading'
951
+ setHistoryAvailable(available)
952
+ if (!available && historyPhase !== 'loading') {
953
+ historyPhase = 'idle'
954
+ historyMessage = ''
955
+ syncHistoryStatus()
956
+ }
957
+ return { ...snapshot, olderButton }
958
+ }
959
+
960
+ function cancelHistory({ paused = true } = {}) {
961
+ if (historyPhase !== 'loading') return
962
+ historyRequest += 1
963
+ historyPhase = paused && historyAvailable ? 'paused' : 'idle'
964
+ historyMessage = ''
965
+ syncHistoryStatus()
966
+ schedule()
967
+ }
968
+
969
+ function cancelNavigation() {
970
+ cancelJump()
971
+ cancelHistory()
972
+ }
973
+
974
+ function onHistoryAction(event) {
975
+ event.preventDefault()
976
+ event.stopPropagation()
977
+ if (historyPhase === 'loading') cancelHistory()
978
+ else void loadHistoryToStart()
979
+ }
980
+
981
+ function onHistoryHintPointerEnter() {
982
+ historyHintHovered = true
983
+ selectHistoryHint()
984
+ }
985
+
986
+ function onHistoryHintPointerLeave() {
987
+ historyHintHovered = false
988
+ if (!pointerInside) {
989
+ hoverIndex = -1
990
+ retargetWave(-1)
991
+ }
992
+ schedule()
993
+ }
994
+
995
+ function schedule(scan = false) {
996
+ if (scan) {
997
+ scanPending = true
998
+ fullScanPending = true
999
+ }
1000
+ if (resizeActive) return
1001
+ if (frame !== undefined || failed) return
1002
+ frame = requestFrame(flush)
1003
+ }
1004
+
1005
+ function clearTimers() {
1006
+ for (const timer of [snapTimer, touchTimer, previewDelayTimer, previewHideTimer, previewRefreshTimer, resizeSettleTimer]) {
1007
+ if (timer !== undefined) window?.clearTimeout?.(timer)
1008
+ }
1009
+ snapTimer = undefined
1010
+ touchTimer = undefined
1011
+ previewDelayTimer = undefined
1012
+ previewHideTimer = undefined
1013
+ previewRefreshTimer = undefined
1014
+ resizeSettleTimer = undefined
1015
+ }
1016
+
1017
+ function setScaleResizeState(active) {
1018
+ resizeActive = Boolean(active)
1019
+ for (const node of [scale, preview, historyHint, historyStatus, composer]) {
1020
+ if (!node || node.hasAttribute('data-prts-resizing') === resizeActive) continue
1021
+ node.toggleAttribute('data-prts-resizing', resizeActive)
1022
+ }
1023
+ }
1024
+
1025
+ function disconnectPreviewObserver() {
1026
+ resizeSettleTimer = undefined
1027
+ previewObserver?.disconnect()
1028
+ previewObserver = undefined
1029
+ previewSource = undefined
1030
+ }
1031
+
1032
+ function observePreviewSource(source) {
1033
+ if (source === previewSource) return
1034
+ disconnectPreviewObserver()
1035
+ const MutationObserver = window?.MutationObserver
1036
+ if (!source || typeof MutationObserver !== 'function') return
1037
+ previewSource = source
1038
+ previewObserver = new MutationObserver(queuePreviewRefresh)
1039
+ previewObserver.observe(source, {
1040
+ subtree: true,
1041
+ childList: true,
1042
+ characterData: true,
1043
+ attributes: true,
1044
+ attributeFilter: ['hidden', 'aria-hidden'],
1045
+ })
1046
+ }
1047
+
1048
+ function resolveRoots() {
1049
+ const nextOperation = document?.querySelector?.('[data-prts-region="operation"]')
1050
+ const nextScroller = nextOperation?.querySelector?.('[data-conversation-scroll]')
1051
+ if (!nextOperation || !nextScroller) {
1052
+ if (operation || scroller || scale || preview) disconnectRoots()
1053
+ return false
1054
+ }
1055
+ const nextComposer = nextScroller.querySelector('[data-composer-seat], [data-slot="conversation.composer"]')
1056
+ if (operation === nextOperation && scroller === nextScroller && scale?.isConnected && preview?.isConnected
1057
+ && historyHint?.isConnected && historyStatus?.isConnected) {
1058
+ if (composer !== nextComposer) {
1059
+ if (composer) resizeObserver?.unobserve?.(composer)
1060
+ composer = nextComposer
1061
+ if (composer) resizeObserver?.observe(composer)
1062
+ geometryDirty = true
1063
+ }
1064
+ return true
1065
+ }
1066
+
1067
+ disconnectRoots()
1068
+ operation = nextOperation
1069
+ scroller = nextScroller
1070
+ composer = nextComposer
1071
+ scale = createScale(document)
1072
+ preview = scale._prtsPreview
1073
+ historyHint = scale._prtsHistoryHint
1074
+ historyStatus = scale._prtsHistoryStatus
1075
+ operation.append(scale, preview, historyHint, historyStatus)
1076
+ scale._prtsHistoryTrigger.addEventListener('click', onHistoryAction)
1077
+ scale._prtsHistoryTrigger.addEventListener('pointerenter', onHistoryHintPointerEnter)
1078
+ scale._prtsHistoryTrigger.addEventListener('pointerleave', onHistoryHintPointerLeave)
1079
+ scale._prtsHistoryAction.addEventListener('click', onHistoryAction)
1080
+ scroller.setAttribute('data-prts-conversation-scale-ready', '')
1081
+ scale.addEventListener('pointermove', onPointerMove)
1082
+ scale.addEventListener('pointerleave', onPointerLeave)
1083
+ scale.addEventListener('pointerdown', onPointerDown)
1084
+ scale.addEventListener('pointerup', onPointerUp)
1085
+ scale.addEventListener('pointercancel', onPointerCancel)
1086
+ scale.addEventListener('click', onClick)
1087
+ scale.addEventListener('wheel', onWheel, { passive: false })
1088
+ scale.addEventListener('keydown', onKeydown)
1089
+ scale.addEventListener('focus', onFocus)
1090
+ scale.addEventListener('blur', onBlur)
1091
+ scroller.addEventListener('wheel', cancelNavigation, { passive: true })
1092
+ scroller.addEventListener('touchstart', cancelNavigation, { passive: true })
1093
+ scroller.addEventListener('pointerdown', cancelNavigation, { passive: true })
1094
+ resizeObserver?.observe(operation)
1095
+ resizeObserver?.observe(scroller)
1096
+ if (composer) resizeObserver?.observe(composer)
1097
+ turns = []
1098
+ scaleOffset = 0
1099
+ previousMaximum = 0
1100
+ followBottom = true
1101
+ obstructionDirty = true
1102
+ cursorIndex = 0
1103
+ hoverIndex = -1
1104
+ previewIndex = -1
1105
+ pendingPreviewIndex = -1
1106
+ contentLeft = undefined
1107
+ geometryDirty = true
1108
+ scaleSpaceObstructed = true
1109
+ overlayScanPending = true
1110
+ historyRequest += 1
1111
+ historyAvailable = false
1112
+ historyPhase = 'idle'
1113
+ historyPages = 0
1114
+ historyMessage = ''
1115
+ historySessionKey = undefined
1116
+ historyHintHovered = false
1117
+ setScaleResizeState(resizeActive)
1118
+ cancelJump()
1119
+ resetWave()
1120
+ return true
1121
+ }
1122
+
1123
+ function disconnectRoots() {
1124
+ clearTimers()
1125
+ hidePreview()
1126
+ disconnectPreviewObserver()
1127
+ if (scroller) {
1128
+ scroller.removeAttribute('data-prts-conversation-scale-ready')
1129
+ scroller.removeAttribute('data-prts-conversation-scale-empty')
1130
+ scroller.removeEventListener('wheel', cancelNavigation)
1131
+ scroller.removeEventListener('touchstart', cancelNavigation)
1132
+ scroller.removeEventListener('pointerdown', cancelNavigation)
1133
+ }
1134
+ if (scale) {
1135
+ scale.removeEventListener('pointermove', onPointerMove)
1136
+ scale.removeEventListener('pointerleave', onPointerLeave)
1137
+ scale.removeEventListener('pointerdown', onPointerDown)
1138
+ scale.removeEventListener('pointerup', onPointerUp)
1139
+ scale.removeEventListener('pointercancel', onPointerCancel)
1140
+ scale.removeEventListener('click', onClick)
1141
+ scale.removeEventListener('wheel', onWheel)
1142
+ scale.removeEventListener('keydown', onKeydown)
1143
+ scale.removeEventListener('focus', onFocus)
1144
+ scale.removeEventListener('blur', onBlur)
1145
+ scale._prtsHistoryTrigger?.removeEventListener('click', onHistoryAction)
1146
+ scale._prtsHistoryTrigger?.removeEventListener('pointerenter', onHistoryHintPointerEnter)
1147
+ scale._prtsHistoryTrigger?.removeEventListener('pointerleave', onHistoryHintPointerLeave)
1148
+ scale._prtsHistoryAction?.removeEventListener('click', onHistoryAction)
1149
+ scale.remove()
1150
+ }
1151
+ preview?.remove()
1152
+ historyHint?.remove()
1153
+ historyStatus?.remove()
1154
+ resizeObserver?.disconnect()
1155
+ historyRequest += 1
1156
+ operation = undefined
1157
+ scroller = undefined
1158
+ composer = undefined
1159
+ scale = undefined
1160
+ preview = undefined
1161
+ historyHint = undefined
1162
+ historyStatus = undefined
1163
+ turns = []
1164
+ layout = undefined
1165
+ geometry = undefined
1166
+ hoverIndex = -1
1167
+ previewIndex = -1
1168
+ keyboardActive = false
1169
+ pointerInside = false
1170
+ pointerStart = undefined
1171
+ historyHintHovered = false
1172
+ resizeActive = false
1173
+ cancelJump()
1174
+ resetWave()
1175
+ overlayCandidates.clear()
1176
+ obstructionDirty = true
1177
+ overlayScanPending = false
1178
+ scaleObstructed = false
1179
+ scaleBlockingObstructed = false
1180
+ scaleSpaceObstructed = true
1181
+ contentLeft = undefined
1182
+ observedOperationWidth = undefined
1183
+ observedOperationHeight = undefined
1184
+ geometryDirty = true
1185
+ pendingMutations = []
1186
+ fullScanPending = false
1187
+ }
1188
+
1189
+ function isTopLevelTurn(node) {
1190
+ if (!node?.matches?.(CONVERSATION_USER_TURN_SELECTOR) || !scroller?.contains(node) || !visible(node)) return false
1191
+ if (node.closest('[data-prts-conversation-scale], [data-prts-conversation-preview]')) return false
1192
+ const parent = node.parentElement?.closest?.(CONVERSATION_USER_TURN_SELECTOR)
1193
+ return !parent || !scroller.contains(parent)
1194
+ }
1195
+
1196
+ function collectTurnsFromNode(node, target) {
1197
+ if (node?.nodeType !== 1) return
1198
+ if (node.matches?.(CONVERSATION_USER_TURN_SELECTOR)) target.add(node)
1199
+ for (const turn of node.querySelectorAll?.(CONVERSATION_USER_TURN_SELECTOR) ?? []) target.add(turn)
1200
+ }
1201
+
1202
+ function collectOverlayCandidates(node) {
1203
+ if (node?.nodeType !== 1) return
1204
+ if (node.matches?.(CONVERSATION_OVERLAY_SELECTOR)) overlayCandidates.add(node)
1205
+ for (const candidate of node.querySelectorAll?.(CONVERSATION_OVERLAY_SELECTOR) ?? []) {
1206
+ overlayCandidates.add(candidate)
1207
+ }
1208
+ }
1209
+
1210
+ function mutationNodeAffectsScale(node) {
1211
+ if (node?.nodeType !== 1) return false
1212
+ if (node.matches?.(CONVERSATION_OVERLAY_SELECTOR)
1213
+ || node.matches?.(CONVERSATION_PORTAL_ROOT_SELECTOR)
1214
+ || node.matches?.('[data-prts-region="operation"], [data-slot="conversation"]')) return true
1215
+ return Boolean(node.querySelector?.([
1216
+ CONVERSATION_OVERLAY_SELECTOR,
1217
+ CONVERSATION_PORTAL_ROOT_SELECTOR,
1218
+ '[data-prts-region="operation"]',
1219
+ '[data-slot="conversation"]',
1220
+ ].join(', ')))
1221
+ }
1222
+
1223
+ function mutationAffectsTurns(mutation) {
1224
+ if (mutation.type === 'attributes') {
1225
+ if (mutation.attributeName === 'data-chat-flow-kind'
1226
+ || mutation.attributeName === 'data-message-role') return true
1227
+ if (mutation.target?.matches?.(CONVERSATION_USER_TURN_SELECTOR)
1228
+ || mutation.target?.querySelector?.(CONVERSATION_USER_TURN_SELECTOR)) return true
1229
+ }
1230
+ for (const node of [...(mutation.addedNodes ?? []), ...(mutation.removedNodes ?? [])]) {
1231
+ if (node?.nodeType !== 1) continue
1232
+ if (node.matches?.(CONVERSATION_USER_TURN_SELECTOR)
1233
+ || node.querySelector?.(CONVERSATION_USER_TURN_SELECTOR)) return true
1234
+ }
1235
+ return false
1236
+ }
1237
+
1238
+ function mutationAffectsOverlay(mutation) {
1239
+ if (overlayCandidates.has(mutation.target)) return true
1240
+ if (mutationNodeAffectsScale(mutation.target)) return true
1241
+ for (const node of mutation.addedNodes ?? []) if (mutationNodeAffectsScale(node)) return true
1242
+ for (const node of mutation.removedNodes ?? []) {
1243
+ if (mutationNodeAffectsScale(node)) return true
1244
+ if ([...overlayCandidates].some(candidate => candidate === node || node?.contains?.(candidate))) return true
1245
+ }
1246
+ return false
1247
+ }
1248
+
1249
+ function collectExternalOverlayCandidates() {
1250
+ for (const child of document?.body?.children ?? []) {
1251
+ if (child === operation || child.contains?.(operation)) continue
1252
+ collectOverlayCandidates(child)
1253
+ }
1254
+ }
1255
+
1256
+ function updateOverlayCandidates(mutations, full = false) {
1257
+ if (full || overlayScanPending) {
1258
+ overlayCandidates.clear()
1259
+ collectOverlayCandidates(operation)
1260
+ collectExternalOverlayCandidates()
1261
+ overlayScanPending = false
1262
+ } else {
1263
+ for (const mutation of mutations) {
1264
+ if (mutation.type === 'attributes') {
1265
+ if (mutation.target?.matches?.(CONVERSATION_OVERLAY_SELECTOR)) overlayCandidates.add(mutation.target)
1266
+ else overlayCandidates.delete(mutation.target)
1267
+ }
1268
+ for (const node of mutation.addedNodes ?? []) collectOverlayCandidates(node)
1269
+ }
1270
+ }
1271
+ for (const candidate of [...overlayCandidates]) {
1272
+ if (!candidate.isConnected) overlayCandidates.delete(candidate)
1273
+ }
1274
+ }
1275
+
1276
+ function applyTurns(nextTurns) {
1277
+ const wasAtBottom = shouldFollowConversationScaleBottom(scaleOffset, previousMaximum)
1278
+ const previousList = turns
1279
+ const previousTurns = new Set(previousList)
1280
+ const previousCount = turns.length
1281
+ const previousPrefix = historyPrefix()
1282
+ const turnAtScaleIndex = index => previousList[index - previousPrefix]
1283
+ const cursorTurn = turnAtScaleIndex(cursorIndex)
1284
+ const hoverTurn = turnAtScaleIndex(hoverIndex)
1285
+ const cursorWasHistory = historyAvailable && cursorIndex === 0
1286
+ const hoverWasHistory = historyAvailable && hoverIndex === 0
1287
+ const previewTurn = previousList[previewIndex]
1288
+ const pendingPreviewTurn = previousList[pendingPreviewIndex]
1289
+ const previewWasVisible = previewIndex >= 0
1290
+ const anchorScaleIndex = layout
1291
+ ? nearestVisibleConversationTick(layout, .5)
1292
+ : -1
1293
+ const anchorTurn = turnAtScaleIndex(anchorScaleIndex)
1294
+ const anchorY = layout && anchorScaleIndex >= 0
1295
+ ? layout.start + anchorScaleIndex * CONVERSATION_SCALE_SPACING - scaleOffset
1296
+ : undefined
1297
+ const replaced = previousCount > 0 && nextTurns.length > 0 && !nextTurns.some(turn => previousTurns.has(turn))
1298
+ turns = nextTurns
1299
+ if (replaced || turns.length !== previousCount) geometryDirty = true
1300
+ if (replaced || turns.length > previousCount) cancelJump()
1301
+ followBottom = replaced || previousCount === 0 || (turns.length > previousCount && wasAtBottom)
1302
+ const nextScaleIndex = node => {
1303
+ const index = node ? turns.indexOf(node) : -1
1304
+ return index < 0 ? -1 : turnToScaleIndex(index)
1305
+ }
1306
+ cursorIndex = cursorWasHistory ? 0 : nextScaleIndex(cursorTurn)
1307
+ if (cursorIndex < 0) cursorIndex = Math.max(0, scaleItemCount() - 1)
1308
+ hoverIndex = hoverWasHistory ? 0 : nextScaleIndex(hoverTurn)
1309
+ previewIndex = previewTurn ? turns.indexOf(previewTurn) : -1
1310
+ pendingPreviewIndex = pendingPreviewTurn ? turns.indexOf(pendingPreviewTurn) : -1
1311
+ if (previewWasVisible && previewIndex < 0) hidePreview()
1312
+ if (!followBottom && anchorTurn && Number.isFinite(anchorY)) {
1313
+ const nextAnchor = nextScaleIndex(anchorTurn)
1314
+ if (nextAnchor >= 0) {
1315
+ scaleOffset = Math.max(0, nextAnchor * CONVERSATION_SCALE_SPACING + (layout?.start ?? .5) - anchorY)
1316
+ }
1317
+ }
1318
+ if ((previousCount === 0 || replaced) && turns.length) cursorIndex = scaleItemCount() - 1
1319
+ const empty = scaleItemCount() < MINIMUM_TURNS
1320
+ scale.dataset.prtsScaleCount = String(turns.length)
1321
+ scale.toggleAttribute('data-prts-conversation-scale-partial', historyAvailable)
1322
+ scale.toggleAttribute('data-prts-conversation-scale-empty', empty)
1323
+ scroller.toggleAttribute('data-prts-conversation-scale-empty', empty)
1324
+ syncAccessibility()
1325
+ }
1326
+
1327
+ function syncTurns(mutations = [], full = false) {
1328
+ updateOverlayCandidates(mutations, full)
1329
+ const conversationMutations = mutations.filter(mutation => operation?.contains?.(mutation.target))
1330
+ if (full || !turns.length) {
1331
+ applyTurns(collectConversationTurns(scroller))
1332
+ return
1333
+ }
1334
+ const next = new Set(turns.filter(turn => turn.isConnected && scroller.contains(turn)))
1335
+ const touched = new Set()
1336
+ for (const mutation of conversationMutations) {
1337
+ if (mutation.type === 'attributes') {
1338
+ collectTurnsFromNode(mutation.target, touched)
1339
+ const closest = mutation.target?.closest?.(CONVERSATION_USER_TURN_SELECTOR)
1340
+ if (closest) touched.add(closest)
1341
+ }
1342
+ for (const node of mutation.addedNodes ?? []) collectTurnsFromNode(node, touched)
1343
+ for (const node of mutation.removedNodes ?? []) {
1344
+ if (node?.nodeType !== 1) continue
1345
+ for (const turn of next) {
1346
+ if (turn === node || node.contains?.(turn)) next.delete(turn)
1347
+ }
1348
+ }
1349
+ }
1350
+ for (const turn of touched) {
1351
+ if (isTopLevelTurn(turn)) next.add(turn)
1352
+ else next.delete(turn)
1353
+ }
1354
+ const ordered = [...next].filter(isTopLevelTurn).sort((left, right) => {
1355
+ if (left === right) return 0
1356
+ return follows(left, right) ? -1 : 1
1357
+ })
1358
+ applyTurns(ordered)
1359
+ }
1360
+
1361
+ function representativeContentLeft(operationBox) {
1362
+ if (!geometryDirty) return contentLeft
1363
+ const assistant = scroller.querySelector?.([
1364
+ '[data-prts-ai-surface]:not([hidden])',
1365
+ '[data-chat-flow-kind="assistant-step"]:not([hidden])',
1366
+ '[data-message-role="assistant"]:not([hidden])',
1367
+ ].join(', '))
1368
+ const representatives = [assistant, turns[0], turns.at(-1)].filter(Boolean)
1369
+ const positions = []
1370
+ for (const node of representatives) {
1371
+ if (!visible(node)) continue
1372
+ const style = window?.getComputedStyle?.(node)
1373
+ if (style?.display === 'none' || style?.visibility === 'hidden') continue
1374
+ const box = node.getBoundingClientRect?.()
1375
+ if (!box || box.width <= 0 || box.height <= 0) continue
1376
+ positions.push(box.left - operationBox.left)
1377
+ }
1378
+ contentLeft = positions.length ? Math.min(...positions) : undefined
1379
+ geometryDirty = false
1380
+ return contentLeft
1381
+ }
1382
+
1383
+ function visibleGeometry() {
1384
+ const operationBox = operation.getBoundingClientRect()
1385
+ const scrollerBox = scroller.getBoundingClientRect()
1386
+ const composerBox = composer?.getBoundingClientRect?.()
1387
+ const top = clamp(scrollerBox.top - operationBox.top + 12, 8, Math.max(8, operationBox.height - 92))
1388
+ const bottomEdge = composerBox && composerBox.height > 0 && composerBox.top < operationBox.bottom
1389
+ ? composerBox.top - operationBox.top - 12
1390
+ : Math.min(scrollerBox.bottom, operationBox.bottom) - operationBox.top - 18
1391
+ const height = Math.max(84, bottomEdge - top)
1392
+ const drawerMode = document.documentElement.dataset.prtsDrawerMode
1393
+ const scaleGeometry = drawerMode === 'overlay'
1394
+ ? { visible: false, reason: 'drawer' }
1395
+ : window.innerWidth <= 640
1396
+ ? { visible: false, reason: 'phone' }
1397
+ : resolveConversationScaleGeometry({
1398
+ operationWidth: operationBox.width,
1399
+ contentLeft: representativeContentLeft(operationBox),
1400
+ maximumDistance,
1401
+ previouslyHidden: scaleSpaceObstructed,
1402
+ })
1403
+ const left = Number.isFinite(scaleGeometry.left)
1404
+ ? scaleGeometry.left
1405
+ : CONVERSATION_SCALE_CONTENT_GAP
1406
+ return { operationBox, scrollerBox, composerBox, top, height, left, scaleGeometry }
1407
+ }
1408
+
1409
+ function boxesIntersect(left, right) {
1410
+ return left.right > right.left
1411
+ && left.left < right.right
1412
+ && left.bottom > right.top
1413
+ && left.top < right.bottom
1414
+ }
1415
+
1416
+ function overlayIntersects(candidate, targets) {
1417
+ if (!candidate?.isConnected || candidate.hidden || candidate.getAttribute('aria-hidden') === 'true') return false
1418
+ if (candidate.closest?.('[data-prts-owned-conversation-scale], [data-prts-owned-conversation-preview]')) return false
1419
+ const style = window?.getComputedStyle?.(candidate)
1420
+ if (style?.display === 'none' || style?.visibility === 'hidden' || style?.opacity === '0') return false
1421
+ const box = candidate.getBoundingClientRect?.()
1422
+ if (!box || box.width <= 0 || box.height <= 0) return false
1423
+ return targets.some(target => target?.width > 0 && target?.height > 0 && boxesIntersect(box, target))
1424
+ }
1425
+
1426
+ function blockingOverlay(candidate) {
1427
+ if (candidate?.matches?.(CONVERSATION_BLOCKING_OVERLAY_SELECTOR)) return true
1428
+ const plugin = candidate?.matches?.('[data-plugin]') ? candidate : candidate?.closest?.('[data-plugin]')
1429
+ return Boolean(plugin
1430
+ && plugin.getAttribute('data-plugin') !== 'dsh-theme-prts'
1431
+ && plugin.closest?.('[data-shell-overlay], [data-slot="shell.overlay"]'))
1432
+ }
1433
+
1434
+ function syncScaleInteractiveState() {
1435
+ if (!scale) return true
1436
+ const hidden = scaleSpaceObstructed || scaleBlockingObstructed || scaleObstructed || scaleItemCount() < MINIMUM_TURNS
1437
+ const wasHidden = scale.tabIndex === -1
1438
+ if (hidden && !wasHidden) {
1439
+ hidePreview()
1440
+ pointerInside = false
1441
+ hoverIndex = -1
1442
+ keyboardActive = false
1443
+ retargetWave(-1)
1444
+ scale.tabIndex = -1
1445
+ scale.setAttribute('aria-hidden', 'true')
1446
+ if (document.activeElement === scale) scale.blur?.()
1447
+ } else if (!hidden && wasHidden) {
1448
+ scale.tabIndex = 0
1449
+ scale.removeAttribute('aria-hidden')
1450
+ }
1451
+ syncHistoryHint()
1452
+ return hidden
1453
+ }
1454
+
1455
+ function syncScaleSpaceObstruction(resolution) {
1456
+ const obstructed = !resolution.visible
1457
+ scaleSpaceObstructed = obstructed
1458
+ scale.toggleAttribute('data-prts-conversation-scale-space-obstructed', obstructed)
1459
+ if (obstructed) scale.dataset.prtsScaleHiddenReason = resolution.reason || 'space'
1460
+ else delete scale.dataset.prtsScaleHiddenReason
1461
+ scale.toggleAttribute('data-prts-conversation-scale-centered', Boolean(resolution.centered))
1462
+ syncScaleInteractiveState()
1463
+ return scaleSpaceObstructed || scaleItemCount() < MINIMUM_TURNS
1464
+ }
1465
+
1466
+ function syncScaleObstruction(currentGeometry) {
1467
+ if (!scale) return false
1468
+ obstructionDirty = false
1469
+ if (scaleSpaceObstructed) {
1470
+ scaleBlockingObstructed = false
1471
+ scaleObstructed = false
1472
+ scale.removeAttribute('data-prts-conversation-scale-blocked')
1473
+ scale.removeAttribute('data-prts-conversation-scale-obstructed')
1474
+ syncScaleInteractiveState()
1475
+ return false
1476
+ }
1477
+ const candidates = [...overlayCandidates]
1478
+ const operationBox = currentGeometry?.operationBox
1479
+ const blocked = Boolean(operationBox?.width && operationBox?.height)
1480
+ && candidates.some(candidate => blockingOverlay(candidate) && overlayIntersects(candidate, [operationBox]))
1481
+ const scaleBox = scale.getBoundingClientRect?.()
1482
+ const previewBox = preview?.hasAttribute?.('data-prts-conversation-preview-visible')
1483
+ ? preview.getBoundingClientRect?.()
1484
+ : undefined
1485
+ const historyHintBox = historyHint && !historyHint.hidden
1486
+ ? historyHint.getBoundingClientRect?.()
1487
+ : undefined
1488
+ const historyTooltipBox = scale._prtsHistoryTooltip && !scale._prtsHistoryTooltip.hidden
1489
+ ? scale._prtsHistoryTooltip.getBoundingClientRect?.()
1490
+ : undefined
1491
+ const historyStatusBox = historyStatus && !historyStatus.hidden
1492
+ ? historyStatus.getBoundingClientRect?.()
1493
+ : undefined
1494
+ const localTargets = [scaleBox, previewBox, historyHintBox, historyTooltipBox, historyStatusBox].filter(Boolean)
1495
+ const obstructed = !blocked
1496
+ && localTargets.length > 0
1497
+ && candidates.some(candidate => !blockingOverlay(candidate) && overlayIntersects(candidate, localTargets))
1498
+ scaleBlockingObstructed = blocked
1499
+ scaleObstructed = obstructed
1500
+ scale.toggleAttribute('data-prts-conversation-scale-blocked', blocked)
1501
+ scale.toggleAttribute('data-prts-conversation-scale-obstructed', obstructed)
1502
+ syncScaleInteractiveState()
1503
+ return blocked || obstructed
1504
+ }
1505
+
1506
+ function positionPreview(index) {
1507
+ if (!preview || !layout || !geometry || index < 0 || index >= layout.count) return
1508
+ const scaleIndex = turnToScaleIndex(index)
1509
+ const tickY = clamp(layout.positions[scaleIndex], .5, layout.height - .5)
1510
+ const anchor = geometry.top + tickY
1511
+ const previewBox = preview.getBoundingClientRect?.()
1512
+ const measuredHeight = previewBox?.height || preview.offsetHeight || PREVIEW_FALLBACK_HEIGHT
1513
+ const maximumTop = Math.max(geometry.top, geometry.top + geometry.height - measuredHeight)
1514
+ const top = clamp(anchor - measuredHeight / 2, geometry.top, maximumTop)
1515
+ const left = geometry.left + SCALE_WIDTH + PREVIEW_GAP
1516
+ preview.style.left = `${Number(left.toFixed(2))}px`
1517
+ preview.style.top = `${Number(top.toFixed(2))}px`
1518
+ preview.style.setProperty('--prts-preview-anchor-y', `${Number(clamp(anchor - top, 1, measuredHeight - 1).toFixed(2))}px`)
1519
+ }
1520
+
1521
+ function render() {
1522
+ if (!operation || !scroller || !scale) return
1523
+ geometry = visibleGeometry()
1524
+ scale.style.top = `${Number(geometry.top.toFixed(2))}px`
1525
+ scale.style.left = `${Number(geometry.left.toFixed(2))}px`
1526
+ scale.style.height = `${Number(geometry.height.toFixed(2))}px`
1527
+ historyStatus.style.top = `${Number(geometry.top.toFixed(2))}px`
1528
+ historyStatus.style.left = `${Number((geometry.left + SCALE_WIDTH + PREVIEW_GAP).toFixed(2))}px`
1529
+ scale.dataset.prtsScaleMaxDistance = String(maximumDistance)
1530
+ scale.dataset.prtsScaleFocusContrast = String(focusContrast)
1531
+ syncHistoryStatus()
1532
+ if (syncScaleSpaceObstruction(geometry.scaleGeometry)) return
1533
+ if (obstructionDirty) {
1534
+ if (syncScaleObstruction(geometry)) return
1535
+ } else if (scaleBlockingObstructed || scaleObstructed) {
1536
+ return
1537
+ }
1538
+
1539
+ const provisional = getConversationScaleLayout(scaleItemCount(), geometry.height, scaleOffset)
1540
+ if (followBottom) scaleOffset = provisional.maxOffset
1541
+ layout = getConversationScaleLayout(scaleItemCount(), geometry.height, scaleOffset)
1542
+ scaleOffset = layout.offset
1543
+ previousMaximum = layout.maxOffset
1544
+ followBottom = false
1545
+ const historyY = layout.positions[0] ?? .5
1546
+ historyHint.style.top = `${Number((geometry.top + historyY).toFixed(2))}px`
1547
+ historyHint.style.left = `${Number(geometry.left.toFixed(2))}px`
1548
+ syncHistoryHint()
1549
+ const historyAnchor = geometry.top + historyY
1550
+ const historyTooltipTop = clamp(
1551
+ historyAnchor - PREVIEW_FALLBACK_HEIGHT / 2,
1552
+ geometry.top,
1553
+ Math.max(geometry.top, geometry.top + geometry.height - PREVIEW_FALLBACK_HEIGHT),
1554
+ )
1555
+ const historyTooltip = scale._prtsHistoryTooltip
1556
+ historyTooltip.style.setProperty('--prts-history-tooltip-top', `${Number((historyTooltipTop - historyAnchor + HISTORY_TRIGGER_HEIGHT / 2).toFixed(2))}px`)
1557
+ historyTooltip.style.setProperty('--prts-history-tooltip-anchor-y', `${Number(clamp(historyAnchor - historyTooltipTop, 1, PREVIEW_FALLBACK_HEIGHT - 1).toFixed(2))}px`)
1558
+
1559
+ const interactionIndex = hoverIndex >= 0
1560
+ ? hoverIndex
1561
+ : keyboardActive && scale.matches(':focus') ? cursorIndex : -1
1562
+ const paths = buildConversationScalePaths(layout, interactionIndex, {
1563
+ center: waveCenter,
1564
+ intensity: waveIntensity,
1565
+ historyAvailable,
1566
+ }, scaleLengths)
1567
+ scale._prtsSvg.setAttribute('viewBox', `0 0 ${SCALE_WIDTH} ${layout.height}`)
1568
+ scale._prtsGray.setAttribute('d', paths.gray)
1569
+ scale._prtsAccent.setAttribute('d', paths.yellow)
1570
+ scale._prtsHistory.setAttribute('d', paths.history)
1571
+ scale._prtsHit.setAttribute('height', String(layout.height))
1572
+ scale._prtsMask.setAttribute('height', String(layout.height))
1573
+ scale._prtsMaskRect.setAttribute('height', String(layout.height))
1574
+ if (scaleOffset > 0) {
1575
+ scale._prtsMarks.setAttribute('mask', `url(#${scale._prtsMaskId})`)
1576
+ scale.setAttribute('data-prts-conversation-scale-top-fade', '')
1577
+ } else {
1578
+ scale._prtsMarks.removeAttribute('mask')
1579
+ scale.removeAttribute('data-prts-conversation-scale-top-fade')
1580
+ }
1581
+ scale.dataset.prtsScaleCount = String(turns.length)
1582
+ scale.toggleAttribute('data-prts-conversation-scale-partial', historyAvailable)
1583
+ scale.dataset.prtsScaleOffset = String(Number(scaleOffset.toFixed(2)))
1584
+ scale.dataset.prtsScaleMaximum = String(Number(layout.maxOffset.toFixed(2)))
1585
+ if (interactionIndex >= 0) scale.dataset.prtsScaleWave = String(interactionIndex + 1)
1586
+ else delete scale.dataset.prtsScaleWave
1587
+ if (previewIndex >= 0) positionPreview(previewIndex)
1588
+ syncAccessibility()
1589
+ }
1590
+
1591
+ function syncAccessibility() {
1592
+ if (!scale) return
1593
+ const maximum = Math.max(1, scaleItemCount())
1594
+ const current = clamp(cursorIndex + 1, 1, maximum)
1595
+ scale.setAttribute('aria-valuemin', '1')
1596
+ scale.setAttribute('aria-valuemax', String(maximum))
1597
+ scale.setAttribute('aria-valuenow', String(current))
1598
+ scale.setAttribute(
1599
+ 'aria-valuetext',
1600
+ historyAvailable && cursorIndex === 0
1601
+ ? '更早内容尚未载入,按回车前往会话开头'
1602
+ : turns.length
1603
+ ? `当前已载入第 ${scaleToTurnIndex(cursorIndex) + 1} 轮,共 ${turns.length} 轮${historyAvailable ? ',前方仍有更早内容' : ''}`
1604
+ : '无可用会话刻度',
1605
+ )
1606
+ }
1607
+
1608
+ function flush() {
1609
+ frame = undefined
1610
+ if (!started || failed) return
1611
+ try {
1612
+ if (!resolveRoots()) return
1613
+ readHistoryState()
1614
+ if (scanPending) {
1615
+ scanPending = false
1616
+ const mutations = pendingMutations
1617
+ const full = fullScanPending
1618
+ pendingMutations = []
1619
+ fullScanPending = false
1620
+ syncTurns(mutations, full)
1621
+ }
1622
+ render()
1623
+ } catch {
1624
+ failed = true
1625
+ disconnectRoots()
1626
+ }
1627
+ }
1628
+
1629
+ function localIndex(event) {
1630
+ if (!layout || scaleItemCount() < MINIMUM_TURNS) return -1
1631
+ const box = scale.getBoundingClientRect()
1632
+ return nearestVisibleConversationTick(layout, event.clientY - box.top)
1633
+ }
1634
+
1635
+ function ensureVisible(index) {
1636
+ if (!layout || index < 0) return
1637
+ const target = layout.start + index * CONVERSATION_SCALE_SPACING - scaleOffset
1638
+ if (target < .5) scaleOffset += target - .5
1639
+ else if (target > layout.height - .5) scaleOffset += target - (layout.height - .5)
1640
+ scaleOffset = clamp(scaleOffset, 0, layout.maxOffset)
1641
+ }
1642
+
1643
+ function animatePreviewContent() {
1644
+ if (reducedMotion() || typeof preview?._prtsBody?.animate !== 'function') return
1645
+ preview._prtsBody.animate([
1646
+ { opacity: .28 },
1647
+ { opacity: 1 },
1648
+ ], { duration: 70, easing: 'ease-out' })
1649
+ }
1650
+
1651
+ function updatePreviewContent(index, animate = false) {
1652
+ if (!preview || index < 0 || index >= turns.length) return
1653
+ const content = getConversationTurnPreview(scroller, turns, index)
1654
+ const changed = preview._prtsQuestion.textContent !== content.question
1655
+ || preview._prtsAnswer.textContent !== content.answer
1656
+ preview._prtsQuestion.textContent = content.question
1657
+ preview._prtsAnswer.textContent = content.answer
1658
+ preview.dataset.prtsPreviewState = content.state
1659
+ preview.dataset.prtsPreviewTurn = String(index + 1)
1660
+ observePreviewSource(content.answerSource)
1661
+ if (changed && animate && preview.hasAttribute('data-prts-conversation-preview-visible')) animatePreviewContent()
1662
+ }
1663
+
1664
+ function showPreview(index, { animate = true, temporary = false } = {}) {
1665
+ if (!preview || index < 0 || index >= turns.length) return
1666
+ if (previewDelayTimer !== undefined) window?.clearTimeout?.(previewDelayTimer)
1667
+ if (previewHideTimer !== undefined) window?.clearTimeout?.(previewHideTimer)
1668
+ previewDelayTimer = undefined
1669
+ previewHideTimer = undefined
1670
+ pendingPreviewIndex = -1
1671
+ const switched = previewIndex !== index
1672
+ previewIndex = index
1673
+ updatePreviewContent(index, animate && switched)
1674
+ preview.setAttribute('data-prts-conversation-preview-visible', '')
1675
+ preview.setAttribute('aria-hidden', 'false')
1676
+ scale?.setAttribute('aria-describedby', preview.id)
1677
+ positionPreview(index)
1678
+ if (temporary) {
1679
+ previewHideTimer = window?.setTimeout?.(() => {
1680
+ previewHideTimer = undefined
1681
+ hidePreview()
1682
+ }, PREVIEW_TOUCH_DURATION)
1683
+ }
1684
+ }
1685
+
1686
+ function queuePreview(index) {
1687
+ if (index < 0 || index >= turns.length) return
1688
+ if (preview?.hasAttribute('data-prts-conversation-preview-visible')) {
1689
+ showPreview(index)
1690
+ return
1691
+ }
1692
+ if (pendingPreviewIndex === index && previewDelayTimer !== undefined) return
1693
+ if (previewDelayTimer !== undefined) window?.clearTimeout?.(previewDelayTimer)
1694
+ pendingPreviewIndex = index
1695
+ previewDelayTimer = window?.setTimeout?.(() => {
1696
+ previewDelayTimer = undefined
1697
+ const pending = pendingPreviewIndex
1698
+ pendingPreviewIndex = -1
1699
+ if (pointerInside && scaleToTurnIndex(hoverIndex) === pending) showPreview(pending)
1700
+ }, PREVIEW_DELAY)
1701
+ }
1702
+
1703
+ function hidePreview() {
1704
+ if (previewDelayTimer !== undefined) window?.clearTimeout?.(previewDelayTimer)
1705
+ if (previewHideTimer !== undefined) window?.clearTimeout?.(previewHideTimer)
1706
+ if (previewRefreshTimer !== undefined) window?.clearTimeout?.(previewRefreshTimer)
1707
+ previewDelayTimer = undefined
1708
+ previewHideTimer = undefined
1709
+ previewRefreshTimer = undefined
1710
+ pendingPreviewIndex = -1
1711
+ previewIndex = -1
1712
+ disconnectPreviewObserver()
1713
+ preview?.removeAttribute('data-prts-conversation-preview-visible')
1714
+ preview?.setAttribute('aria-hidden', 'true')
1715
+ scale?.removeAttribute('aria-describedby')
1716
+ }
1717
+
1718
+ function queuePreviewRefresh() {
1719
+ if (previewIndex < 0 || previewRefreshTimer !== undefined) return
1720
+ previewRefreshTimer = window?.setTimeout?.(() => {
1721
+ previewRefreshTimer = undefined
1722
+ if (previewIndex >= 0) {
1723
+ updatePreviewContent(previewIndex, true)
1724
+ positionPreview(previewIndex)
1725
+ }
1726
+ }, PREVIEW_REFRESH_INTERVAL)
1727
+ }
1728
+
1729
+ function wait(milliseconds) {
1730
+ return new Promise(resolve => window?.setTimeout?.(resolve, milliseconds))
1731
+ }
1732
+
1733
+ async function waitForHistoryPage(request, before, deadline) {
1734
+ let sawLoading = false
1735
+ while (Date.now() < deadline) {
1736
+ await wait(40)
1737
+ if (request !== historyRequest) return 'cancelled'
1738
+ const state = readHistoryState()
1739
+ sawLoading ||= state.loading || Boolean(state.olderButton?.disabled)
1740
+ const firstTurn = collectConversationTurns(scroller)[0]
1741
+ const progressed = historyMutationRevision > before.revision
1742
+ && (firstTurn !== before.firstTurn || !state.loading)
1743
+ if (!state.hasMore && !state.olderButton && !state.loading) return 'complete'
1744
+ if (progressed && !state.loading && !state.olderButton?.disabled) return 'progress'
1745
+ if (sawLoading && !state.loading && !state.olderButton?.disabled) return 'progress'
1746
+ }
1747
+ return 'timeout'
1748
+ }
1749
+
1750
+ async function loadHistoryToStart() {
1751
+ if (!operation || !scroller || historyPhase === 'loading') return
1752
+ const initial = readHistoryState()
1753
+ if (!historyAvailable) {
1754
+ if (turns[0]) jump(0)
1755
+ return
1756
+ }
1757
+ const request = historyRequest + 1
1758
+ historyRequest = request
1759
+ const startedAt = Date.now()
1760
+ const sessionKey = initial.key
1761
+ let loadedThisRun = 0
1762
+ historyPhase = 'loading'
1763
+ historyMessage = ''
1764
+ hidePreview()
1765
+ syncHistoryStatus()
1766
+ schedule()
1767
+
1768
+ while (loadedThisRun < CONVERSATION_HISTORY_PAGE_LIMIT
1769
+ && Date.now() - startedAt < CONVERSATION_HISTORY_TIME_LIMIT) {
1770
+ if (request !== historyRequest) return
1771
+ const state = readHistoryState()
1772
+ if (sessionKey !== undefined && state.key !== undefined && state.key !== sessionKey) {
1773
+ cancelHistory({ paused: false })
1774
+ return
1775
+ }
1776
+ if (!state.hasMore && !state.olderButton && !state.loading) {
1777
+ setHistoryAvailable(false)
1778
+ historyPhase = 'idle'
1779
+ syncTurns([], true)
1780
+ schedule()
1781
+ if (turns[0]) jump(0)
1782
+ return
1783
+ }
1784
+ if (state.loading || state.olderButton?.disabled) {
1785
+ const result = await waitForHistoryPage(
1786
+ request,
1787
+ {
1788
+ firstTurn: collectConversationTurns(scroller)[0],
1789
+ revision: historyMutationRevision,
1790
+ },
1791
+ Math.min(startedAt + CONVERSATION_HISTORY_TIME_LIMIT, Date.now() + HISTORY_PAGE_SETTLE_LIMIT),
1792
+ )
1793
+ if (result === 'cancelled') return
1794
+ if (result === 'timeout') {
1795
+ historyPhase = 'error'
1796
+ historyMessage = '载入超时,可继续重试'
1797
+ break
1798
+ }
1799
+ historyPages += 1
1800
+ loadedThisRun += 1
1801
+ syncTurns([], true)
1802
+ syncHistoryStatus()
1803
+ schedule()
1804
+ continue
1805
+ }
1806
+ if (!state.olderButton) {
1807
+ const availableDeadline = Math.min(startedAt + CONVERSATION_HISTORY_TIME_LIMIT, Date.now() + 600)
1808
+ while (request === historyRequest && Date.now() < availableDeadline && !findConversationOlderButton(operation)) {
1809
+ await wait(40)
1810
+ }
1811
+ }
1812
+ const button = findConversationOlderButton(operation)
1813
+ if (!button || button.disabled) {
1814
+ historyPhase = 'error'
1815
+ historyMessage = '暂时无法载入更早内容'
1816
+ break
1817
+ }
1818
+ const before = {
1819
+ firstTurn: collectConversationTurns(scroller)[0],
1820
+ revision: historyMutationRevision,
1821
+ }
1822
+ button.click()
1823
+ const result = await waitForHistoryPage(
1824
+ request,
1825
+ before,
1826
+ Math.min(startedAt + CONVERSATION_HISTORY_TIME_LIMIT, Date.now() + HISTORY_PAGE_SETTLE_LIMIT),
1827
+ )
1828
+ if (result === 'cancelled') return
1829
+ if (result === 'timeout') {
1830
+ historyPhase = 'error'
1831
+ historyMessage = '载入超时,可继续重试'
1832
+ break
1833
+ }
1834
+ historyPages += 1
1835
+ loadedThisRun += 1
1836
+ syncTurns([], true)
1837
+ syncHistoryStatus()
1838
+ schedule()
1839
+ }
1840
+
1841
+ if (request !== historyRequest) return
1842
+ const finalState = readHistoryState()
1843
+ if (!finalState.hasMore && !finalState.olderButton && !finalState.loading) {
1844
+ setHistoryAvailable(false)
1845
+ historyPhase = 'idle'
1846
+ syncTurns([], true)
1847
+ if (turns[0]) jump(0)
1848
+ } else if (historyPhase === 'loading') {
1849
+ historyPhase = 'paused'
1850
+ }
1851
+ syncHistoryStatus()
1852
+ schedule()
1853
+ }
1854
+
1855
+ function jump(index) {
1856
+ const turn = turns[index]
1857
+ if (!turn || !scroller || !operation) return
1858
+ const jumpScroller = scroller
1859
+ const jumpOperation = operation
1860
+ const jumpComposer = composer
1861
+ const request = jumpRequest + 1
1862
+ jumpRequest = request
1863
+ const getTargetTop = () => {
1864
+ if (!turn.isConnected || scroller !== jumpScroller || operation !== jumpOperation) return Number.NaN
1865
+ const operationBox = jumpOperation.getBoundingClientRect()
1866
+ const scrollerBox = jumpScroller.getBoundingClientRect()
1867
+ const composerBox = jumpComposer?.getBoundingClientRect?.()
1868
+ const visibleTop = Math.max(scrollerBox.top, operationBox.top)
1869
+ const visibleBottom = Math.min(scrollerBox.bottom, operationBox.bottom, composerBox?.top ?? operationBox.bottom)
1870
+ return calculateConversationJumpTop({
1871
+ scrollTop: jumpScroller.scrollTop,
1872
+ scrollHeight: jumpScroller.scrollHeight,
1873
+ clientHeight: jumpScroller.clientHeight,
1874
+ turnTop: turn.getBoundingClientRect().top,
1875
+ visibleTop,
1876
+ visibleHeight: Math.max(0, visibleBottom - visibleTop),
1877
+ })
1878
+ }
1879
+ void performConversationJump({
1880
+ scroller: jumpScroller,
1881
+ getTargetTop,
1882
+ prefersReducedMotion: reducedMotion(),
1883
+ requestFrame,
1884
+ now: () => currentTime(),
1885
+ cancelled: () => request !== jumpRequest,
1886
+ onFrame: schedule,
1887
+ }).catch(() => {
1888
+ if (request === jumpRequest) cancelJump()
1889
+ })
1890
+ cursorIndex = turnToScaleIndex(index)
1891
+ syncAccessibility()
1892
+ }
1893
+
1894
+ function showScalePreview(index, options) {
1895
+ const turnIndex = scaleToTurnIndex(index)
1896
+ if (turnIndex < 0) {
1897
+ hidePreview()
1898
+ return
1899
+ }
1900
+ showPreview(turnIndex, options)
1901
+ }
1902
+
1903
+ function queueScalePreview(index) {
1904
+ const turnIndex = scaleToTurnIndex(index)
1905
+ if (turnIndex < 0) {
1906
+ hidePreview()
1907
+ return
1908
+ }
1909
+ queuePreview(turnIndex)
1910
+ }
1911
+
1912
+ function activateScaleIndex(index, options) {
1913
+ if (historyAvailable && index === 0) {
1914
+ hidePreview()
1915
+ void loadHistoryToStart()
1916
+ return
1917
+ }
1918
+ const turnIndex = scaleToTurnIndex(index)
1919
+ if (turnIndex < 0) return
1920
+ showPreview(turnIndex, options)
1921
+ jump(turnIndex)
1922
+ }
1923
+
1924
+ function showWave(index, temporary = false) {
1925
+ if (index < 0) return
1926
+ hoverIndex = index
1927
+ cursorIndex = index
1928
+ ensureVisible(index)
1929
+ retargetWave(index)
1930
+ if (touchTimer !== undefined) window?.clearTimeout?.(touchTimer)
1931
+ if (temporary) {
1932
+ touchTimer = window?.setTimeout?.(() => {
1933
+ touchTimer = undefined
1934
+ hoverIndex = -1
1935
+ retargetWave(-1)
1936
+ schedule()
1937
+ }, TOUCH_WAVE_DURATION)
1938
+ }
1939
+ schedule()
1940
+ }
1941
+
1942
+ function onPointerMove(event) {
1943
+ if (event.pointerType === 'touch') return
1944
+ pointerInside = true
1945
+ keyboardActive = false
1946
+ const index = localIndex(event)
1947
+ if (index === hoverIndex) return
1948
+ showWave(index)
1949
+ queueScalePreview(index)
1950
+ }
1951
+
1952
+ function onPointerLeave(event) {
1953
+ if (event.pointerType === 'touch') return
1954
+ pointerInside = false
1955
+ hoverIndex = -1
1956
+ retargetWave(-1)
1957
+ hidePreview()
1958
+ schedule()
1959
+ }
1960
+
1961
+ function onPointerDown(event) {
1962
+ cancelNavigation()
1963
+ if (event.pointerType !== 'touch' && event.pointerType !== 'pen') return
1964
+ pointerInside = true
1965
+ pointerStart = { x: event.clientX, y: event.clientY, id: event.pointerId }
1966
+ scale.setPointerCapture?.(event.pointerId)
1967
+ event.preventDefault()
1968
+ }
1969
+
1970
+ function onPointerUp(event) {
1971
+ if (!pointerStart || pointerStart.id !== event.pointerId) return
1972
+ const distance = Math.hypot(event.clientX - pointerStart.x, event.clientY - pointerStart.y)
1973
+ pointerStart = undefined
1974
+ if (event.pointerType === 'touch') pointerInside = false
1975
+ scale.releasePointerCapture?.(event.pointerId)
1976
+ if (distance > 8) return
1977
+ const index = localIndex(event)
1978
+ if (index < 0) return
1979
+ lastDirectJump = Date.now()
1980
+ keyboardActive = false
1981
+ showWave(index, true)
1982
+ activateScaleIndex(index, { temporary: true })
1983
+ scale.focus({ preventScroll: true })
1984
+ event.preventDefault()
1985
+ }
1986
+
1987
+ function onPointerCancel(event) {
1988
+ if (pointerStart?.id === event.pointerId) {
1989
+ pointerStart = undefined
1990
+ if (event.pointerType === 'touch') pointerInside = false
1991
+ }
1992
+ }
1993
+
1994
+ function onClick(event) {
1995
+ if (Date.now() - lastDirectJump < 300) return
1996
+ keyboardActive = false
1997
+ const index = localIndex(event)
1998
+ if (index < 0) return
1999
+ showWave(index)
2000
+ activateScaleIndex(index)
2001
+ scale.focus({ preventScroll: true })
2002
+ }
2003
+
2004
+ function queueSnap() {
2005
+ if (snapTimer !== undefined) window?.clearTimeout?.(snapTimer)
2006
+ snapTimer = window?.setTimeout?.(() => {
2007
+ snapTimer = undefined
2008
+ if (!layout) return
2009
+ scaleOffset = snapConversationScaleOffset(scaleOffset, layout.maxOffset)
2010
+ schedule()
2011
+ }, SNAP_DELAY)
2012
+ }
2013
+
2014
+ function onWheel(event) {
2015
+ cancelNavigation()
2016
+ event.preventDefault()
2017
+ event.stopPropagation()
2018
+ hidePreview()
2019
+ if (!layout || layout.maxOffset <= 0) return
2020
+ const factor = event.deltaMode === 1 ? CONVERSATION_SCALE_SPACING : event.deltaMode === 2 ? layout.height : 1
2021
+ scaleOffset = clamp(scaleOffset + event.deltaY * factor, 0, layout.maxOffset)
2022
+ keyboardActive = false
2023
+ hoverIndex = -1
2024
+ retargetWave(-1)
2025
+ schedule()
2026
+ queueSnap()
2027
+ }
2028
+
2029
+ function page(direction) {
2030
+ if (!layout) return
2031
+ const distance = Math.max(CONVERSATION_SCALE_SPACING, Math.floor(layout.height / CONVERSATION_SCALE_SPACING) * CONVERSATION_SCALE_SPACING)
2032
+ scaleOffset = clamp(scaleOffset + direction * distance, 0, layout.maxOffset)
2033
+ const edgeY = direction < 0 ? .5 : layout.height - .5
2034
+ const nextLayout = getConversationScaleLayout(scaleItemCount(), layout.height, scaleOffset)
2035
+ cursorIndex = nearestVisibleConversationTick(nextLayout, edgeY)
2036
+ hoverIndex = cursorIndex
2037
+ retargetWave(cursorIndex)
2038
+ showScalePreview(cursorIndex)
2039
+ schedule()
2040
+ }
2041
+
2042
+ function onKeydown(event) {
2043
+ if (scaleItemCount() < MINIMUM_TURNS) return
2044
+ if (event.key === 'Escape' && historyPhase === 'loading') {
2045
+ cancelHistory()
2046
+ event.preventDefault()
2047
+ return
2048
+ }
2049
+ if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
2050
+ keyboardActive = true
2051
+ cursorIndex = clamp(cursorIndex + (event.key === 'ArrowUp' ? -1 : 1), 0, scaleItemCount() - 1)
2052
+ hoverIndex = cursorIndex
2053
+ ensureVisible(cursorIndex)
2054
+ showScalePreview(cursorIndex)
2055
+ } else if (event.key === 'Home' || event.key === 'End') {
2056
+ keyboardActive = true
2057
+ cursorIndex = event.key === 'Home' ? 0 : scaleItemCount() - 1
2058
+ hoverIndex = cursorIndex
2059
+ ensureVisible(cursorIndex)
2060
+ showScalePreview(cursorIndex)
2061
+ } else if (event.key === 'PageUp' || event.key === 'PageDown') {
2062
+ keyboardActive = true
2063
+ page(event.key === 'PageUp' ? -1 : 1)
2064
+ event.preventDefault()
2065
+ return
2066
+ } else if (event.key === 'Enter' || event.key === ' ') {
2067
+ event.preventDefault()
2068
+ activateScaleIndex(cursorIndex)
2069
+ return
2070
+ } else return
2071
+ event.preventDefault()
2072
+ retargetWave(cursorIndex)
2073
+ schedule()
2074
+ }
2075
+
2076
+ function onFocus() {
2077
+ ensureVisible(cursorIndex)
2078
+ schedule()
2079
+ }
2080
+
2081
+ function onBlur() {
2082
+ keyboardActive = false
2083
+ hoverIndex = -1
2084
+ retargetWave(-1)
2085
+ if (!pointerInside) hidePreview()
2086
+ schedule()
2087
+ }
2088
+
2089
+ return {
2090
+ getCalibrationState(candidate = maximumDistance) {
2091
+ if (!started || failed || (!operation && !resolveRoots())) {
2092
+ const fallback = resolveConversationScaleCalibrationState({
2093
+ maximumDistance: candidate,
2094
+ viewportWidth: window?.innerWidth,
2095
+ })
2096
+ return { ...fallback, simulated: true }
2097
+ }
2098
+ const cachedOperationBox = geometry?.operationBox
2099
+ const operationBox = cachedOperationBox ?? operation.getBoundingClientRect?.()
2100
+ const cachedContentLeft = Number.isFinite(contentLeft) ? contentLeft : undefined
2101
+ return resolveConversationScaleCalibrationState({
2102
+ operationWidth: operationBox?.width,
2103
+ contentLeft: cachedContentLeft ?? representativeContentLeft(operationBox),
2104
+ maximumDistance: candidate,
2105
+ turnCount: turns.length,
2106
+ drawerMode: document.documentElement.dataset.prtsDrawerMode,
2107
+ viewportWidth: window?.innerWidth,
2108
+ previouslyHidden: scaleSpaceObstructed,
2109
+ })
2110
+ },
2111
+ update(nextPreferences = {}) {
2112
+ const distanceNumeric = Number(nextPreferences.conversationScaleMaxDistance)
2113
+ const nextDistance = Number.isFinite(distanceNumeric)
2114
+ ? clamp(Math.round(distanceNumeric / 8) * 8, 16, 240)
2115
+ : CONVERSATION_SCALE_DEFAULT_MAX_DISTANCE
2116
+ const contrastNumeric = Number(nextPreferences.conversationScaleFocusContrast)
2117
+ const nextContrast = Number.isFinite(contrastNumeric)
2118
+ ? clamp(Math.round(contrastNumeric / 10) * 10, 0, 100)
2119
+ : CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT
2120
+ if (nextDistance === maximumDistance && nextContrast === focusContrast) return
2121
+ maximumDistance = nextDistance
2122
+ if (nextContrast !== focusContrast) {
2123
+ focusContrast = nextContrast
2124
+ scaleLengths = resolveConversationScaleLengths(focusContrast)
2125
+ }
2126
+ schedule()
2127
+ },
2128
+ start() {
2129
+ if (started || !document) return
2130
+ started = true
2131
+ failed = false
2132
+ const ResizeObserver = window?.ResizeObserver
2133
+ if (typeof ResizeObserver === 'function') {
2134
+ resizeObserver = new ResizeObserver((entries = []) => {
2135
+ geometryDirty = true
2136
+ const operationEntry = entries.find(entry => entry.target === operation)
2137
+ const width = Number(operationEntry?.contentRect?.width)
2138
+ const height = Number(operationEntry?.contentRect?.height)
2139
+ const operationChanged = !entries.length || Boolean(operationEntry) && (
2140
+ !Number.isFinite(width)
2141
+ || !Number.isFinite(height)
2142
+ || width !== observedOperationWidth
2143
+ || height !== observedOperationHeight
2144
+ )
2145
+ if (operationEntry) {
2146
+ if (Number.isFinite(width)) observedOperationWidth = width
2147
+ if (Number.isFinite(height)) observedOperationHeight = height
2148
+ }
2149
+ if (!operationChanged) {
2150
+ schedule()
2151
+ return
2152
+ }
2153
+ if (!resizeActive) {
2154
+ setScaleResizeState(true)
2155
+ if (frame !== undefined) {
2156
+ if (window?.cancelAnimationFrame) window.cancelAnimationFrame(frame)
2157
+ else window?.clearTimeout?.(frame)
2158
+ frame = undefined
2159
+ }
2160
+ }
2161
+ if (resizeSettleTimer !== undefined) window?.clearTimeout?.(resizeSettleTimer)
2162
+ resizeSettleTimer = window?.setTimeout?.(() => {
2163
+ resizeSettleTimer = undefined
2164
+ obstructionDirty = true
2165
+ setScaleResizeState(false)
2166
+ schedule()
2167
+ }, 160)
2168
+ })
2169
+ }
2170
+ const MutationObserver = window?.MutationObserver
2171
+ if (typeof MutationObserver === 'function' && document.body) {
2172
+ mutationObserver = new MutationObserver(mutations => {
2173
+ const external = mutations.filter(mutation =>
2174
+ !mutation.target?.closest?.('[data-prts-conversation-scale], [data-prts-conversation-preview], [data-prts-conversation-history-hint], [data-prts-conversation-history-status]')
2175
+ )
2176
+ const historyRelevant = external.some(mutation =>
2177
+ mutation.type === 'childList'
2178
+ && operation?.contains?.(mutation.target)
2179
+ && ((mutation.addedNodes?.length ?? 0) > 0 || (mutation.removedNodes?.length ?? 0) > 0)
2180
+ )
2181
+ if (historyRelevant) historyMutationRevision += 1
2182
+ const relevant = external.filter(mutation =>
2183
+ mutationAffectsTurns(mutation) || mutationAffectsOverlay(mutation)
2184
+ )
2185
+ if (relevant.some(mutationAffectsOverlay)) obstructionDirty = true
2186
+ if (!relevant.length && !historyRelevant) return
2187
+ if (previewIndex >= 0) queuePreviewRefresh()
2188
+ pendingMutations.push(...relevant)
2189
+ scanPending ||= relevant.length > 0
2190
+ schedule()
2191
+ })
2192
+ mutationObserver.observe(document.body, {
2193
+ subtree: true,
2194
+ childList: true,
2195
+ attributes: true,
2196
+ attributeFilter: [
2197
+ 'data-chat-flow-kind',
2198
+ 'data-message-role',
2199
+ 'data-prts-floating-glass',
2200
+ 'hidden',
2201
+ 'aria-hidden',
2202
+ 'aria-modal',
2203
+ 'role',
2204
+ 'open',
2205
+ 'popover',
2206
+ 'class',
2207
+ 'data-slot',
2208
+ 'data-plugin',
2209
+ 'style',
2210
+ ],
2211
+ })
2212
+ }
2213
+ schedule(true)
2214
+ },
2215
+ dispose() {
2216
+ started = false
2217
+ failed = false
2218
+ mutationObserver?.disconnect()
2219
+ mutationObserver = undefined
2220
+ resizeObserver?.disconnect()
2221
+ resizeObserver = undefined
2222
+ disconnectPreviewObserver()
2223
+ if (frame !== undefined) {
2224
+ if (window?.cancelAnimationFrame) window.cancelAnimationFrame(frame)
2225
+ else window?.clearTimeout?.(frame)
2226
+ }
2227
+ frame = undefined
2228
+ scanPending = false
2229
+ fullScanPending = false
2230
+ pendingMutations = []
2231
+ overlayCandidates.clear()
2232
+ overlayScanPending = false
2233
+ scaleObstructed = false
2234
+ scaleBlockingObstructed = false
2235
+ scaleSpaceObstructed = true
2236
+ contentLeft = undefined
2237
+ geometryDirty = true
2238
+ clearTimers()
2239
+ setScaleResizeState(false)
2240
+ disconnectRoots()
2241
+ },
2242
+ }
2243
+ }