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