@ash-qw/dsh-theme-prts 0.1.94 → 0.1.95

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