@ash-qw/dsh-theme-prts 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/THIRD_PARTY_NOTICES.zh-CN.md +7 -0
- package/lib/client.js +761 -23
- package/package.json +3 -2
- package/src/client/facility-geometry.js +14 -10
- package/src/client/facility-vector.js +1 -1
- package/src/client/index.js +3 -1
- package/src/client/preferences.js +12 -1
- package/src/client/preview-markdown.js +18 -0
- package/src/client/session-summary-preview.js +479 -0
- package/src/client/sidebar-control-adapter.js +52 -6
- package/src/client/theme-settings-workbench.js +29 -2
- package/src/styles/facility-vector.css +91 -3
- package/src/styles/prts.css +3 -9
- package/src/styles/refinement.css +10 -10
- package/src/styles/theme-settings-workbench.css +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ash-qw/dsh-theme-prts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web and the official desktop client",
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@playwright/test": "1.55.1",
|
|
76
|
-
"jsdom": "24.1.3"
|
|
76
|
+
"jsdom": "24.1.3",
|
|
77
|
+
"markdown-it": "^14.3.2"
|
|
77
78
|
},
|
|
78
79
|
"dependencies": {
|
|
79
80
|
"sharp": "^0.35.5"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const FACILITY_GEOMETRY = Object.freeze({
|
|
2
2
|
stroke: 1,
|
|
3
3
|
corner: 1,
|
|
4
|
-
sideDepth:
|
|
4
|
+
sideDepth: 14 / 17,
|
|
5
5
|
sideOpening: 10,
|
|
6
6
|
sideInner: 6,
|
|
7
7
|
topOpening: 50,
|
|
@@ -20,7 +20,7 @@ function point(x, y) {
|
|
|
20
20
|
return `${number(x)} ${number(y)}`
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function createFacilityPath({ width, height, topNotch = false, sessionNotch = false } = {}) {
|
|
23
|
+
export function createFacilityPath({ width, height, topNotch = false, sessionNotch = false, spine = false } = {}) {
|
|
24
24
|
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 2 || height <= FACILITY_GEOMETRY.sideOpening + 2) return ''
|
|
25
25
|
|
|
26
26
|
const halfStroke = FACILITY_GEOMETRY.stroke / 2
|
|
@@ -28,6 +28,10 @@ export function createFacilityPath({ width, height, topNotch = false, sessionNot
|
|
|
28
28
|
const right = width - halfStroke
|
|
29
29
|
const top = halfStroke
|
|
30
30
|
const bottom = height - halfStroke
|
|
31
|
+
// Keep the original RIIC sidebar width ratio; align notch ends with the card face.
|
|
32
|
+
const sideDepth = spine ? (width - FACILITY_GEOMETRY.stroke) * 2 / 17 : FACILITY_GEOMETRY.sideDepth
|
|
33
|
+
const sideOpening = FACILITY_GEOMETRY.sideOpening
|
|
34
|
+
const sideInner = FACILITY_GEOMETRY.sideInner
|
|
31
35
|
const centerX = width / 2
|
|
32
36
|
const centerY = height / 2
|
|
33
37
|
const points = [point(left + FACILITY_GEOMETRY.corner, top)]
|
|
@@ -47,18 +51,18 @@ export function createFacilityPath({ width, height, topNotch = false, sessionNot
|
|
|
47
51
|
points.push(
|
|
48
52
|
point(right - FACILITY_GEOMETRY.corner, top),
|
|
49
53
|
point(right, top + FACILITY_GEOMETRY.corner),
|
|
50
|
-
point(right, centerY -
|
|
51
|
-
point(right -
|
|
52
|
-
point(right -
|
|
53
|
-
point(right, centerY +
|
|
54
|
+
point(right, centerY - sideOpening / 2),
|
|
55
|
+
point(right - sideDepth, centerY - sideInner / 2),
|
|
56
|
+
point(right - sideDepth, centerY + sideInner / 2),
|
|
57
|
+
point(right, centerY + sideOpening / 2),
|
|
54
58
|
point(right, bottom - FACILITY_GEOMETRY.corner),
|
|
55
59
|
point(right - FACILITY_GEOMETRY.corner, bottom),
|
|
56
60
|
point(left + FACILITY_GEOMETRY.corner, bottom),
|
|
57
61
|
point(left, bottom - FACILITY_GEOMETRY.corner),
|
|
58
|
-
point(left, centerY +
|
|
59
|
-
point(left +
|
|
60
|
-
point(left +
|
|
61
|
-
point(left, centerY -
|
|
62
|
+
point(left, centerY + sideOpening / 2),
|
|
63
|
+
point(left + sideDepth, centerY + sideInner / 2),
|
|
64
|
+
point(left + sideDepth, centerY - sideInner / 2),
|
|
65
|
+
point(left, centerY - sideOpening / 2),
|
|
62
66
|
point(left, top + FACILITY_GEOMETRY.corner),
|
|
63
67
|
)
|
|
64
68
|
|
|
@@ -279,7 +279,7 @@ export function renderFacilityGraphic(owner, { width, height } = {}) {
|
|
|
279
279
|
renderStates.set(graphic, state)
|
|
280
280
|
}
|
|
281
281
|
const { statusUnderlay, idleIndicator, silhouette, pickup, lifeline } = state
|
|
282
|
-
const path = createFacilityPath({ width, height, topNotch, sessionNotch })
|
|
282
|
+
const path = createFacilityPath({ width, height, topNotch, sessionNotch, spine: graphic.getAttribute('data-prts-facility-svg') === 'spine' })
|
|
283
283
|
graphic.setAttribute('viewBox', `0 0 ${Number(width.toFixed(3))} ${Number(height.toFixed(3))}`)
|
|
284
284
|
for (const layer of state.layers) layer.setAttribute('d', path)
|
|
285
285
|
statusUnderlay?.setAttribute('d', createFacilityPath({ width, height }))
|
package/src/client/index.js
CHANGED
|
@@ -100,6 +100,8 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
100
100
|
workspaces: contextService(ctx, 'workspaces'),
|
|
101
101
|
getSessionStatus: () => contextService(ctx, 'uiSession')?.sessionStatus,
|
|
102
102
|
getUiWorkspace: () => contextService(ctx, 'uiWorkspace'),
|
|
103
|
+
getSessions: () => contextService(ctx, 'sessions'),
|
|
104
|
+
getUiConversation: () => contextService(ctx, 'uiConversation'),
|
|
103
105
|
})
|
|
104
106
|
const floatingGlass = createFloatingGlassAdapter({ document, window })
|
|
105
107
|
const adapter = createRc7Adapter({ document })
|
|
@@ -257,7 +259,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
257
259
|
toBottom.start()
|
|
258
260
|
conversationControls.start()
|
|
259
261
|
layoutDiagnostics.start()
|
|
260
|
-
sidebarControls.start()
|
|
262
|
+
sidebarControls.start(preferences)
|
|
261
263
|
floatingGlass.start()
|
|
262
264
|
}
|
|
263
265
|
|
|
@@ -30,6 +30,12 @@ export const DEFAULT_PREFERENCES = Object.freeze({
|
|
|
30
30
|
particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
|
|
31
31
|
particlePattern: 'orthogonal',
|
|
32
32
|
railDefaultHidden: false,
|
|
33
|
+
sessionPreview: true,
|
|
34
|
+
previewOngoing: true,
|
|
35
|
+
previewWarning: true,
|
|
36
|
+
previewDone: true,
|
|
37
|
+
previewDoubleClick: true,
|
|
38
|
+
|
|
33
39
|
sessionFlow: true,
|
|
34
40
|
sessionGlow: true,
|
|
35
41
|
sessionFlowPalette: 'triad',
|
|
@@ -80,7 +86,7 @@ export const PREFERENCE_GROUPS = Object.freeze({
|
|
|
80
86
|
particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
|
|
81
87
|
material: Object.freeze(['glass']),
|
|
82
88
|
accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
|
|
83
|
-
navigation: Object.freeze(['railDefaultHidden']),
|
|
89
|
+
navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick']),
|
|
84
90
|
conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'conversationEmblem']),
|
|
85
91
|
})
|
|
86
92
|
|
|
@@ -197,6 +203,11 @@ export function normalizePreferences(value) {
|
|
|
197
203
|
sessionFlowColorCore: sessionFlowColors.core,
|
|
198
204
|
sessionFlowColorFront: sessionFlowColors.front,
|
|
199
205
|
sessionFlowSpeed: sessionFlowSpeedOr(input.sessionFlowSpeed, DEFAULT_PREFERENCES.sessionFlowSpeed),
|
|
206
|
+
sessionPreview: booleanOr(input.sessionPreview, DEFAULT_PREFERENCES.sessionPreview),
|
|
207
|
+
previewOngoing: booleanOr(input.previewOngoing, DEFAULT_PREFERENCES.previewOngoing),
|
|
208
|
+
previewWarning: booleanOr(input.previewWarning, DEFAULT_PREFERENCES.previewWarning),
|
|
209
|
+
previewDone: booleanOr(input.previewDone, DEFAULT_PREFERENCES.previewDone),
|
|
210
|
+
previewDoubleClick: booleanOr(input.previewDoubleClick, DEFAULT_PREFERENCES.previewDoubleClick),
|
|
200
211
|
conversationEmblem: booleanOr(input.conversationEmblem, DEFAULT_PREFERENCES.conversationEmblem),
|
|
201
212
|
bakerGroupAvatars: booleanOr(input.bakerGroupAvatars, DEFAULT_PREFERENCES.bakerGroupAvatars),
|
|
202
213
|
conversationStyle: enumOr(input.conversationStyle, CONVERSATION_STYLES, DEFAULT_PREFERENCES.conversationStyle),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import MarkdownIt from 'markdown-it'
|
|
2
|
+
|
|
3
|
+
let previewMarkdownParser
|
|
4
|
+
export function renderPreviewMarkdown(text) {
|
|
5
|
+
if (!previewMarkdownParser) {
|
|
6
|
+
previewMarkdownParser = new MarkdownIt({html:false,linkify:false,typographer:false})
|
|
7
|
+
// Previews never fetch remote images or render model-supplied HTML.
|
|
8
|
+
previewMarkdownParser.renderer.rules.image = (tokens,index) =>
|
|
9
|
+
previewMarkdownParser.utils.escapeHtml(tokens[index].content || '图片')
|
|
10
|
+
const link = previewMarkdownParser.renderer.rules.link_open
|
|
11
|
+
previewMarkdownParser.renderer.rules.link_open = (tokens,index,options,env,self) => {
|
|
12
|
+
tokens[index].attrSet('target','_blank')
|
|
13
|
+
tokens[index].attrSet('rel','noopener noreferrer')
|
|
14
|
+
return link ? link(tokens,index,options,env,self) : self.renderToken(tokens,index,options)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return previewMarkdownParser.render(text)
|
|
18
|
+
}
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { renderPreviewMarkdown } from './preview-markdown.js'
|
|
2
|
+
export function canAnswerPreview(pending) {
|
|
3
|
+
if (pending?.kind === 'approval') return pending.answerable === true && typeof pending.answer === 'function'
|
|
4
|
+
const question = pending?.questions?.[0]
|
|
5
|
+
return pending?.kind === 'question' && pending.questions.length === 1
|
|
6
|
+
&& typeof pending.answer === 'function' && typeof question?.id === 'string'
|
|
7
|
+
&& !question.intent && !question.detail && question.options?.length > 0
|
|
8
|
+
&& !pending.getSnapshot?.().closed
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Pending choices are separate from assistant prose in DSH.
|
|
12
|
+
export function pendingQuestionPreview(pending) {
|
|
13
|
+
const questions = pending.questions ?? []
|
|
14
|
+
const text = questions.slice(0, 2).map(item => {
|
|
15
|
+
const question = (item.question ?? '').slice(0, 80)
|
|
16
|
+
const options = (item.options ?? []).slice(0, 4).map(option => option.label).filter(Boolean).join(' / ')
|
|
17
|
+
return [question, options].filter(Boolean).join(':')
|
|
18
|
+
}).join(';')
|
|
19
|
+
return text ? text.slice(0, 240) : '有待回答的问题,请打开会话查看'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Read the host's assembled Chat view rather than interpreting raw stream events.
|
|
23
|
+
export function latestAssistantReply(snapshot) {
|
|
24
|
+
const order = snapshot?.order ?? []
|
|
25
|
+
const latestTurn = snapshot?.timeline?.turnOrder?.at(-1)
|
|
26
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
27
|
+
const node = snapshot.nodes?.get?.(order[i])
|
|
28
|
+
// Never search past a new user input, even before turn/start arrives.
|
|
29
|
+
if (['user', 'steering', 'turn-trigger'].includes(node?.kind)) return ''
|
|
30
|
+
if (latestTurn !== undefined && node?.location?.turn?.turn !== latestTurn) continue
|
|
31
|
+
if (node?.kind !== 'assistant-step' || node.visibility === 'hidden') continue
|
|
32
|
+
let text = ''
|
|
33
|
+
for (const block of node.data?.blocks ?? []) {
|
|
34
|
+
if (block.kind === 'text') text += block.text ?? ''
|
|
35
|
+
}
|
|
36
|
+
text = text.trim()
|
|
37
|
+
if (text) return text
|
|
38
|
+
}
|
|
39
|
+
return ''
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function completedPreviewReplies(snapshot) {
|
|
43
|
+
const replies = []
|
|
44
|
+
const latestTurn = snapshot?.timeline?.turnOrder?.at(-1)
|
|
45
|
+
for (const key of [...(snapshot?.order ?? [])].reverse()) {
|
|
46
|
+
const node = snapshot.nodes?.get?.(key)
|
|
47
|
+
if (['user', 'steering', 'turn-trigger'].includes(node?.kind)) break
|
|
48
|
+
if (latestTurn !== undefined && node?.location?.turn?.turn !== latestTurn) continue
|
|
49
|
+
if (node?.kind !== 'assistant-step' || node.visibility === 'hidden' || node.data?.status !== 'settled') continue
|
|
50
|
+
const text = (node.data.blocks ?? []).filter(block => block.kind === 'text').map(block => block.text ?? '').join('').trim()
|
|
51
|
+
if (text) replies.push({key, text})
|
|
52
|
+
}
|
|
53
|
+
return replies.reverse()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createSessionSummaryPreview({ document, window, sessions, getSessions, getUiConversation, getSessionStatus, openSession, getPreviewPreferences }) {
|
|
57
|
+
let active
|
|
58
|
+
function stop(force = true) {
|
|
59
|
+
if (!force && active) return
|
|
60
|
+
const old = active
|
|
61
|
+
active = undefined
|
|
62
|
+
if (!old) return
|
|
63
|
+
window.clearTimeout(old.leaveTimer)
|
|
64
|
+
window.clearTimeout(old.delay)
|
|
65
|
+
window.clearTimeout(old.refresh)
|
|
66
|
+
old.unsubscribe?.()
|
|
67
|
+
old.unsubscribeStatus?.()
|
|
68
|
+
old.reference?.release()
|
|
69
|
+
old.panel.hidePopover?.()
|
|
70
|
+
old.panel.hidden = true
|
|
71
|
+
old.panel.removeAttribute('data-prts-preview-editing')
|
|
72
|
+
old.panel.setAttribute('role', 'note')
|
|
73
|
+
old.cleanupEditor?.()
|
|
74
|
+
old.cleanupHover?.()
|
|
75
|
+
old.detail?.hidePopover?.()
|
|
76
|
+
old.detail?.remove()
|
|
77
|
+
old.group?.removeAttribute('data-prts-preview-open')
|
|
78
|
+
old.anchor?.removeAttribute('data-prts-preview-active')
|
|
79
|
+
if (old.detached) {
|
|
80
|
+
old.panel.remove()
|
|
81
|
+
if (old.originalParent?.isConnected) old.originalParent.append(old.panel)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function show(id, panel, title, anchor) {
|
|
85
|
+
if (active?.sticky && active.id !== id) return
|
|
86
|
+
if (active?.id === id) {
|
|
87
|
+
window.clearTimeout(active.leaveTimer)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
stop()
|
|
91
|
+
const current = { id, panel, anchor, originalParent: panel.parentElement,
|
|
92
|
+
group: anchor?.closest('[data-prts-session-summary-group]') }
|
|
93
|
+
current.group?.setAttribute('data-prts-preview-open', '')
|
|
94
|
+
anchor?.setAttribute('data-prts-preview-active', '')
|
|
95
|
+
active = current
|
|
96
|
+
const keep = () => window.clearTimeout(current.leaveTimer)
|
|
97
|
+
const onDoubleClick = event => {
|
|
98
|
+
if (active !== current || event.button !== 0 || getPreviewPreferences?.().previewDoubleClick === false) return
|
|
99
|
+
if (event.target.closest?.('button, a, input, textarea, select, label, summary, [role="button"], [role="link"], [contenteditable]:not([contenteditable="false"])')) return
|
|
100
|
+
event.preventDefault()
|
|
101
|
+
event.stopPropagation()
|
|
102
|
+
stop()
|
|
103
|
+
openSession?.(id)
|
|
104
|
+
}
|
|
105
|
+
const dismissSticky = event => {
|
|
106
|
+
if (!current.sticky) return
|
|
107
|
+
if (event.type === 'keydown') {
|
|
108
|
+
if (event.key !== 'Escape') return
|
|
109
|
+
event.preventDefault()
|
|
110
|
+
} else if (panel.contains(event.target) || current.anchor?.contains(event.target)) return
|
|
111
|
+
stop()
|
|
112
|
+
}
|
|
113
|
+
panel.ownerDocument.addEventListener('pointerdown', dismissSticky)
|
|
114
|
+
panel.ownerDocument.addEventListener('keydown', dismissSticky)
|
|
115
|
+
panel.addEventListener('dblclick', onDoubleClick)
|
|
116
|
+
panel.addEventListener('pointerenter', keep)
|
|
117
|
+
panel.addEventListener('pointerleave', leave)
|
|
118
|
+
current.cleanupHover = () => {
|
|
119
|
+
panel.ownerDocument.removeEventListener('pointerdown', dismissSticky)
|
|
120
|
+
panel.ownerDocument.removeEventListener('keydown', dismissSticky)
|
|
121
|
+
panel.removeEventListener('dblclick', onDoubleClick)
|
|
122
|
+
panel.removeEventListener('pointerenter', keep)
|
|
123
|
+
panel.removeEventListener('pointerleave', leave)
|
|
124
|
+
}
|
|
125
|
+
panel.removeAttribute('data-prts-preview-editing')
|
|
126
|
+
panel.setAttribute('role', 'note')
|
|
127
|
+
panel.hidden = false
|
|
128
|
+
if (anchor) {
|
|
129
|
+
const list = anchor.closest('[data-prts-session-summary-popover]').getBoundingClientRect()
|
|
130
|
+
const row = anchor.getBoundingClientRect()
|
|
131
|
+
panel.style.left = (list.right + 10) + 'px'
|
|
132
|
+
panel.style.top = Math.max(8, Math.min(row.top, window.innerHeight - 230)) + 'px'
|
|
133
|
+
}
|
|
134
|
+
panel.ownerDocument.body.append(panel)
|
|
135
|
+
current.detached = true
|
|
136
|
+
panel.showPopover?.()
|
|
137
|
+
const label = panel.querySelector('strong')
|
|
138
|
+
let body = panel.querySelector('[data-prts-preview-body]')
|
|
139
|
+
if (!body) {
|
|
140
|
+
body = panel.ownerDocument.createElement('div')
|
|
141
|
+
body.setAttribute('data-prts-preview-body', '')
|
|
142
|
+
panel.querySelector('p').replaceWith(body)
|
|
143
|
+
}
|
|
144
|
+
const choices = panel.ownerDocument.createElement('div')
|
|
145
|
+
choices.setAttribute('data-prts-preview-choices', '')
|
|
146
|
+
panel.querySelector('[data-prts-preview-choices]')?.remove()
|
|
147
|
+
panel.append(choices)
|
|
148
|
+
panel.querySelector('[data-prts-preview-actions]')?.remove()
|
|
149
|
+
const actions = panel.ownerDocument.createElement('div')
|
|
150
|
+
actions.setAttribute('data-prts-preview-actions', '')
|
|
151
|
+
panel.append(actions)
|
|
152
|
+
const pendingNow = () => getSessionStatus?.()?.getSnapshot().get(id)?.pendingInteraction
|
|
153
|
+
const button = (text, action) => {
|
|
154
|
+
const el = panel.ownerDocument.createElement('button')
|
|
155
|
+
el.type = 'button'
|
|
156
|
+
el.textContent = text
|
|
157
|
+
el.addEventListener('click', action)
|
|
158
|
+
return el
|
|
159
|
+
}
|
|
160
|
+
const closeSticky = button('关闭预览', () => stop())
|
|
161
|
+
const enter = button('进入会话', () => { stop(); openSession?.(id) })
|
|
162
|
+
const enableAnswer = () => {
|
|
163
|
+
const pending = pendingNow()
|
|
164
|
+
if (!canAnswerPreview(pending)) return
|
|
165
|
+
current.editor = true
|
|
166
|
+
// Detach from the hover menu before promoting again to the top layer.
|
|
167
|
+
panel.setAttribute('data-prts-preview-editing', '')
|
|
168
|
+
panel.setAttribute('role', 'dialog')
|
|
169
|
+
|
|
170
|
+
const approval = pending.kind === 'approval'
|
|
171
|
+
const question = approval ? {
|
|
172
|
+
options: [{label:'仅允许本次'}, {label:'拒绝'}]
|
|
173
|
+
} : pending.questions[0]
|
|
174
|
+
const submitLabel = approval ? '提交审批' : '提交回答'
|
|
175
|
+
const selected = new Set()
|
|
176
|
+
let busy = false
|
|
177
|
+
const message = panel.ownerDocument.createElement('small')
|
|
178
|
+
message.setAttribute('role', 'status')
|
|
179
|
+
const submit = button(submitLabel, async () => {
|
|
180
|
+
if (busy || !selected.size) return
|
|
181
|
+
if (pendingNow() !== pending || pending.getSnapshot?.().closed || (approval && !pending.answerable)) {
|
|
182
|
+
message.textContent = '问题状态已变化,请重新打开预览。'
|
|
183
|
+
submit.disabled = true
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
busy = true
|
|
187
|
+
submit.disabled = true
|
|
188
|
+
submit.textContent = '正在提交…'
|
|
189
|
+
try {
|
|
190
|
+
const channel = pending.getSnapshot?.().channel
|
|
191
|
+
await pending.answer(approval
|
|
192
|
+
? selected.has('仅允许本次') ? 'allowed-once' : 'rejected'
|
|
193
|
+
: {answers:[{id:question.id,selected:question.options.map(o=>o.label).filter(value=>selected.has(value))}]})
|
|
194
|
+
if (active === current) {
|
|
195
|
+
current.sticky = true
|
|
196
|
+
keep()
|
|
197
|
+
current.answeredPending = pending
|
|
198
|
+
current.cleanupEditor?.()
|
|
199
|
+
current.cleanupEditor = undefined
|
|
200
|
+
current.validate = undefined
|
|
201
|
+
current.editor = false
|
|
202
|
+
panel.removeAttribute('data-prts-preview-editing')
|
|
203
|
+
panel.setAttribute('role', 'note')
|
|
204
|
+
render()
|
|
205
|
+
}
|
|
206
|
+
if (channel === 'rpc') await pending.dismiss?.()
|
|
207
|
+
} catch {
|
|
208
|
+
if (active !== current) return
|
|
209
|
+
busy = false
|
|
210
|
+
submit.disabled = false
|
|
211
|
+
submit.textContent = submitLabel
|
|
212
|
+
message.textContent = '提交失败,请重试或进入会话处理。'
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
submit.disabled = true
|
|
216
|
+
choices.replaceChildren(...question.options.map(option => {
|
|
217
|
+
const choice = button(option.label, () => {
|
|
218
|
+
if (busy) return
|
|
219
|
+
if (question.multiSelect) {
|
|
220
|
+
if (selected.has(option.label)) selected.delete(option.label)
|
|
221
|
+
else selected.add(option.label)
|
|
222
|
+
} else {
|
|
223
|
+
selected.clear()
|
|
224
|
+
selected.add(option.label)
|
|
225
|
+
}
|
|
226
|
+
pending.engage?.()
|
|
227
|
+
for (const el of choices.children) el.setAttribute('aria-pressed', String(selected.has(el.dataset.value)))
|
|
228
|
+
submit.disabled = !selected.size
|
|
229
|
+
})
|
|
230
|
+
choice.dataset.value = option.label
|
|
231
|
+
choice.setAttribute('aria-pressed','false')
|
|
232
|
+
if (option.description) {
|
|
233
|
+
const detail = panel.ownerDocument.createElement('small')
|
|
234
|
+
detail.textContent = option.description
|
|
235
|
+
choice.append(detail)
|
|
236
|
+
}
|
|
237
|
+
return choice
|
|
238
|
+
}))
|
|
239
|
+
choices.hidden = false
|
|
240
|
+
actions.replaceChildren(button('取消', () => stop()), enter, submit, message)
|
|
241
|
+
const onKey = event => { if (event.key === 'Escape') { event.preventDefault(); stop() } }
|
|
242
|
+
panel.addEventListener('keydown', onKey)
|
|
243
|
+
const onEnter = () => pending.holdFocus?.()
|
|
244
|
+
const onOutside = event => { if (!panel.contains(event.target) && !anchor?.contains(event.target)) stop() }
|
|
245
|
+
panel.addEventListener('pointerenter', onEnter)
|
|
246
|
+
panel.ownerDocument.addEventListener('pointerdown', onOutside)
|
|
247
|
+
current.cleanupEditor = () => {
|
|
248
|
+
pending.releaseFocus?.()
|
|
249
|
+
panel.removeEventListener('keydown', onKey)
|
|
250
|
+
panel.removeEventListener('pointerenter', onEnter)
|
|
251
|
+
panel.ownerDocument.removeEventListener('pointerdown', onOutside)
|
|
252
|
+
}
|
|
253
|
+
current.validate = () => {
|
|
254
|
+
if (!busy && (pendingNow() !== pending || (approval && !pending.answerable))) {
|
|
255
|
+
message.textContent = '问题已更新或结束,请进入会话查看。'
|
|
256
|
+
submit.disabled = true
|
|
257
|
+
for (const el of choices.children) el.disabled = true
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let lastChoices = ''
|
|
262
|
+
let source
|
|
263
|
+
let previousText
|
|
264
|
+
let previousQuestion
|
|
265
|
+
let historyKeys = []
|
|
266
|
+
let detailKey
|
|
267
|
+
let detailEntry
|
|
268
|
+
const hideDetail = () => {
|
|
269
|
+
current.detail?.hidePopover?.()
|
|
270
|
+
if (current.detail) current.detail.hidden = true
|
|
271
|
+
detailKey = undefined
|
|
272
|
+
detailEntry?.removeAttribute('data-prts-reply-active')
|
|
273
|
+
detailEntry = undefined
|
|
274
|
+
}
|
|
275
|
+
const showDetail = (reply, index, item) => {
|
|
276
|
+
keep()
|
|
277
|
+
if (detailKey === reply.key && current.detail && !current.detail.hidden) return
|
|
278
|
+
if (!current.detail) {
|
|
279
|
+
const detail = panel.ownerDocument.createElement('section')
|
|
280
|
+
detail.setAttribute('data-prts-summary-reply', '')
|
|
281
|
+
detail.setAttribute('data-prts-reply-detail', '')
|
|
282
|
+
detail.setAttribute('popover', 'manual')
|
|
283
|
+
detail.setAttribute('role', 'note')
|
|
284
|
+
const heading = panel.ownerDocument.createElement('strong')
|
|
285
|
+
const content = panel.ownerDocument.createElement('div')
|
|
286
|
+
content.setAttribute('data-prts-preview-body', '')
|
|
287
|
+
detail.append(heading, content)
|
|
288
|
+
panel.append(detail)
|
|
289
|
+
current.detail = detail
|
|
290
|
+
}
|
|
291
|
+
const detail = current.detail
|
|
292
|
+
detailKey = reply.key
|
|
293
|
+
detailEntry?.removeAttribute('data-prts-reply-active')
|
|
294
|
+
detailEntry = item
|
|
295
|
+
item?.setAttribute('data-prts-reply-active', '')
|
|
296
|
+
detail.querySelector('strong').textContent = '回复 ' + (index + 1)
|
|
297
|
+
const content = detail.querySelector('[data-prts-preview-body]')
|
|
298
|
+
content.innerHTML = renderPreviewMarkdown(reply.text)
|
|
299
|
+
content.scrollTop = 0
|
|
300
|
+
detail.hidden = false
|
|
301
|
+
const rect = panel.getBoundingClientRect()
|
|
302
|
+
const width = Math.min(340, window.innerWidth - 16)
|
|
303
|
+
detail.style.width = width + 'px'
|
|
304
|
+
const beside = rect.right + 10 + width <= window.innerWidth - 8
|
|
305
|
+
detail.style.left = (beside ? rect.right + 10 : Math.max(8, Math.min(rect.left, window.innerWidth - width - 8))) + 'px'
|
|
306
|
+
detail.style.top = Math.max(8, Math.min(beside ? rect.top : rect.bottom + 8, window.innerHeight - 240)) + 'px'
|
|
307
|
+
detail.showPopover?.()
|
|
308
|
+
}
|
|
309
|
+
const render = () => {
|
|
310
|
+
if (active !== current) return
|
|
311
|
+
if (current.editor) { current.validate?.(); return }
|
|
312
|
+
const status = getSessionStatus?.()?.getSnapshot().get(id)
|
|
313
|
+
const pending = status?.pendingInteraction === current.answeredPending
|
|
314
|
+
? undefined : status?.pendingInteraction
|
|
315
|
+
const approval = pending?.kind === 'approval'
|
|
316
|
+
const question = approval || pending?.kind === 'question' || pending?.kind === 'plan-review'
|
|
317
|
+
const state = question ? 'warning' : status?.running || (current.answeredPending && status?.pendingInteraction === current.answeredPending) ? 'ongoing'
|
|
318
|
+
: status?.completionUnread || status?.running === false ? 'done' : current.group?.getAttribute('data-prts-session-summary-group') || ''
|
|
319
|
+
if (current.state && current.state !== state) {
|
|
320
|
+
current.sticky = true
|
|
321
|
+
keep()
|
|
322
|
+
}
|
|
323
|
+
current.state = state
|
|
324
|
+
panel.setAttribute('data-prts-preview-state', state)
|
|
325
|
+
label.textContent = approval ? '等待审批 · 仅本次授权' : question ? '等待你的选择' : state === 'ongoing' ? '运行中 · 已完成正文' : state === 'done' ? '已完成 · 最后回复' : '最新 AI 回复'
|
|
326
|
+
panel.toggleAttribute('data-prts-preview-question', question)
|
|
327
|
+
if (question) {
|
|
328
|
+
if (!actions.contains(enter)) actions.replaceChildren(enter)
|
|
329
|
+
} else if (current.sticky) {
|
|
330
|
+
if (!actions.contains(closeSticky)) actions.replaceChildren(closeSticky)
|
|
331
|
+
} else actions.replaceChildren()
|
|
332
|
+
const first = question ? pending.questions?.[0] : undefined
|
|
333
|
+
const labels = (first?.options ?? []).slice(0, 3).map(option => option.label)
|
|
334
|
+
const remaining = Math.max(0, (first?.options?.length ?? 0) - 3)
|
|
335
|
+
if (remaining) labels.push('另有 ' + remaining + ' 个选项')
|
|
336
|
+
if ((pending?.questions?.length ?? 0) > 1) labels.push('共 ' + pending.questions.length + ' 个问题')
|
|
337
|
+
const signature = JSON.stringify(labels)
|
|
338
|
+
if (signature !== lastChoices) {
|
|
339
|
+
lastChoices = signature
|
|
340
|
+
choices.replaceChildren(...labels.map(text => {
|
|
341
|
+
const option = panel.ownerDocument.createElement('span')
|
|
342
|
+
option.textContent = text
|
|
343
|
+
return option
|
|
344
|
+
}))
|
|
345
|
+
}
|
|
346
|
+
choices.hidden = !labels.length
|
|
347
|
+
panel.setAttribute('aria-label', title + ':' + label.textContent)
|
|
348
|
+
if (state === 'ongoing' && source) {
|
|
349
|
+
const replies = completedPreviewReplies(source.getSnapshot())
|
|
350
|
+
panel.toggleAttribute('data-prts-preview-empty', !replies.length)
|
|
351
|
+
const extendsHistory = historyKeys.length > 0 && historyKeys.every((entry,index) => replies[index]?.key === entry.key && replies[index]?.text === entry.text)
|
|
352
|
+
if (!extendsHistory) {
|
|
353
|
+
hideDetail()
|
|
354
|
+
body.replaceChildren()
|
|
355
|
+
body.scrollTop = 0
|
|
356
|
+
historyKeys = []
|
|
357
|
+
}
|
|
358
|
+
if (!replies.length) body.textContent = '正在处理,等待本轮正文输出完成…'
|
|
359
|
+
const previousHeight = body.scrollHeight
|
|
360
|
+
const previousTop = body.scrollTop
|
|
361
|
+
for (let index = historyKeys.length; index < replies.length; index++) {
|
|
362
|
+
const entry = panel.ownerDocument.createElement('article')
|
|
363
|
+
entry.setAttribute('data-prts-preview-message', '')
|
|
364
|
+
const item = panel.ownerDocument.createElement('button')
|
|
365
|
+
item.type = 'button'
|
|
366
|
+
item.setAttribute('data-prts-reply-entry', '')
|
|
367
|
+
const heading = panel.ownerDocument.createElement('small')
|
|
368
|
+
heading.textContent = '回复 ' + (index + 1)
|
|
369
|
+
const summary = panel.ownerDocument.createElement('span')
|
|
370
|
+
const plain = panel.ownerDocument.createElement('div')
|
|
371
|
+
plain.innerHTML = renderPreviewMarkdown(replies[index].text)
|
|
372
|
+
summary.textContent = plain.textContent.replace(/\s+/g, ' ').trim().slice(0, 100)
|
|
373
|
+
item.append(heading, summary)
|
|
374
|
+
item.addEventListener('pointermove', () => showDetail(replies[index], index, item))
|
|
375
|
+
item.addEventListener('focus', () => showDetail(replies[index], index, item))
|
|
376
|
+
item.addEventListener('click', () => showDetail(replies[index], index, item))
|
|
377
|
+
entry.append(item)
|
|
378
|
+
body.prepend(entry)
|
|
379
|
+
}
|
|
380
|
+
if (previousTop > 0) body.scrollTop = previousTop + body.scrollHeight - previousHeight
|
|
381
|
+
else body.scrollTop = 0
|
|
382
|
+
historyKeys = replies
|
|
383
|
+
previousText = undefined
|
|
384
|
+
previousQuestion = undefined
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
hideDetail()
|
|
388
|
+
historyKeys = []
|
|
389
|
+
const excerpt = source ? latestAssistantReply(source.getSnapshot()) : ''
|
|
390
|
+
panel.toggleAttribute('data-prts-preview-empty', !question && !excerpt)
|
|
391
|
+
const text = question
|
|
392
|
+
? approval
|
|
393
|
+
? [pending.toolName, pending.displayReason?.['zh-CN'] || pending.displayReason?.zh || pending.reason || pending.displayReason?.en || '请进入会话查看操作详情'].filter(Boolean).join(':')
|
|
394
|
+
: first?.question || pendingQuestionPreview(pending)
|
|
395
|
+
: source ? excerpt || (state === 'ongoing' ? '正在处理,尚未输出正文…' : '暂无正文回复,可进入会话查看执行记录。') : '正在读取…'
|
|
396
|
+
if (previousText !== text || previousQuestion !== question) {
|
|
397
|
+
const extendsReply = previousQuestion === question && previousText && text.startsWith(previousText)
|
|
398
|
+
if (question) body.textContent = text
|
|
399
|
+
else body.innerHTML = renderPreviewMarkdown(text)
|
|
400
|
+
previousText = text
|
|
401
|
+
previousQuestion = question
|
|
402
|
+
if (!extendsReply) body.scrollTop = 0
|
|
403
|
+
}
|
|
404
|
+
if (canAnswerPreview(pending)) enableAnswer()
|
|
405
|
+
}
|
|
406
|
+
current.unsubscribeStatus = getSessionStatus?.()?.subscribe(render)
|
|
407
|
+
render()
|
|
408
|
+
body.scrollTop = 0
|
|
409
|
+
current.delay = window.setTimeout(async () => {
|
|
410
|
+
try {
|
|
411
|
+
const service = getUiConversation?.()
|
|
412
|
+
const controller = getSessions?.() ?? sessions
|
|
413
|
+
if (!controller?.retain || !service?.binding) throw new Error('unavailable')
|
|
414
|
+
current.reference = controller.retain(id, { source: 'prtsSummaryPreview' })
|
|
415
|
+
const binding = await current.reference.ready
|
|
416
|
+
if (active !== current) return
|
|
417
|
+
source = service.binding(binding).target('chat')
|
|
418
|
+
current.unsubscribe = source.subscribe(() => {
|
|
419
|
+
if (active !== current || current.refresh !== undefined) return
|
|
420
|
+
current.refresh = window.setTimeout(() => {
|
|
421
|
+
current.refresh = undefined
|
|
422
|
+
render()
|
|
423
|
+
}, 120)
|
|
424
|
+
})
|
|
425
|
+
render()
|
|
426
|
+
} catch {
|
|
427
|
+
if (active !== current) return
|
|
428
|
+
current.unsubscribe?.()
|
|
429
|
+
current.unsubscribe = undefined
|
|
430
|
+
current.reference?.release()
|
|
431
|
+
current.reference = undefined
|
|
432
|
+
render()
|
|
433
|
+
if (body.textContent.trim() === '正在读取…') body.textContent = '暂时无法读取回复,可点击会话查看'
|
|
434
|
+
}
|
|
435
|
+
}, 80)
|
|
436
|
+
}
|
|
437
|
+
function leave() {
|
|
438
|
+
if (!active || active.sticky) return
|
|
439
|
+
const current = active
|
|
440
|
+
window.clearTimeout(current.leaveTimer)
|
|
441
|
+
current.leaveTimer = window.setTimeout(() => {
|
|
442
|
+
if (active === current && !current.panel.matches(':hover')) stop()
|
|
443
|
+
}, 300)
|
|
444
|
+
}
|
|
445
|
+
function reanchor(root) {
|
|
446
|
+
if (!active) return
|
|
447
|
+
const anchor = [...root.querySelectorAll('[data-prts-session-summary-item]')]
|
|
448
|
+
.find(item => item.getAttribute('data-prts-session-summary-item') === active.id)
|
|
449
|
+
active.group?.removeAttribute('data-prts-preview-open')
|
|
450
|
+
active.anchor?.removeAttribute('data-prts-preview-active')
|
|
451
|
+
active.anchor = anchor
|
|
452
|
+
active.group = anchor?.closest('[data-prts-session-summary-group]')
|
|
453
|
+
active.originalParent = undefined
|
|
454
|
+
active.group?.setAttribute('data-prts-preview-open', '')
|
|
455
|
+
anchor?.setAttribute('data-prts-preview-active', '')
|
|
456
|
+
window.clearTimeout(active.leaveTimer)
|
|
457
|
+
}
|
|
458
|
+
return { show, stop, leave, reanchor }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function scrollSummaryTitle(button, viewport, text, window) {
|
|
462
|
+
let animation
|
|
463
|
+
const stop = () => { animation?.cancel(); animation = undefined }
|
|
464
|
+
const start = () => {
|
|
465
|
+
stop()
|
|
466
|
+
const distance = text.scrollWidth - viewport.clientWidth
|
|
467
|
+
if (distance <= 8 || !text.animate) return
|
|
468
|
+
const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
469
|
+
animation = text.animate([
|
|
470
|
+
{ transform: 'translateX(0)' },
|
|
471
|
+
{ transform: `translateX(-${distance}px)` },
|
|
472
|
+
], { duration: reduced ? 0 : distance / 30 * 1000, fill: 'forwards', easing: 'linear' })
|
|
473
|
+
}
|
|
474
|
+
button.addEventListener('pointerenter', start)
|
|
475
|
+
button.addEventListener('pointerleave', stop)
|
|
476
|
+
button.addEventListener('focus', start)
|
|
477
|
+
button.addEventListener('blur', stop)
|
|
478
|
+
return stop
|
|
479
|
+
}
|