@cyber-dash-tech/revela 0.18.3 → 0.18.5
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/README.md +7 -5
- package/README.zh-CN.md +7 -5
- package/lib/commands/refine.ts +1 -1
- package/lib/decks-state.ts +1 -0
- package/lib/document-materials/extract.ts +25 -20
- package/lib/material-intake.ts +9 -4
- package/lib/narrative-vault/constants.ts +1 -1
- package/lib/narrative-vault/paths.ts +7 -2
- package/lib/refine/comment-requests.ts +1 -1
- package/lib/refine/prompt-bridge.ts +94 -25
- package/lib/refine/review-comments.ts +203 -0
- package/lib/refine/server.ts +1073 -216
- package/lib/runtime/index.ts +3 -2
- package/lib/workspace-meta.ts +32 -0
- package/package.json +1 -1
- package/plugin.ts +4 -3
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/hooks/revela_guard.ts +2 -2
- package/plugins/revela/mcp/revela-server.ts +1 -1
- package/plugins/revela/skills/revela-export/SKILL.md +30 -1
- package/plugins/revela/skills/revela-helper/SKILL.md +48 -0
- package/plugins/revela/skills/revela-make-deck/SKILL.md +93 -15
- package/plugins/revela/skills/revela-research/SKILL.md +57 -15
- package/plugins/revela/skills/{revela-review-deck → revela-review}/SKILL.md +28 -7
- package/tools/workspace-scan.ts +1 -1
- package/plugins/revela/skills/revela-design/SKILL.md +0 -46
- package/plugins/revela/skills/revela-domain/SKILL.md +0 -30
- package/plugins/revela/skills/revela-init/SKILL.md +0 -31
- package/plugins/revela/skills/revela-upgrade/SKILL.md +0 -33
package/lib/refine/server.ts
CHANGED
|
@@ -16,13 +16,20 @@ import type { MediaAssetRecord, MediaPurpose } from "../media/types"
|
|
|
16
16
|
import { addCommentRequestEvent, completeCommentRequest, createCommentRequest, failCommentRequest, getCommentRequest, subscribeCommentRequestEvents } from "./comment-requests"
|
|
17
17
|
import { createOpenCodeReviewPromptBridge, type ReviewPromptBridge } from "./prompt-bridge"
|
|
18
18
|
import { suppressReviewApplyFixArtifactQa } from "./qa-suppression"
|
|
19
|
+
import { createReviewComment, deleteReviewComment, listReviewComments, markReviewCommentApplied, markReviewCommentApplying, markReviewCommentFailed, markReviewCommentQueued, markReviewCommentStopped, readReviewComment, type ReviewCommentRecord } from "./review-comments"
|
|
19
20
|
import { annotateVisualEditTargets, applyVisualTargetChanges, type VisualEditTarget } from "./visual-targets"
|
|
20
21
|
|
|
21
22
|
const TOKEN_BYTES = 24
|
|
22
23
|
const SESSION_TTL_MS = 2 * 60 * 60 * 1000
|
|
23
24
|
const IDLE_STOP_MS = 30 * 60 * 1000
|
|
25
|
+
export const REVIEW_REF_LABEL_MAX_DISPLAY_CHARS = 32
|
|
24
26
|
export const LIVE_EDITOR_IDLE_MS = 10 * 1000
|
|
25
27
|
|
|
28
|
+
export function displayReviewReferenceLabel(label: string): string {
|
|
29
|
+
const text = String(label || "")
|
|
30
|
+
return text.length > REVIEW_REF_LABEL_MAX_DISPLAY_CHARS ? text.slice(0, REVIEW_REF_LABEL_MAX_DISPLAY_CHARS - 1) + "…" : text
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
interface EditAsset {
|
|
27
34
|
id: string
|
|
28
35
|
absoluteFile: string
|
|
@@ -45,11 +52,27 @@ interface EditSession {
|
|
|
45
52
|
defaultMode: RefineMode
|
|
46
53
|
visualTargets: Map<string, VisualEditTarget>
|
|
47
54
|
visualTargetDeckVersion?: string
|
|
55
|
+
activeApplyCommentId?: string
|
|
56
|
+
applyQueue: string[]
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
export type RefineMode = "edit" | "inspect"
|
|
51
60
|
export type ReviewShellSurface = "legacy" | "codex"
|
|
52
61
|
|
|
62
|
+
function lucideIcon(name: "image" | "list" | "play" | "plus" | "refresh-cw" | "send" | "square" | "trash-2", className = "composer-icon"): string {
|
|
63
|
+
const paths: Record<typeof name, string> = {
|
|
64
|
+
image: '<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>',
|
|
65
|
+
list: '<path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/>',
|
|
66
|
+
play: '<polygon points="6 3 20 12 6 21 6 3"/>',
|
|
67
|
+
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
|
68
|
+
"refresh-cw": '<path d="M3 12a9 9 0 0 1 15.5-6.2L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15.5 6.2L3 16"/><path d="M3 21v-5h5"/>',
|
|
69
|
+
send: '<path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/>',
|
|
70
|
+
square: '<rect width="14" height="14" x="5" y="5" rx="2"/>',
|
|
71
|
+
"trash-2": '<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/>',
|
|
72
|
+
}
|
|
73
|
+
return `<svg class="lucide lucide-${name} ${className}" data-lucide="${name}" viewBox="0 0 24 24" aria-hidden="true">${paths[name]}</svg>`
|
|
74
|
+
}
|
|
75
|
+
|
|
53
76
|
export interface RefineServerHandle {
|
|
54
77
|
baseUrl: string
|
|
55
78
|
getOrCreateSession(input: {
|
|
@@ -98,6 +121,7 @@ export function startRefineServer(): RefineServerHandle {
|
|
|
98
121
|
existing.session.workspaceRoot = resolve(input.workspaceRoot)
|
|
99
122
|
existing.session.defaultMode = input.mode ?? "edit"
|
|
100
123
|
existing.session.visualTargets = existing.session.visualTargets ?? new Map()
|
|
124
|
+
existing.session.applyQueue = existing.session.applyQueue ?? []
|
|
101
125
|
return {
|
|
102
126
|
token: existing.token,
|
|
103
127
|
reused: true,
|
|
@@ -122,6 +146,7 @@ export function startRefineServer(): RefineServerHandle {
|
|
|
122
146
|
lastActiveAt: Date.now(),
|
|
123
147
|
defaultMode: input.mode ?? "edit",
|
|
124
148
|
visualTargets: new Map(),
|
|
149
|
+
applyQueue: [],
|
|
125
150
|
})
|
|
126
151
|
return { token, reused: false, live: false }
|
|
127
152
|
},
|
|
@@ -202,6 +227,39 @@ async function handleRequest(req: Request): Promise<Response> {
|
|
|
202
227
|
return handleComment(req, session.value)
|
|
203
228
|
}
|
|
204
229
|
|
|
230
|
+
if (url.pathname === "/api/comments" && req.method === "GET") {
|
|
231
|
+
const session = validateSession(url.searchParams.get("token"))
|
|
232
|
+
if (!session.ok) return session.response
|
|
233
|
+
return handleReviewCommentsList(session.value)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (url.pathname === "/api/comments" && req.method === "POST") {
|
|
237
|
+
const session = validateSession(url.searchParams.get("token"))
|
|
238
|
+
if (!session.ok) return session.response
|
|
239
|
+
return handleReviewCommentCreate(req, session.value)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const applyMatch = url.pathname.match(/^\/api\/comments\/([^/]+)\/apply$/)
|
|
243
|
+
if (applyMatch && req.method === "POST") {
|
|
244
|
+
const session = validateSession(url.searchParams.get("token"))
|
|
245
|
+
if (!session.ok) return session.response
|
|
246
|
+
return handleReviewCommentApply(decodeURIComponent(applyMatch[1]), req, session.value)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const stopMatch = url.pathname.match(/^\/api\/comments\/([^/]+)\/stop$/)
|
|
250
|
+
if (stopMatch && req.method === "POST") {
|
|
251
|
+
const session = validateSession(url.searchParams.get("token"))
|
|
252
|
+
if (!session.ok) return session.response
|
|
253
|
+
return handleReviewCommentStop(decodeURIComponent(stopMatch[1]), session.value)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const deleteMatch = url.pathname.match(/^\/api\/comments\/([^/]+)$/)
|
|
257
|
+
if (deleteMatch && req.method === "DELETE") {
|
|
258
|
+
const session = validateSession(url.searchParams.get("token"))
|
|
259
|
+
if (!session.ok) return session.response
|
|
260
|
+
return handleReviewCommentDelete(decodeURIComponent(deleteMatch[1]), session.value)
|
|
261
|
+
}
|
|
262
|
+
|
|
205
263
|
if (url.pathname === "/api/comment-result" && req.method === "GET") {
|
|
206
264
|
const session = validateSession(url.searchParams.get("token"))
|
|
207
265
|
if (!session.ok) return session.response
|
|
@@ -749,6 +807,174 @@ async function handleComment(req: Request, session: EditSession): Promise<Respon
|
|
|
749
807
|
return jsonResponse({ ok: false, error: "Invalid JSON body" }, 400)
|
|
750
808
|
}
|
|
751
809
|
|
|
810
|
+
return applyCommentPayload(body, session)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function handleReviewCommentsList(session: EditSession): Response {
|
|
814
|
+
const deckVersion = readDeckVersion(session).version
|
|
815
|
+
const comments = listReviewComments(session.workspaceRoot, session.file)
|
|
816
|
+
session.lastActiveAt = Date.now()
|
|
817
|
+
scheduleIdleStop()
|
|
818
|
+
return jsonResponse({ ok: true, deckVersion, comments })
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function handleReviewCommentCreate(req: Request, session: EditSession): Promise<Response> {
|
|
822
|
+
let body: Partial<EditCommentPayload>
|
|
823
|
+
try {
|
|
824
|
+
body = await req.json()
|
|
825
|
+
} catch {
|
|
826
|
+
return jsonResponse({ ok: false, error: "Invalid JSON body" }, 400)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const comment = typeof body.comment === "string" ? body.comment.trim() : ""
|
|
830
|
+
const elements = Array.isArray(body.elements) ? body.elements : []
|
|
831
|
+
if (!comment) return jsonResponse({ ok: false, error: "Comment is required" }, 400)
|
|
832
|
+
try {
|
|
833
|
+
const deckVersion = readDeckVersion(session).version
|
|
834
|
+
const saved = createReviewComment(session.workspaceRoot, {
|
|
835
|
+
deckFile: session.file,
|
|
836
|
+
deckVersion,
|
|
837
|
+
comment,
|
|
838
|
+
elements,
|
|
839
|
+
asset: (body as any).asset,
|
|
840
|
+
drop: (body as any).drop,
|
|
841
|
+
})
|
|
842
|
+
session.lastActiveAt = Date.now()
|
|
843
|
+
scheduleIdleStop()
|
|
844
|
+
return jsonResponse({ ok: true, comment: saved, deckVersion })
|
|
845
|
+
} catch (error) {
|
|
846
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
847
|
+
return jsonResponse({ ok: false, error: message }, 400)
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function handleReviewCommentApply(commentId: string, req: Request, session: EditSession): Promise<Response> {
|
|
852
|
+
let body: any = {}
|
|
853
|
+
try {
|
|
854
|
+
const text = await req.text()
|
|
855
|
+
body = text ? JSON.parse(text) : {}
|
|
856
|
+
} catch {
|
|
857
|
+
return jsonResponse({ ok: false, error: "Invalid JSON body" }, 400)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const comment = readReviewComment(session.workspaceRoot, commentId)
|
|
861
|
+
if (!comment || comment.deckFile !== session.file) return jsonResponse({ ok: false, error: "Review comment not found" }, 404)
|
|
862
|
+
return enqueueOrStartPersistedReviewCommentApply(session, comment, body)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function handleReviewCommentStop(commentId: string, session: EditSession): Response {
|
|
866
|
+
const comment = readReviewComment(session.workspaceRoot, commentId)
|
|
867
|
+
if (!comment || comment.deckFile !== session.file) return jsonResponse({ ok: false, error: "Review comment not found" }, 404)
|
|
868
|
+
session.applyQueue = (session.applyQueue ?? []).filter((id) => id !== comment.id)
|
|
869
|
+
if (session.activeApplyCommentId === comment.id) session.activeApplyCommentId = undefined
|
|
870
|
+
const stopped = markReviewCommentStopped(session.workspaceRoot, comment.id) ?? comment
|
|
871
|
+
session.lastActiveAt = Date.now()
|
|
872
|
+
scheduleIdleStop()
|
|
873
|
+
if (comment.status === "queued") drainPersistedReviewCommentApplyQueue(session)
|
|
874
|
+
return jsonResponse({ ok: true, status: "stopped", comment: stopped, deckVersion: readDeckVersion(session).version })
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function handleReviewCommentDelete(commentId: string, session: EditSession): Response {
|
|
878
|
+
const comment = readReviewComment(session.workspaceRoot, commentId)
|
|
879
|
+
if (!comment || comment.deckFile !== session.file) return jsonResponse({ ok: false, error: "Review comment not found" }, 404)
|
|
880
|
+
if (comment.status === "applying") return jsonResponse({ ok: false, error: "Stop the applying comment before deleting it." }, 409)
|
|
881
|
+
session.applyQueue = (session.applyQueue ?? []).filter((id) => id !== comment.id)
|
|
882
|
+
const deleted = deleteReviewComment(session.workspaceRoot, comment.id)
|
|
883
|
+
if (!deleted) return jsonResponse({ ok: false, error: "Review comment not found" }, 404)
|
|
884
|
+
session.lastActiveAt = Date.now()
|
|
885
|
+
scheduleIdleStop()
|
|
886
|
+
return jsonResponse({ ok: true, deleted: true, commentId: comment.id, deckVersion: readDeckVersion(session).version })
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function enqueueOrStartPersistedReviewCommentApply(session: EditSession, comment: ReviewCommentRecord, body: any = {}): Promise<Response> {
|
|
890
|
+
session.applyQueue = session.applyQueue ?? []
|
|
891
|
+
if (session.activeApplyCommentId === comment.id) {
|
|
892
|
+
const current = readReviewComment(session.workspaceRoot, comment.id) ?? comment
|
|
893
|
+
return jsonResponse({
|
|
894
|
+
ok: true,
|
|
895
|
+
requestId: current.lastApplyRequestId,
|
|
896
|
+
commentRequestId: current.lastApplyRequestId,
|
|
897
|
+
deckVersion: readDeckVersion(session).version,
|
|
898
|
+
status: "pending",
|
|
899
|
+
comment: current,
|
|
900
|
+
})
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const queuedIndex = session.applyQueue.indexOf(comment.id)
|
|
904
|
+
if (queuedIndex >= 0) {
|
|
905
|
+
const queued = markReviewCommentQueued(session.workspaceRoot, comment.id) ?? comment
|
|
906
|
+
return jsonResponse({
|
|
907
|
+
ok: true,
|
|
908
|
+
deckVersion: readDeckVersion(session).version,
|
|
909
|
+
status: "queued",
|
|
910
|
+
queuePosition: queuedIndex + 1,
|
|
911
|
+
comment: queued,
|
|
912
|
+
})
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (session.activeApplyCommentId) {
|
|
916
|
+
session.applyQueue.push(comment.id)
|
|
917
|
+
const queued = markReviewCommentQueued(session.workspaceRoot, comment.id) ?? comment
|
|
918
|
+
session.lastActiveAt = Date.now()
|
|
919
|
+
scheduleIdleStop()
|
|
920
|
+
return jsonResponse({
|
|
921
|
+
ok: true,
|
|
922
|
+
deckVersion: readDeckVersion(session).version,
|
|
923
|
+
status: "queued",
|
|
924
|
+
queuePosition: session.applyQueue.length,
|
|
925
|
+
comment: queued,
|
|
926
|
+
})
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return startPersistedReviewCommentApply(session, comment, body)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function startPersistedReviewCommentApply(session: EditSession, comment: ReviewCommentRecord, body: any = {}): Promise<Response> {
|
|
933
|
+
session.activeApplyCommentId = comment.id
|
|
934
|
+
try {
|
|
935
|
+
const response = await applyCommentPayload({
|
|
936
|
+
...body,
|
|
937
|
+
comment: comment.comment,
|
|
938
|
+
elements: comment.elements,
|
|
939
|
+
asset: comment.asset,
|
|
940
|
+
drop: comment.drop,
|
|
941
|
+
requestId: typeof body.requestId === "string" && body.requestId.trim() ? body.requestId.trim() : randomBytes(10).toString("base64url"),
|
|
942
|
+
}, session, {
|
|
943
|
+
persistedCommentId: comment.id,
|
|
944
|
+
onSettled: () => settlePersistedReviewCommentApply(session, comment.id),
|
|
945
|
+
})
|
|
946
|
+
if (!response.ok) settlePersistedReviewCommentApply(session, comment.id)
|
|
947
|
+
return response
|
|
948
|
+
} catch (error) {
|
|
949
|
+
settlePersistedReviewCommentApply(session, comment.id)
|
|
950
|
+
throw error
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function settlePersistedReviewCommentApply(session: EditSession, commentId: string): void {
|
|
955
|
+
if (session.activeApplyCommentId === commentId) session.activeApplyCommentId = undefined
|
|
956
|
+
drainPersistedReviewCommentApplyQueue(session)
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function drainPersistedReviewCommentApplyQueue(session: EditSession): void {
|
|
960
|
+
if (session.activeApplyCommentId) return
|
|
961
|
+
session.applyQueue = session.applyQueue ?? []
|
|
962
|
+
while (session.applyQueue.length) {
|
|
963
|
+
const nextId = session.applyQueue.shift()
|
|
964
|
+
if (!nextId) continue
|
|
965
|
+
const next = readReviewComment(session.workspaceRoot, nextId)
|
|
966
|
+
if (!next || next.deckFile !== session.file) continue
|
|
967
|
+
void startPersistedReviewCommentApply(session, next)
|
|
968
|
+
return
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function applyCommentPayload(
|
|
973
|
+
body: Partial<EditCommentPayload>,
|
|
974
|
+
session: EditSession,
|
|
975
|
+
options: { persistedCommentId?: string; onSettled?: () => void } = {},
|
|
976
|
+
): Promise<Response> {
|
|
977
|
+
const { persistedCommentId, onSettled } = options
|
|
752
978
|
const comments = Array.isArray(body.comments)
|
|
753
979
|
? body.comments
|
|
754
980
|
.map((draft: any) => ({
|
|
@@ -778,6 +1004,7 @@ async function handleComment(req: Request, session: EditSession): Promise<Respon
|
|
|
778
1004
|
? (body as any).requestId.trim()
|
|
779
1005
|
: randomBytes(10).toString("base64url")
|
|
780
1006
|
createCommentRequest({ requestId, deckVersion })
|
|
1007
|
+
if (persistedCommentId) markReviewCommentApplying(session.workspaceRoot, persistedCommentId, requestId)
|
|
781
1008
|
suppressReviewApplyFixArtifactQa({
|
|
782
1009
|
workspaceRoot: session.workspaceRoot,
|
|
783
1010
|
file: session.file,
|
|
@@ -794,17 +1021,23 @@ async function handleComment(req: Request, session: EditSession): Promise<Respon
|
|
|
794
1021
|
}).then((result) => {
|
|
795
1022
|
if (result.ok) {
|
|
796
1023
|
completeCommentRequest(requestId)
|
|
1024
|
+
if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentApplied(session.workspaceRoot, persistedCommentId)
|
|
797
1025
|
} else {
|
|
798
1026
|
failCommentRequest(requestId, result.error, result.raw)
|
|
1027
|
+
if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentFailed(session.workspaceRoot, persistedCommentId, result.error, result.raw)
|
|
799
1028
|
}
|
|
1029
|
+
onSettled?.()
|
|
800
1030
|
}).catch((error: unknown) => {
|
|
801
1031
|
const message = error instanceof Error ? error.message : String(error)
|
|
802
1032
|
failCommentRequest(requestId, message)
|
|
1033
|
+
if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentFailed(session.workspaceRoot, persistedCommentId, message)
|
|
1034
|
+
onSettled?.()
|
|
803
1035
|
})
|
|
804
1036
|
|
|
805
1037
|
session.lastActiveAt = Date.now()
|
|
806
1038
|
scheduleIdleStop()
|
|
807
|
-
|
|
1039
|
+
const persistedComment = persistedCommentId ? readReviewComment(session.workspaceRoot, persistedCommentId) : undefined
|
|
1040
|
+
return jsonResponse({ ok: true, requestId, commentRequestId: requestId, deckVersion, status: "pending", ...(persistedComment ? { comment: persistedComment } : {}) })
|
|
808
1041
|
}
|
|
809
1042
|
|
|
810
1043
|
function handleCommentEvents(requestId: string | null, session: EditSession): Response {
|
|
@@ -1088,7 +1321,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1088
1321
|
const encodedToken = JSON.stringify(token)
|
|
1089
1322
|
const encodedDefaultMode = JSON.stringify(defaultMode)
|
|
1090
1323
|
const encodedSurface = JSON.stringify(surface)
|
|
1091
|
-
const
|
|
1324
|
+
const sendButtonHtml = `${lucideIcon("send")}<span class="sr-only">Leave Comment</span>`
|
|
1325
|
+
const encodedSendButtonHtml = JSON.stringify(sendButtonHtml)
|
|
1326
|
+
const activityLabel = "Comments"
|
|
1092
1327
|
const bodyClass = surface === "codex" ? "codex-review" : "legacy-review"
|
|
1093
1328
|
return `<!doctype html>
|
|
1094
1329
|
<html lang="en">
|
|
@@ -1099,96 +1334,131 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1099
1334
|
<style>
|
|
1100
1335
|
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
1101
1336
|
* { box-sizing: border-box; }
|
|
1102
|
-
body { margin: 0; background: #
|
|
1337
|
+
body { margin: 0; background: #f8fafc; color: #111827; height: 100vh; overflow: hidden; }
|
|
1103
1338
|
body.resizing { cursor: col-resize; user-select: none; }
|
|
1104
1339
|
body.resizing iframe, body.resizing .hitbox { pointer-events: none; }
|
|
1105
1340
|
.app { --editor-width: 376px; position: relative; display: grid; grid-template-columns: minmax(0, 1fr) var(--editor-width); height: 100vh; }
|
|
1106
|
-
.preview { position: relative; min-width: 0; background: #
|
|
1341
|
+
.preview { position: relative; min-width: 0; background: #eef2f7; }
|
|
1107
1342
|
.resize-handle { position: absolute; top: 0; bottom: 0; right: calc(var(--editor-width) - 7px); width: 14px; z-index: 5; cursor: col-resize; background: transparent; }
|
|
1108
1343
|
.resize-handle::before { content: ""; position: absolute; left: 50%; top: 50%; width: 4px; height: 44px; border-radius: 999px; transform: translate(-50%, -50%); background: rgba(148,163,184,.34); box-shadow: 0 1px 2px rgba(15,23,42,.06); transition: background .16s ease, height .16s ease, box-shadow .16s ease; }
|
|
1109
1344
|
.resize-handle:hover::before, body.resizing .resize-handle::before { height: 52px; background: #94a3b8; box-shadow: 0 0 0 4px rgba(148,163,184,.16); }
|
|
1110
1345
|
iframe { display: block; width: 100%; height: 100%; border: 0; background: #fff; }
|
|
1346
|
+
.comment-highlight-layer { position: absolute; inset: 0; z-index: 6; pointer-events: none; overflow: hidden; }
|
|
1347
|
+
.comment-highlight-box { position: absolute; display: none; border: 2px solid #2563eb; border-radius: 6px; background: rgba(37,99,235,.12); box-shadow: 0 0 0 3px rgba(255,255,255,.74), 0 10px 28px rgba(15,23,42,.14); }
|
|
1111
1348
|
.hitbox { position: absolute; inset: 0; z-index: 2; cursor: crosshair; background: transparent; }
|
|
1112
|
-
.visual-move-handle { position: absolute; z-index: 4; width: 16px; height: 16px; border: 2px solid #111827; border-radius: 999px; background: #
|
|
1349
|
+
.visual-move-handle { position: absolute; z-index: 4; width: 16px; height: 16px; border: 2px solid #111827; border-radius: 999px; background: #ffffff; box-shadow: 0 6px 16px rgba(15,23,42,.18); transform: translate(-50%, -50%); pointer-events: none; display: none; }
|
|
1113
1350
|
.visual-move-handle::before { content: ""; position: absolute; inset: 4px; border-top: 2px solid #111827; border-left: 2px solid #111827; transform: rotate(45deg); }
|
|
1114
|
-
.visual-resize-handle { position: absolute; z-index: 3; width: 14px; height: 14px; border: 2px solid #111827; border-radius: 4px; background: #
|
|
1351
|
+
.visual-resize-handle { position: absolute; z-index: 3; width: 14px; height: 14px; border: 2px solid #111827; border-radius: 4px; background: #ffffff; box-shadow: 0 6px 16px rgba(15,23,42,.18); transform: translate(-50%, -50%); pointer-events: none; display: none; }
|
|
1115
1352
|
.visual-resize-handle[data-mode="text-width"] { width: 10px; height: 28px; border-radius: 999px; cursor: ew-resize; }
|
|
1116
|
-
.visual-edit-toolbar { position: absolute; top: 14px; left: 50%; z-index: 6; display:
|
|
1117
|
-
.visual-edit-toolbar.active {
|
|
1353
|
+
.visual-edit-toolbar { position: absolute; top: 14px; left: 50%; z-index: 6; display: inline-flex; align-items: center; gap: 8px; transform: translateX(-50%); padding: 8px 10px; border: 1px solid rgba(148,163,184,.42); border-radius: 999px; background: rgba(15,23,42,.78); color: #ffffff; box-shadow: 0 16px 40px rgba(15,23,42,.2); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); font-size: 12px; font-weight: 800; }
|
|
1354
|
+
.visual-edit-toolbar.active { background: rgba(15,23,42,.9); box-shadow: 0 16px 40px rgba(15,23,42,.24); }
|
|
1118
1355
|
.visual-edit-toolbar button { width: auto; min-width: 0; padding: 7px 10px; border-radius: 999px; border-color: rgba(255,255,255,.2); background: rgba(255,255,255,.12); color: #fff; box-shadow: none; font-size: 12px; }
|
|
1119
|
-
.visual-edit-toolbar .save-visual { background: #
|
|
1356
|
+
.visual-edit-toolbar .save-visual { background: #ffffff; color: #111827; }
|
|
1120
1357
|
.deck-nav { position: absolute; left: 50%; bottom: 18px; z-index: 4; display: inline-flex; align-items: center; gap: 8px; transform: translateX(-50%); padding: 7px; border: 1px solid rgba(148,163,184,.42); border-radius: 999px; background: rgba(15,23,42,.76); box-shadow: 0 16px 44px rgba(15,23,42,.24); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); pointer-events: auto; }
|
|
1121
1358
|
.deck-nav button { width: auto; min-width: 84px; padding: 8px 12px; border-radius: 999px; background: rgba(255,255,255,.12); color: #fff; box-shadow: none; font-size: 12px; font-weight: 900; }
|
|
1122
1359
|
.deck-nav button:hover:not(:disabled) { background: rgba(255,255,255,.22); }
|
|
1123
1360
|
.deck-nav button:disabled { opacity: .38; }
|
|
1124
1361
|
.deck-nav-status { min-width: 76px; color: #e2e8f0; font-size: 12px; font-weight: 900; text-align: center; font-variant-numeric: tabular-nums; }
|
|
1125
|
-
aside { position: relative; display: flex; flex-direction: column; gap: 16px; padding:
|
|
1362
|
+
aside { position: relative; display: flex; flex-direction: column; gap: 16px; padding: 18px; background: #ffffff; overflow: hidden; border-left: 1px solid #e2e8f0; box-shadow: -10px 0 28px rgba(15,23,42,.06); }
|
|
1126
1363
|
aside button, aside input, aside select, aside textarea, aside .comment-editor { font-family: inherit; }
|
|
1127
|
-
h1 { margin: 0; font-size:
|
|
1128
|
-
.wordmark { font-family:
|
|
1364
|
+
h1 { margin: 0; font-size: 17px; line-height: 1.2; letter-spacing: 0; color: #0f172a; }
|
|
1365
|
+
.wordmark { font-family: inherit; font-size: 17px; letter-spacing: 0; font-weight: 800; }
|
|
1129
1366
|
.panel { display: flex; flex-direction: column; gap: 10px; }
|
|
1130
|
-
.tabs { display: flex; gap:
|
|
1131
|
-
.tab { width: auto; min-width: 112px; padding:
|
|
1132
|
-
.tab:hover:not(:disabled) { background:
|
|
1133
|
-
.tab.active { position: relative; top:
|
|
1134
|
-
.tab-panel { display: none; flex-direction: column; gap: 12px; padding-top: 12px; }
|
|
1367
|
+
.tabs { display: flex; gap: 4px; padding: 3px; border: 1px solid #e2e8f0; border-radius: 999px; background: #f8fafc; }
|
|
1368
|
+
.tab { width: auto; min-width: 112px; padding: 8px 16px; border: 1px solid transparent; border-radius: 999px; background: transparent; color: #64748b; box-shadow: none; font-weight: 750; }
|
|
1369
|
+
.tab:hover:not(:disabled) { background: #eef2f7; color: #334155; }
|
|
1370
|
+
.tab.active { position: relative; top: 0; background: #ffffff; border-color: #e2e8f0; color: #0f172a; box-shadow: 0 1px 3px rgba(15,23,42,.08); }
|
|
1371
|
+
.tab-panel { display: none; flex-direction: column; gap: 12px; min-height: 0; padding-top: 12px; }
|
|
1135
1372
|
.tab-panel.active { display: flex; }
|
|
1373
|
+
#editPanel { flex: 1 1 auto; min-height: 0; }
|
|
1136
1374
|
#inspectTab, #inspectPanel { display: none !important; }
|
|
1137
1375
|
.sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important; }
|
|
1138
|
-
.selection-summary { padding: 10px 12px; border: 1px solid #
|
|
1139
|
-
.selection-summary strong { display: block; margin-bottom: 7px; color: #
|
|
1376
|
+
.selection-summary { padding: 10px 12px; border: 1px solid #e2e8f0; border-radius: 14px; background: #f8fafc; color: #334155; font-size: 13px; line-height: 1.45; box-shadow: none; }
|
|
1377
|
+
.selection-summary strong { display: block; margin-bottom: 7px; color: #64748b; font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }
|
|
1140
1378
|
.selection-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1141
|
-
.label { color: #
|
|
1142
|
-
.comment-
|
|
1143
|
-
.comment-
|
|
1144
|
-
.comment-
|
|
1145
|
-
.
|
|
1146
|
-
.
|
|
1147
|
-
.comment-
|
|
1148
|
-
.comment-
|
|
1149
|
-
.comment-
|
|
1150
|
-
.
|
|
1151
|
-
.
|
|
1152
|
-
.comment-
|
|
1153
|
-
.comment-
|
|
1154
|
-
.comment-bubble
|
|
1155
|
-
.comment-bubble
|
|
1156
|
-
.comment-
|
|
1157
|
-
.comment-
|
|
1158
|
-
.comment-
|
|
1159
|
-
.comment-
|
|
1379
|
+
.label { color: #64748b; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }
|
|
1380
|
+
.comment-composer { position: relative; display: flex; flex-direction: column; gap: 10px; padding-top: 10px; border-top: 1px solid #e2e8f0; }
|
|
1381
|
+
.comment-input-box { position: relative; display: flex; flex-direction: column; min-height: 166px; border: 1px solid #dbe3ef; border-radius: 18px; background: #ffffff; box-shadow: 0 1px 2px rgba(15,23,42,.04); }
|
|
1382
|
+
.comment-input-box:focus-within { border-color: #93c5fd; box-shadow: 0 0 0 3px rgba(59,130,246,.12), 0 8px 22px rgba(15,23,42,.06); }
|
|
1383
|
+
.comment-editor { width: 100%; min-height: 132px; max-height: 30vh; overflow: auto; padding: 13px 14px; border: 1px solid #dbe3ef; border-radius: 14px; background: #ffffff; color: #111827; font: inherit; line-height: 1.5; outline: none; white-space: pre-wrap; box-shadow: none; }
|
|
1384
|
+
.comment-editor:focus { border-color: #93c5fd; box-shadow: 0 0 0 3px rgba(59,130,246,.12); }
|
|
1385
|
+
.comment-input-box .comment-editor { min-height: 116px; padding: 14px 56px 8px 14px; border: 0; border-radius: 18px 18px 0 0; background: transparent; box-shadow: none; }
|
|
1386
|
+
.comment-input-box .comment-editor:focus { outline: none; box-shadow: none; }
|
|
1387
|
+
.comment-editor:empty::before { content: attr(data-placeholder); color: #94a3b8; pointer-events: none; }
|
|
1388
|
+
.ref-chip { display: inline-flex; align-items: center; max-width: 32ch; overflow: hidden; text-overflow: ellipsis; margin: 0 2px; padding: 1px 7px; border-radius: 999px; background: var(--ref-bg, #eff6ff); color: var(--ref-text, #1d4ed8); border: 1px solid var(--ref-border, #bfdbfe); font-weight: 800; white-space: nowrap; }
|
|
1389
|
+
.activity-panel { display: flex; flex: 1 1 auto; flex-direction: column; gap: 8px; min-height: 0; padding-top: 2px; }
|
|
1390
|
+
.comment-thread { display: flex; flex: 1 1 auto; flex-direction: column; gap: 9px; min-height: 160px; overflow-y: auto; overflow-x: hidden; padding: 1px 2px 1px 1px; }
|
|
1391
|
+
.comment-thread:empty::before { content: "No activity yet. Leave a comment to start."; display: block; padding: 14px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; font-size: 12px; line-height: 1.45; background: #f8fafc; box-shadow: none; }
|
|
1392
|
+
.comment-bubble { position: relative; display: flex; flex: 0 0 132px; flex-direction: column; min-height: 132px; max-height: 132px; overflow: hidden; border: 1px solid #e2e8f0; border-radius: 15px; padding: 11px 12px 10px 14px; background: #ffffff; color: #334155; font-size: 13px; line-height: 1.45; box-shadow: 0 1px 2px rgba(15,23,42,.04), inset 3px 0 0 var(--comment-accent, #e2e8f0); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; cursor: pointer; }
|
|
1393
|
+
.comment-bubble:hover { transform: translateY(-1px); border-color: #cbd5e1; box-shadow: 0 8px 20px rgba(15,23,42,.07), inset 3px 0 0 var(--comment-accent, #e2e8f0); }
|
|
1394
|
+
.comment-bubble.active { border-color: #93c5fd; box-shadow: 0 0 0 3px rgba(59,130,246,.12), 0 8px 20px rgba(15,23,42,.08), inset 3px 0 0 #2563eb; }
|
|
1395
|
+
.comment-bubble.sending { --comment-accent: #3b82f6; border-color: #bfdbfe; background: #f8fbff; }
|
|
1396
|
+
.comment-bubble.open { --comment-accent: #cbd5e1; border-color: #e2e8f0; background: #ffffff; }
|
|
1397
|
+
.comment-bubble.queued { --comment-accent: #f59e0b; border-color: #fde68a; background: #fffdf4; }
|
|
1398
|
+
.comment-bubble.applying { --comment-accent: #2563eb; border-color: #bfdbfe; background: #f8fbff; box-shadow: 0 8px 22px rgba(37,99,235,.1), inset 3px 0 0 var(--comment-accent); }
|
|
1399
|
+
.comment-bubble.applied { --comment-accent: #16a34a; border-color: #bbf7d0; background: #f8fefb; }
|
|
1400
|
+
.comment-bubble.updated { --comment-accent: #15803d; border-color: #bbf7d0; background: #f7fef9; }
|
|
1401
|
+
.comment-bubble.stale { --comment-accent: #d97706; border-color: #fed7aa; background: #fffaf0; }
|
|
1402
|
+
.comment-bubble.failed { --comment-accent: #dc2626; border-color: #fecaca; background: #fffafa; }
|
|
1403
|
+
.comment-bubble-text { flex: 1 1 auto; min-height: 0; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1404
|
+
.comment-bubble-state { margin-top: 8px; align-self: flex-start; padding: 2px 7px; border-radius: 999px; background: #f1f5f9; color: #475569; font-size: 11px; font-weight: 800; }
|
|
1405
|
+
.comment-bubble-meta { margin-bottom: 6px; color: #64748b; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
|
|
1406
|
+
.comment-actions { position: absolute; right: 9px; bottom: 9px; display: flex; gap: 6px; }
|
|
1407
|
+
.comment-action-button { display: inline-flex; align-items: center; justify-content: center; width: 30px; min-width: 30px; height: 30px; min-height: 30px; padding: 0; border-radius: 999px; border-color: #e2e8f0; background: rgba(255,255,255,.92); color: #475569; box-shadow: 0 1px 2px rgba(15,23,42,.06); }
|
|
1408
|
+
.comment-action-button:hover:not(:disabled) { background: #f1f5f9; color: #111827; transform: translateY(-1px); }
|
|
1409
|
+
.comment-action-button.danger { color: #dc2626; }
|
|
1410
|
+
.comment-action-button.stop { color: #b45309; }
|
|
1411
|
+
.comment-action-icon { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
|
|
1412
|
+
.comment-progress { margin-top: 7px; display: flex; flex-direction: column; gap: 4px; color: #475569; font-size: 12px; }
|
|
1413
|
+
.comment-progress-line { display: flex; gap: 8px; align-items: flex-start; padding: 7px 8px; border: 1px solid #dbeafe; border-radius: 10px; background: #eff6ff; }
|
|
1414
|
+
.comment-progress-line::before { content: ""; width: 7px; height: 7px; margin-top: 5px; border-radius: 999px; background: #2563eb; box-shadow: 0 0 0 4px rgba(37,99,235,.12); flex: 0 0 auto; animation: progress-pulse 1.2s ease-in-out infinite; }
|
|
1415
|
+
.comment-raw { margin-top: 8px; color: #b91c1c; font-size: 12px; }
|
|
1160
1416
|
.comment-raw summary { cursor: pointer; font-weight: 800; }
|
|
1161
|
-
.comment-raw pre { margin: 6px 0 0; max-height: 160px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; background:
|
|
1162
|
-
.codex-log { margin-top: 8px; color: #
|
|
1417
|
+
.comment-raw pre { margin: 6px 0 0; max-height: 160px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; background: #f8fafc; border: 1px solid #fecaca; border-radius: 8px; padding: 8px; }
|
|
1418
|
+
.codex-log { margin-top: 8px; color: #475569; font-size: 12px; }
|
|
1163
1419
|
.codex-log summary { cursor: pointer; font-weight: 900; }
|
|
1164
|
-
.codex-log-list { margin-top: 7px; display: flex; flex-direction: column; gap:
|
|
1165
|
-
.codex-log-entry { padding:
|
|
1166
|
-
.codex-log-
|
|
1167
|
-
.codex-log-
|
|
1168
|
-
.codex-log-
|
|
1169
|
-
.
|
|
1170
|
-
.
|
|
1171
|
-
.
|
|
1420
|
+
.codex-log-list { margin-top: 7px; display: flex; flex-direction: column; gap: 0; max-height: 240px; overflow: auto; }
|
|
1421
|
+
.codex-log-entry { position: relative; padding: 9px 10px 9px 18px; border: 0; border-left: 1px solid #dbe3ef; border-radius: 0; background: transparent; }
|
|
1422
|
+
.codex-log-entry::before { content: ""; position: absolute; left: -4px; top: 15px; width: 7px; height: 7px; border-radius: 999px; background: #94a3b8; box-shadow: 0 0 0 3px #ffffff; }
|
|
1423
|
+
.codex-log-meta { display: flex; justify-content: space-between; gap: 8px; color: #94a3b8; font-size: 11px; font-weight: 750; text-transform: uppercase; }
|
|
1424
|
+
.codex-log-message { margin-top: 4px; color: #334155; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1425
|
+
.codex-log-detail { margin: 6px 0 0; max-height: 120px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: #e2e8f0; background: #0f172a; border: 1px solid #1e293b; border-radius: 8px; padding: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
1426
|
+
.codex-log-modal { position: fixed; inset: 0; z-index: 80; display: none; align-items: center; justify-content: center; padding: 24px; }
|
|
1427
|
+
.codex-log-modal.open { display: flex; }
|
|
1428
|
+
.codex-log-backdrop { position: absolute; inset: 0; background: rgba(15,23,42,.42); }
|
|
1429
|
+
.codex-log-dialog { position: relative; z-index: 1; display: flex; flex-direction: column; width: min(860px, calc(100vw - 48px)); max-height: min(720px, calc(100vh - 48px)); overflow: hidden; border: 1px solid #e2e8f0; border-radius: 16px; background: #ffffff; box-shadow: 0 28px 70px rgba(15,23,42,.28); }
|
|
1430
|
+
.codex-log-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #e2e8f0; background: #f8fafc; }
|
|
1431
|
+
.codex-log-title { margin: 0; color: #111827; font-size: 15px; line-height: 1.25; font-weight: 900; }
|
|
1432
|
+
.codex-log-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; min-width: 32px; height: 32px; padding: 0; border-radius: 999px; box-shadow: none; }
|
|
1433
|
+
.codex-log-modal .codex-log-list { margin: 0; padding: 14px; max-height: none; overflow: auto; }
|
|
1434
|
+
.codex-log-modal .codex-log-entry { background: transparent; }
|
|
1435
|
+
.codex-log-modal .codex-log-detail { max-height: 260px; }
|
|
1436
|
+
.comment-bubble.updated .comment-bubble-state { background: #dcfce7; color: #166534; }
|
|
1437
|
+
.comment-bubble.applied .comment-bubble-state { background: #dcfce7; color: #166534; }
|
|
1438
|
+
.comment-bubble.applying .comment-bubble-state { background: #dbeafe; color: #1d4ed8; }
|
|
1439
|
+
.comment-bubble.queued .comment-bubble-state { background: #fef3c7; color: #92400e; }
|
|
1440
|
+
.comment-bubble.stale .comment-bubble-state { background: #ffedd5; color: #9a3412; }
|
|
1441
|
+
.comment-bubble.failed .comment-bubble-state { background: #fee2e2; color: #991b1b; }
|
|
1172
1442
|
.inspect-actions { display: flex; flex-direction: column; gap: 8px; }
|
|
1173
1443
|
.inspect-options { display: flex; flex-direction: column; gap: 5px; }
|
|
1174
|
-
.inspect-options label { color: #
|
|
1175
|
-
.inspect-select { width: 100%; padding: 10px 11px; border: 1px solid #
|
|
1444
|
+
.inspect-options label { color: #64748b; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
|
|
1445
|
+
.inspect-select { width: 100%; padding: 10px 11px; border: 1px solid #dbe3ef; border-radius: 12px; background: #ffffff; color: #111827; font-weight: 700; }
|
|
1176
1446
|
.inspect-cards { display: flex; flex-direction: column; gap: 12px; }
|
|
1177
|
-
.inspect-card { border: 1px solid #
|
|
1447
|
+
.inspect-card { border: 1px solid #e2e8f0; border-radius: 16px; background: #ffffff; padding: 13px; box-shadow: 0 1px 2px rgba(15,23,42,.04); }
|
|
1178
1448
|
.inspect-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
1179
1449
|
.inspect-card h2 { margin: 0; font-size: 13px; color: #0f172a; }
|
|
1180
|
-
.badge { border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; background: #
|
|
1181
|
-
.badge.supported, .badge.found, .badge.present, .badge.known, .badge.suggested { background: #
|
|
1182
|
-
.badge.weak, .badge.missing { background: #
|
|
1183
|
-
.badge.unsupported { background: #
|
|
1184
|
-
.inspect-card p, .inspect-empty, .inspect-loading { margin: 0; color: #
|
|
1185
|
-
.inspect-item { margin-top: 7px; padding: 8px; border-radius: 10px; background: #
|
|
1450
|
+
.badge { border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; background: #f1f5f9; color: #475569; }
|
|
1451
|
+
.badge.supported, .badge.found, .badge.present, .badge.known, .badge.suggested { background: #dcfce7; color: #166534; }
|
|
1452
|
+
.badge.weak, .badge.missing { background: #fef3c7; color: #92400e; }
|
|
1453
|
+
.badge.unsupported { background: #fee2e2; color: #991b1b; }
|
|
1454
|
+
.inspect-card p, .inspect-empty, .inspect-loading { margin: 0; color: #475569; font-size: 12px; line-height: 1.5; }
|
|
1455
|
+
.inspect-item { margin-top: 7px; padding: 8px; border-radius: 10px; background: #f8fafc; color: #334155; font-size: 12px; line-height: 1.45; }
|
|
1186
1456
|
.inspect-warning, .inspect-stale { margin-top: 8px; padding: 8px; border-radius: 10px; background: #fff7ed; color: #9a3412; font-size: 12px; line-height: 1.45; }
|
|
1187
1457
|
.loading-row { display: inline-flex; align-items: center; gap: 8px; }
|
|
1188
|
-
.spinner { width: 16px; height: 16px; border: 2px solid rgba(
|
|
1458
|
+
.spinner { width: 16px; height: 16px; border: 2px solid rgba(59,130,246,.22); border-top-color: currentColor; border-radius: 999px; animation: spin .8s linear infinite; }
|
|
1189
1459
|
button .spinner { width: 15px; height: 15px; border-color: rgba(255,255,255,.36); border-top-color: #fff; }
|
|
1190
|
-
.skeleton-card { border: 1px solid #
|
|
1191
|
-
.skeleton-line { height: 10px; margin: 8px 0; border-radius: 999px; background: linear-gradient(90deg, #
|
|
1460
|
+
.skeleton-card { border: 1px solid #e2e8f0; border-radius: 16px; background: #ffffff; padding: 13px; box-shadow: 0 1px 2px rgba(15,23,42,.04); }
|
|
1461
|
+
.skeleton-line { height: 10px; margin: 8px 0; border-radius: 999px; background: linear-gradient(90deg, #e2e8f0 0%, #f8fafc 48%, #e2e8f0 100%); background-size: 200% 100%; animation: shimmer 1.2s ease-in-out infinite; }
|
|
1192
1462
|
.skeleton-line.short { width: 42%; }
|
|
1193
1463
|
.skeleton-line.medium { width: 68%; }
|
|
1194
1464
|
.skeleton-line.long { width: 92%; }
|
|
@@ -1196,48 +1466,57 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1196
1466
|
.asset-card.is-saving .asset-save { z-index: 1; }
|
|
1197
1467
|
.asset-card.is-saved-candidate .asset-thumb { opacity: .72; }
|
|
1198
1468
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
1469
|
+
@keyframes progress-pulse { 0%, 100% { transform: scale(.85); opacity: .68; } 50% { transform: scale(1.16); opacity: 1; } }
|
|
1199
1470
|
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
|
1471
|
+
@media (prefers-reduced-motion: reduce) { .comment-bubble, .comment-progress-line::before, .skeleton-line, .spinner { animation: none !important; transition: none !important; } .comment-bubble:hover { transform: none; } }
|
|
1200
1472
|
.asset-search { display: grid; grid-template-columns: minmax(0, 1fr) 118px; gap: 8px; }
|
|
1201
|
-
.asset-search input, .asset-search select { min-width: 0; padding: 10px 11px; border: 1px solid #
|
|
1202
|
-
.asset-search input:focus, .asset-search select:focus { border-color: #
|
|
1473
|
+
.asset-search input, .asset-search select { min-width: 0; padding: 10px 11px; border: 1px solid #dbe3ef; border-radius: 12px; background: #ffffff; color: #111827; font: inherit; font-size: 12px; font-weight: 700; outline: none; }
|
|
1474
|
+
.asset-search input:focus, .asset-search select:focus { border-color: #93c5fd; box-shadow: 0 0 0 3px rgba(59,130,246,.12); }
|
|
1203
1475
|
.asset-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }
|
|
1204
1476
|
.asset-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
|
|
1205
|
-
.asset-card { position: relative; min-width: 0; aspect-ratio: 1 / 1; overflow: hidden; border: 1px solid #
|
|
1477
|
+
.asset-card { position: relative; min-width: 0; aspect-ratio: 1 / 1; overflow: hidden; border: 1px solid #e2e8f0; border-radius: 14px; background: #ffffff; box-shadow: 0 1px 2px rgba(15,23,42,.04); }
|
|
1206
1478
|
.asset-card.saved { width: 64px; height: 64px; aspect-ratio: auto; border-radius: 12px; }
|
|
1207
1479
|
.asset-card[draggable="true"] { cursor: grab; }
|
|
1208
1480
|
.asset-card[draggable="true"]:active { cursor: grabbing; }
|
|
1209
|
-
.asset-thumb { width: 100%; height: 100%; display: block; background: #
|
|
1210
|
-
.
|
|
1211
|
-
.asset-
|
|
1212
|
-
.asset-
|
|
1481
|
+
.asset-thumb { width: 100%; height: 100%; display: block; background: #f1f5f9; object-fit: contain; }
|
|
1482
|
+
.composer-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 50px; padding: 8px 10px 10px 10px; }
|
|
1483
|
+
.asset-menu-wrap { position: static; min-width: 0; }
|
|
1484
|
+
.asset-menu-toggle { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; min-width: 42px; padding: 0; border-radius: 999px; background: transparent; box-shadow: none; color: #64748b; }
|
|
1485
|
+
.asset-menu-toggle:hover:not(:disabled) { background: #f1f5f9; color: #111827; }
|
|
1486
|
+
.local-assets-menu { position: absolute; left: 0; right: 0; bottom: calc(100% + 10px); z-index: 11; display: none; width: 100%; padding: 10px; border: 1px solid #e2e8f0; border-radius: 16px; background: #ffffff; box-shadow: 0 18px 44px rgba(15,23,42,.12); }
|
|
1487
|
+
.local-assets-menu.open { display: block; }
|
|
1488
|
+
.local-assets-menu-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
1489
|
+
.asset-search-toggle { display: inline-flex; align-items: center; justify-content: center; width: 32px; min-width: 32px; height: 32px; padding: 0; border-radius: 999px; box-shadow: 0 1px 2px rgba(15,23,42,.06); }
|
|
1490
|
+
.asset-search-view { position: absolute; inset: 0; z-index: 12; display: flex; flex-direction: column; gap: 14px; padding: 20px; background: #ffffff; overflow: auto; transform: translateX(105%); transition: transform .2s ease; box-shadow: -18px 0 44px rgba(15,23,42,.12); }
|
|
1213
1491
|
.asset-search-view.open { transform: translateX(0); }
|
|
1214
1492
|
.asset-search-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
1215
1493
|
.asset-search-title { display: flex; flex-direction: column; gap: 2px; }
|
|
1216
|
-
.asset-search-title h2 { margin: 0; color: #0f172a; font-size: 16px; letter-spacing:
|
|
1217
|
-
.asset-search-title span { color: #
|
|
1218
|
-
.asset-back { width: auto; padding: 9px 11px; border-radius: 999px; background: #
|
|
1219
|
-
.asset-save { position: absolute; left: 7px; right: 7px; bottom: 7px; width: auto; padding: 7px 8px; border-radius: 10px; font-size: 11px; background: rgba(
|
|
1220
|
-
.asset-save.saved { background: rgba(
|
|
1221
|
-
.asset-empty { grid-column: 1 / -1; margin: 0; color: #
|
|
1222
|
-
.
|
|
1223
|
-
.
|
|
1224
|
-
.
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
button { width: 100%; padding: 12px 14px; border: 1px solid #d8d2c6; border-radius: 12px; background: #ebe4d8; color: #111827; font-weight: 800; cursor: pointer; box-shadow: 0 8px 16px rgba(31,41,51,.08); }
|
|
1228
|
-
button:hover:not(:disabled) { background: #e3dacb; }
|
|
1494
|
+
.asset-search-title h2 { margin: 0; color: #0f172a; font-size: 16px; letter-spacing: 0; }
|
|
1495
|
+
.asset-search-title span { color: #64748b; font-size: 12px; line-height: 1.35; }
|
|
1496
|
+
.asset-back { width: auto; padding: 9px 11px; border-radius: 999px; background: #f1f5f9; color: #111827; box-shadow: none; }
|
|
1497
|
+
.asset-save { position: absolute; left: 7px; right: 7px; bottom: 7px; width: auto; padding: 7px 8px; border-radius: 10px; font-size: 11px; background: rgba(15,23,42,.9); color: #ffffff; box-shadow: 0 8px 16px rgba(15,23,42,.18); opacity: .96; }
|
|
1498
|
+
.asset-save.saved { background: rgba(22,101,52,.94); color: #ffffff; cursor: default; }
|
|
1499
|
+
.asset-empty { grid-column: 1 / -1; margin: 0; color: #64748b; font-size: 12px; line-height: 1.45; }
|
|
1500
|
+
.local-assets-menu .asset-grid { grid-template-columns: repeat(auto-fill, 64px); align-items: start; max-height: 220px; overflow: auto; }
|
|
1501
|
+
.local-assets-menu .asset-thumb { width: 64px; height: 64px; }
|
|
1502
|
+
.drop-active .hitbox { background: rgba(37,99,235,.08); outline: 2px dashed rgba(37,99,235,.38); outline-offset: -10px; }
|
|
1503
|
+
button { width: 100%; padding: 12px 14px; border: 1px solid #dbe3ef; border-radius: 12px; background: #f8fafc; color: #111827; font-weight: 800; cursor: pointer; box-shadow: none; }
|
|
1504
|
+
button:hover:not(:disabled) { background: #f1f5f9; }
|
|
1229
1505
|
button:disabled { cursor: not-allowed; opacity: .5; }
|
|
1230
|
-
.primary-action { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 46px; border-radius: 14px; border-color: #
|
|
1231
|
-
.primary-action:hover:not(:disabled) { background:
|
|
1232
|
-
.primary-action:active:not(:disabled) { transform: translateY(0); box-shadow: 0
|
|
1233
|
-
.send
|
|
1234
|
-
.
|
|
1235
|
-
|
|
1506
|
+
.primary-action { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 46px; border-radius: 14px; border-color: #1d4ed8; background: #2563eb; color: #ffffff; font-size: 14px; letter-spacing: 0; box-shadow: 0 8px 18px rgba(37,99,235,.22); transition: transform .14s ease, box-shadow .14s ease, background .14s ease; }
|
|
1507
|
+
.primary-action:hover:not(:disabled) { background: #1d4ed8; transform: translateY(-1px); box-shadow: 0 12px 24px rgba(37,99,235,.26); }
|
|
1508
|
+
.primary-action:active:not(:disabled) { transform: translateY(0); box-shadow: 0 6px 14px rgba(37,99,235,.22); }
|
|
1509
|
+
.composer-send { position: absolute; right: 10px; bottom: 10px; width: 42px; min-width: 42px; height: 42px; min-height: 42px; padding: 0; border-radius: 999px; }
|
|
1510
|
+
.composer-send:disabled { opacity: .44; }
|
|
1511
|
+
.composer-send .spinner + span { display: none; }
|
|
1512
|
+
.composer-icon { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
1513
|
+
.status { min-height: 20px; color: #64748b; font-size: 13px; line-height: 1.45; }
|
|
1514
|
+
@media (max-width: 900px) { .app { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; } .resize-handle { display: none; } aside { max-height: 48vh; min-width: 0; border-left: 0; border-top: 1px solid #e2e8f0; } .deck-nav { bottom: 10px; } .asset-search { grid-template-columns: 1fr; } }
|
|
1236
1515
|
</style>
|
|
1237
1516
|
</head>
|
|
1238
1517
|
<body class="${bodyClass}">
|
|
1239
1518
|
<main class="app">
|
|
1240
|
-
<section class="preview"><iframe id="deck" src="/deck?token=${encodeURIComponent(token)}"></iframe><div id="hitbox" class="hitbox" aria-label="Deck element selection layer"></div><div id="visualMoveHandle" class="visual-move-handle" aria-hidden="true"></div><div id="visualResizeHandle" class="visual-resize-handle" aria-hidden="true"></div><div id="visualEditToolbar" class="visual-edit-toolbar" aria-live="polite"><span id="visualEditCount">No unsaved visual changes</span><button id="visualUndo" type="button">Undo</button><button id="visualReset" type="button">Reset</button><button id="visualSave" class="save-visual" type="button">Save Changes</button></div><nav class="deck-nav" aria-label="Deck navigation"><button id="deckPrev" type="button" title="Previous slide (ArrowLeft / ArrowUp / PageUp)">Previous</button><div id="deckCounter" class="deck-nav-status" aria-live="polite">-- / --</div><button id="deckNext" type="button" title="Next slide (ArrowRight / ArrowDown / Space / PageDown)">Next</button></nav></section>
|
|
1519
|
+
<section class="preview"><iframe id="deck" src="/deck?token=${encodeURIComponent(token)}"></iframe><div id="commentHighlightLayer" class="comment-highlight-layer" aria-hidden="true"></div><div id="hitbox" class="hitbox" aria-label="Deck element selection layer"></div><div id="visualMoveHandle" class="visual-move-handle" aria-hidden="true"></div><div id="visualResizeHandle" class="visual-resize-handle" aria-hidden="true"></div><div id="visualEditToolbar" class="visual-edit-toolbar" aria-live="polite"><span id="visualEditCount">No unsaved visual changes</span><button id="visualUndo" type="button">Undo</button><button id="visualReset" type="button">Reset</button><button id="visualSave" class="save-visual" type="button">Save Changes</button></div><nav class="deck-nav" aria-label="Deck navigation"><button id="deckPrev" type="button" title="Previous slide (ArrowLeft / ArrowUp / PageUp)">Previous</button><div id="deckCounter" class="deck-nav-status" aria-live="polite">-- / --</div><button id="deckNext" type="button" title="Next slide (ArrowRight / ArrowDown / Space / PageDown)">Next</button></nav></section>
|
|
1241
1520
|
<div id="resizeHandle" class="resize-handle" role="separator" aria-label="Resize editor panel" aria-orientation="vertical" title="Drag to resize editor. Double-click to reset."></div>
|
|
1242
1521
|
<aside>
|
|
1243
1522
|
<div>
|
|
@@ -1249,18 +1528,23 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1249
1528
|
<button id="inspectTab" class="tab" type="button" role="tab">Insight</button>
|
|
1250
1529
|
</div>
|
|
1251
1530
|
<div id="editPanel" class="tab-panel">
|
|
1252
|
-
<div class="panel">
|
|
1531
|
+
<div class="activity-panel"><div class="label">${activityLabel}</div><div id="commentThread" class="comment-thread" aria-live="polite"></div></div>
|
|
1532
|
+
<div class="comment-composer">
|
|
1253
1533
|
<div class="label">Describe the change</div>
|
|
1254
|
-
<div
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1534
|
+
<div class="comment-input-box">
|
|
1535
|
+
<div id="comment" class="comment-editor" contenteditable="true" role="textbox" aria-multiline="true" data-placeholder="Cmd/Ctrl-click slide elements to add @refs, then describe the exact edit."></div>
|
|
1536
|
+
<div class="composer-actions">
|
|
1537
|
+
<div class="asset-menu-wrap">
|
|
1538
|
+
<button id="localAssetToggle" class="asset-menu-toggle" type="button" aria-label="Local Assets" title="Local Assets" aria-expanded="false" aria-controls="localAssetMenu">${lucideIcon("image")}</button>
|
|
1539
|
+
<div id="localAssetMenu" class="local-assets-menu" aria-hidden="true">
|
|
1540
|
+
<div class="local-assets-menu-head"><div class="label">Local Assets</div><button id="assetSearchToggle" class="asset-search-toggle" type="button" aria-label="Search assets" aria-expanded="false" aria-controls="assetSearchView" title="Search assets">${lucideIcon("plus")}</button></div>
|
|
1541
|
+
<div id="editSavedAssets" class="asset-grid"><p class="asset-empty">No local assets yet. Click + to search assets.</p></div>
|
|
1542
|
+
</div>
|
|
1543
|
+
</div>
|
|
1544
|
+
</div>
|
|
1545
|
+
<button id="send" class="primary-action composer-send" type="button" aria-label="Leave Comment" title="Leave Comment" disabled>${sendButtonHtml}</button>
|
|
1260
1546
|
</div>
|
|
1261
1547
|
</div>
|
|
1262
|
-
<button id="send" class="primary-action" disabled><svg class="send-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94L14.7 6.3z"/></svg><span>Apply Fix</span></button>
|
|
1263
|
-
<div class="activity-panel"><div class="label">${activityLabel}</div><div id="commentThread" class="comment-thread" aria-live="polite"></div></div>
|
|
1264
1548
|
</div>
|
|
1265
1549
|
<div id="inspectPanel" class="tab-panel">
|
|
1266
1550
|
<div class="panel">
|
|
@@ -1288,11 +1572,22 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1288
1572
|
<div id="status" class="status"></div>
|
|
1289
1573
|
</aside>
|
|
1290
1574
|
</main>
|
|
1575
|
+
<div id="codexLogModal" class="codex-log-modal" aria-hidden="true">
|
|
1576
|
+
<div id="codexLogBackdrop" class="codex-log-backdrop"></div>
|
|
1577
|
+
<section class="codex-log-dialog" role="dialog" aria-modal="true" aria-labelledby="codexLogTitle">
|
|
1578
|
+
<div class="codex-log-head">
|
|
1579
|
+
<h2 id="codexLogTitle" class="codex-log-title">Codex execution log</h2>
|
|
1580
|
+
<button id="codexLogClose" class="codex-log-close" type="button" aria-label="Close execution log">×</button>
|
|
1581
|
+
</div>
|
|
1582
|
+
<div id="codexLogBody" class="codex-log-list"></div>
|
|
1583
|
+
</section>
|
|
1584
|
+
</div>
|
|
1291
1585
|
<script>
|
|
1292
1586
|
(() => {
|
|
1293
1587
|
const token = ${encodedToken};
|
|
1294
1588
|
const defaultMode = ${encodedDefaultMode};
|
|
1295
1589
|
const reviewSurface = ${encodedSurface};
|
|
1590
|
+
const sendButtonHtml = ${encodedSendButtonHtml};
|
|
1296
1591
|
const codexReview = reviewSurface === 'codex';
|
|
1297
1592
|
const COMMENT_STALE_MS = 60000;
|
|
1298
1593
|
const EDITOR_WIDTH_KEY = 'revela-edit-editor-width';
|
|
@@ -1314,9 +1609,13 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1314
1609
|
const state = {
|
|
1315
1610
|
references: [],
|
|
1316
1611
|
pendingComments: [],
|
|
1612
|
+
queuedCommentPolls: new Set(),
|
|
1317
1613
|
hoverEl: null,
|
|
1318
1614
|
hoverOutline: null,
|
|
1319
1615
|
referenceOutlines: [],
|
|
1616
|
+
activeCommentId: '',
|
|
1617
|
+
activeCommentElements: [],
|
|
1618
|
+
commentHighlightOutlines: [],
|
|
1320
1619
|
nextReferenceId: 1,
|
|
1321
1620
|
nextCommentId: 1,
|
|
1322
1621
|
initializedDoc: null,
|
|
@@ -1355,6 +1654,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1355
1654
|
};
|
|
1356
1655
|
const els = {
|
|
1357
1656
|
frame: null,
|
|
1657
|
+
commentHighlightLayer: null,
|
|
1358
1658
|
hitbox: null,
|
|
1359
1659
|
resizeHandle: null,
|
|
1360
1660
|
visualMoveHandle: null,
|
|
@@ -1381,6 +1681,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1381
1681
|
inspectLanguage: null,
|
|
1382
1682
|
inspectCards: null,
|
|
1383
1683
|
inspectStale: null,
|
|
1684
|
+
localAssetToggle: null,
|
|
1685
|
+
localAssetMenu: null,
|
|
1384
1686
|
assetSearchToggle: null,
|
|
1385
1687
|
assetSearchBack: null,
|
|
1386
1688
|
assetSearchView: null,
|
|
@@ -1390,6 +1692,11 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1390
1692
|
assetShuffleButton: null,
|
|
1391
1693
|
assetResults: null,
|
|
1392
1694
|
editSavedAssets: null,
|
|
1695
|
+
codexLogModal: null,
|
|
1696
|
+
codexLogBackdrop: null,
|
|
1697
|
+
codexLogClose: null,
|
|
1698
|
+
codexLogTitle: null,
|
|
1699
|
+
codexLogBody: null,
|
|
1393
1700
|
status: null,
|
|
1394
1701
|
};
|
|
1395
1702
|
|
|
@@ -1405,6 +1712,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1405
1712
|
function boot() {
|
|
1406
1713
|
try {
|
|
1407
1714
|
els.frame = document.getElementById('deck');
|
|
1715
|
+
els.commentHighlightLayer = document.getElementById('commentHighlightLayer');
|
|
1408
1716
|
els.hitbox = document.getElementById('hitbox');
|
|
1409
1717
|
els.resizeHandle = document.getElementById('resizeHandle');
|
|
1410
1718
|
els.visualMoveHandle = document.getElementById('visualMoveHandle');
|
|
@@ -1430,6 +1738,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1430
1738
|
els.inspectButton = document.getElementById('inspectButton');
|
|
1431
1739
|
els.inspectCards = document.getElementById('inspectCards');
|
|
1432
1740
|
els.inspectStale = document.getElementById('inspectStale');
|
|
1741
|
+
els.localAssetToggle = document.getElementById('localAssetToggle');
|
|
1742
|
+
els.localAssetMenu = document.getElementById('localAssetMenu');
|
|
1433
1743
|
els.assetSearchToggle = document.getElementById('assetSearchToggle');
|
|
1434
1744
|
els.assetSearchBack = document.getElementById('assetSearchBack');
|
|
1435
1745
|
els.assetSearchView = document.getElementById('assetSearchView');
|
|
@@ -1439,19 +1749,26 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1439
1749
|
els.assetShuffleButton = document.getElementById('assetShuffleButton');
|
|
1440
1750
|
els.assetResults = document.getElementById('assetResults');
|
|
1441
1751
|
els.editSavedAssets = document.getElementById('editSavedAssets');
|
|
1752
|
+
els.codexLogModal = document.getElementById('codexLogModal');
|
|
1753
|
+
els.codexLogBackdrop = document.getElementById('codexLogBackdrop');
|
|
1754
|
+
els.codexLogClose = document.getElementById('codexLogClose');
|
|
1755
|
+
els.codexLogTitle = document.getElementById('codexLogTitle');
|
|
1756
|
+
els.codexLogBody = document.getElementById('codexLogBody');
|
|
1442
1757
|
els.status = document.getElementById('status');
|
|
1443
1758
|
|
|
1444
1759
|
els.inspectLanguage = document.getElementById('inspectLanguage');
|
|
1445
1760
|
|
|
1446
|
-
if (!els.frame || !els.hitbox || !els.resizeHandle || !els.visualMoveHandle || !els.visualResizeHandle || !els.visualEditToolbar || !els.visualEditCount || !els.visualUndo || !els.visualReset || !els.visualSave || !els.deckPrev || !els.deckNext || !els.deckCounter || !els.selectionSummary || !els.selectionChips || !els.editTab || !els.inspectTab || !els.editPanel || !els.inspectPanel || !els.comment || !els.commentThread || !els.send || !els.inspectComment || !els.inspectButton || !els.inspectLanguage || !els.inspectCards || !els.inspectStale || !els.assetSearchToggle || !els.assetSearchBack || !els.assetSearchView || !els.assetQuery || !els.assetPurpose || !els.assetSearchButton || !els.assetShuffleButton || !els.assetResults || !els.editSavedAssets || !els.status) {
|
|
1761
|
+
if (!els.frame || !els.commentHighlightLayer || !els.hitbox || !els.resizeHandle || !els.visualMoveHandle || !els.visualResizeHandle || !els.visualEditToolbar || !els.visualEditCount || !els.visualUndo || !els.visualReset || !els.visualSave || !els.deckPrev || !els.deckNext || !els.deckCounter || !els.selectionSummary || !els.selectionChips || !els.editTab || !els.inspectTab || !els.editPanel || !els.inspectPanel || !els.comment || !els.commentThread || !els.send || !els.inspectComment || !els.inspectButton || !els.inspectLanguage || !els.inspectCards || !els.inspectStale || !els.localAssetToggle || !els.localAssetMenu || !els.assetSearchToggle || !els.assetSearchBack || !els.assetSearchView || !els.assetQuery || !els.assetPurpose || !els.assetSearchButton || !els.assetShuffleButton || !els.assetResults || !els.editSavedAssets || !els.codexLogModal || !els.codexLogBackdrop || !els.codexLogClose || !els.codexLogTitle || !els.codexLogBody || !els.status) {
|
|
1447
1762
|
throw new Error('Editor boot failed: required DOM nodes are missing.');
|
|
1448
1763
|
}
|
|
1449
1764
|
|
|
1450
1765
|
restoreEditorWidth();
|
|
1451
1766
|
bindEvents();
|
|
1452
1767
|
setMode(state.mode);
|
|
1768
|
+
updateVisualToolbar();
|
|
1453
1769
|
setStatus('Review ready. Ctrl/Cmd + click deck elements to reference them.');
|
|
1454
1770
|
initFrame();
|
|
1771
|
+
loadReviewComments();
|
|
1455
1772
|
loadSavedAssets();
|
|
1456
1773
|
startDeckVersionPolling();
|
|
1457
1774
|
} catch (error) {
|
|
@@ -1463,8 +1780,14 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1463
1780
|
if (state.bound) return;
|
|
1464
1781
|
state.bound = true;
|
|
1465
1782
|
els.frame.addEventListener('load', initFrame);
|
|
1783
|
+
window.addEventListener('resize', renderActiveCommentHighlights);
|
|
1466
1784
|
document.addEventListener('keydown', (event) => {
|
|
1467
1785
|
if (event.key === 'Escape') {
|
|
1786
|
+
if (els.codexLogModal.classList.contains('open')) {
|
|
1787
|
+
closeCodexLogModal();
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
closeLocalAssetMenu();
|
|
1468
1791
|
clearHover();
|
|
1469
1792
|
return;
|
|
1470
1793
|
}
|
|
@@ -1477,6 +1800,12 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1477
1800
|
prevDeckSlide();
|
|
1478
1801
|
}
|
|
1479
1802
|
});
|
|
1803
|
+
document.addEventListener('click', (event) => {
|
|
1804
|
+
if (!els.localAssetMenu.classList.contains('open')) return;
|
|
1805
|
+
const target = event.target;
|
|
1806
|
+
if (target instanceof Node && (els.localAssetMenu.contains(target) || els.localAssetToggle.contains(target))) return;
|
|
1807
|
+
closeLocalAssetMenu();
|
|
1808
|
+
});
|
|
1480
1809
|
els.comment.addEventListener('input', () => {
|
|
1481
1810
|
saveCommentRange();
|
|
1482
1811
|
syncReferencesFromComment(false, els.comment);
|
|
@@ -1507,6 +1836,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1507
1836
|
win.scrollBy({ top: event.deltaY, left: event.deltaX, behavior: 'auto' });
|
|
1508
1837
|
renderHoverOutline(state.hoverEl);
|
|
1509
1838
|
renderReferenceOutlines();
|
|
1839
|
+
renderActiveCommentHighlights();
|
|
1510
1840
|
}, { passive: false });
|
|
1511
1841
|
els.resizeHandle.addEventListener('pointerdown', startEditorResize);
|
|
1512
1842
|
els.resizeHandle.addEventListener('dblclick', resetEditorWidth);
|
|
@@ -1517,10 +1847,13 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1517
1847
|
els.deckNext.addEventListener('click', nextDeckSlide);
|
|
1518
1848
|
els.send.addEventListener('click', sendComment);
|
|
1519
1849
|
els.inspectButton.addEventListener('click', inspectCurrentSelection);
|
|
1850
|
+
els.localAssetToggle.addEventListener('click', toggleLocalAssetMenu);
|
|
1520
1851
|
els.assetSearchToggle.addEventListener('click', toggleAssetSearchPanel);
|
|
1521
1852
|
els.assetSearchBack.addEventListener('click', closeAssetSearchPanel);
|
|
1522
1853
|
els.assetSearchButton.addEventListener('click', () => searchAssets(false));
|
|
1523
1854
|
els.assetShuffleButton.addEventListener('click', () => searchAssets(true));
|
|
1855
|
+
els.codexLogBackdrop.addEventListener('click', closeCodexLogModal);
|
|
1856
|
+
els.codexLogClose.addEventListener('click', closeCodexLogModal);
|
|
1524
1857
|
els.assetQuery.addEventListener('keydown', (event) => {
|
|
1525
1858
|
if (event.key === 'Enter') {
|
|
1526
1859
|
event.preventDefault();
|
|
@@ -1552,6 +1885,20 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1552
1885
|
return state.mode === 'inspect' ? els.inspectComment : els.comment;
|
|
1553
1886
|
}
|
|
1554
1887
|
|
|
1888
|
+
function toggleLocalAssetMenu() {
|
|
1889
|
+
setLocalAssetMenuOpen(!els.localAssetMenu.classList.contains('open'));
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function closeLocalAssetMenu() {
|
|
1893
|
+
setLocalAssetMenuOpen(false);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function setLocalAssetMenuOpen(open) {
|
|
1897
|
+
els.localAssetMenu.classList.toggle('open', open);
|
|
1898
|
+
els.localAssetMenu.setAttribute('aria-hidden', open ? 'false' : 'true');
|
|
1899
|
+
els.localAssetToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1555
1902
|
function toggleAssetSearchPanel() {
|
|
1556
1903
|
const open = !els.assetSearchView.classList.contains('open');
|
|
1557
1904
|
setAssetSearchOpen(open);
|
|
@@ -1562,10 +1909,11 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1562
1909
|
}
|
|
1563
1910
|
|
|
1564
1911
|
function setAssetSearchOpen(open) {
|
|
1912
|
+
if (open) closeLocalAssetMenu();
|
|
1565
1913
|
els.assetSearchView.classList.toggle('open', open);
|
|
1566
1914
|
els.assetSearchView.setAttribute('aria-hidden', open ? 'false' : 'true');
|
|
1567
1915
|
els.assetSearchToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
1568
|
-
els.assetSearchToggle.
|
|
1916
|
+
els.assetSearchToggle.innerHTML = '${lucideIcon("plus")}';
|
|
1569
1917
|
if (open) els.assetQuery.focus();
|
|
1570
1918
|
}
|
|
1571
1919
|
|
|
@@ -1895,7 +2243,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1895
2243
|
function updateVisualToolbar() {
|
|
1896
2244
|
const count = state.visualChanges.length;
|
|
1897
2245
|
els.visualEditToolbar.classList.toggle('active', count > 0);
|
|
1898
|
-
els.visualEditCount.textContent = count + ' unsaved visual change' + (count === 1 ? '' : 's');
|
|
2246
|
+
els.visualEditCount.textContent = count === 0 ? 'No unsaved visual changes' : count + ' unsaved visual change' + (count === 1 ? '' : 's');
|
|
1899
2247
|
els.visualUndo.disabled = count === 0 || state.savingVisualChanges;
|
|
1900
2248
|
els.visualReset.disabled = count === 0 || state.savingVisualChanges;
|
|
1901
2249
|
els.visualSave.disabled = count === 0 || state.savingVisualChanges;
|
|
@@ -1955,17 +2303,21 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1955
2303
|
state.hoverEl = null;
|
|
1956
2304
|
state.hoverOutline = createOutline(doc, '#38bdf8', 'rgba(56,189,248,.12)');
|
|
1957
2305
|
state.assetDropTarget = null;
|
|
1958
|
-
state.assetDropOutline = createOutline(doc, '#
|
|
2306
|
+
state.assetDropOutline = createOutline(doc, '#2563eb', 'rgba(37,99,235,.12)');
|
|
1959
2307
|
state.referenceOutlines = [];
|
|
2308
|
+
state.commentHighlightOutlines.forEach((outline) => outline.remove());
|
|
2309
|
+
state.commentHighlightOutlines = [];
|
|
1960
2310
|
doc.addEventListener('scroll', () => {
|
|
1961
2311
|
renderHoverOutline(state.hoverEl);
|
|
1962
2312
|
renderVisualHandles(state.hoverEl);
|
|
1963
2313
|
renderReferenceOutlines();
|
|
2314
|
+
renderActiveCommentHighlights();
|
|
1964
2315
|
}, true);
|
|
1965
2316
|
const slides = getSlides(doc);
|
|
1966
2317
|
syncDeckNavigation();
|
|
1967
2318
|
restoreDeckSlideAfterRefresh();
|
|
1968
2319
|
updateSendState();
|
|
2320
|
+
renderActiveCommentHighlights();
|
|
1969
2321
|
if (state.pendingRefreshMessage) {
|
|
1970
2322
|
state.pendingRefreshMessage = false;
|
|
1971
2323
|
setStatus('Deck updated. Preview refreshed. Element references were cleared.');
|
|
@@ -2050,6 +2402,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2050
2402
|
else renderHoverOutline(state.hoverEl);
|
|
2051
2403
|
renderVisualHandles(state.hoverEl);
|
|
2052
2404
|
renderReferenceOutlines();
|
|
2405
|
+
renderActiveCommentHighlights();
|
|
2053
2406
|
} catch (error) {
|
|
2054
2407
|
reportError(error);
|
|
2055
2408
|
}
|
|
@@ -2130,44 +2483,18 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2130
2483
|
const { body, version: nextVersion } = await fetchDeckVersion();
|
|
2131
2484
|
if (!state.deckVersion) {
|
|
2132
2485
|
state.deckVersion = nextVersion;
|
|
2133
|
-
markCommentsUpdatedForVersion(nextVersion);
|
|
2134
|
-
markStaleComments();
|
|
2135
2486
|
return;
|
|
2136
2487
|
}
|
|
2137
2488
|
if (state.deckVersion === nextVersion) {
|
|
2138
|
-
markStaleComments();
|
|
2139
2489
|
return;
|
|
2140
2490
|
}
|
|
2141
2491
|
state.deckVersion = nextVersion;
|
|
2142
|
-
markCommentsUpdatedForVersion(nextVersion);
|
|
2143
2492
|
refreshDeckPreview(body.mtimeMs);
|
|
2144
2493
|
} catch (error) {
|
|
2145
2494
|
reportError(error);
|
|
2146
2495
|
}
|
|
2147
2496
|
}
|
|
2148
2497
|
|
|
2149
|
-
async function watchDeckVersionAfterComment(commentId) {
|
|
2150
|
-
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
2151
|
-
const baseDeckVersion = comment?.baseDeckVersion || state.deckVersion;
|
|
2152
|
-
const started = Date.now();
|
|
2153
|
-
while (Date.now() - started < 15000) {
|
|
2154
|
-
if (pendingCommentStatus(commentId) === 'updated' || pendingCommentStatus(commentId) === 'failed') return;
|
|
2155
|
-
await delay(250);
|
|
2156
|
-
try {
|
|
2157
|
-
const { body, version: nextVersion } = await fetchDeckVersion();
|
|
2158
|
-
if (nextVersion && nextVersion !== baseDeckVersion) {
|
|
2159
|
-
state.deckVersion = nextVersion;
|
|
2160
|
-
markCommentsUpdatedForVersion(nextVersion);
|
|
2161
|
-
refreshDeckPreview(body.mtimeMs);
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
} catch (error) {
|
|
2165
|
-
reportError(error);
|
|
2166
|
-
return;
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
2498
|
function refreshDeckPreview(version) {
|
|
2172
2499
|
state.pendingRefreshMessage = true;
|
|
2173
2500
|
state.pendingDeckSlideRestore = state.deckSlideIndex;
|
|
@@ -2184,6 +2511,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2184
2511
|
if (state.assetDropOutline) state.assetDropOutline.style.display = 'none';
|
|
2185
2512
|
state.referenceOutlines.forEach((outline) => outline.style.display = 'none');
|
|
2186
2513
|
state.referenceOutlines = [];
|
|
2514
|
+
state.commentHighlightOutlines.forEach((outline) => outline.remove());
|
|
2515
|
+
state.commentHighlightOutlines = [];
|
|
2187
2516
|
updateSendState();
|
|
2188
2517
|
els.frame.src = '/deck?token=' + encodeURIComponent(token) + '&v=' + encodeURIComponent(String(version));
|
|
2189
2518
|
setStatus('Deck changed. Refreshing preview...');
|
|
@@ -2249,27 +2578,24 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2249
2578
|
if (!text) return;
|
|
2250
2579
|
const elements = state.references.map((reference) => reference.payload);
|
|
2251
2580
|
const asset = state.selectedAsset || undefined;
|
|
2252
|
-
const commentId = addPendingComment(text, elements, 'sending');
|
|
2253
|
-
clearReferences(false);
|
|
2254
|
-
state.selectedAsset = null;
|
|
2255
|
-
els.comment.textContent = '';
|
|
2256
|
-
renderReferenceOutlines();
|
|
2257
2581
|
state.sendingEdit = true;
|
|
2258
2582
|
updateSendState();
|
|
2259
|
-
setStatus('
|
|
2583
|
+
setStatus('Saving comment...');
|
|
2260
2584
|
try {
|
|
2261
|
-
const res = await fetch('/api/
|
|
2585
|
+
const res = await fetch('/api/comments?token=' + encodeURIComponent(token), {
|
|
2262
2586
|
method: 'POST',
|
|
2263
2587
|
headers: { 'content-type': 'application/json' },
|
|
2264
2588
|
body: JSON.stringify({ comment: text, elements, asset }),
|
|
2265
2589
|
});
|
|
2266
2590
|
const body = await res.json().catch(() => ({}));
|
|
2267
|
-
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2591
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to save comment');
|
|
2592
|
+
upsertPersistedComment(body.comment);
|
|
2593
|
+
clearReferences(false);
|
|
2594
|
+
state.selectedAsset = null;
|
|
2595
|
+
els.comment.textContent = '';
|
|
2596
|
+
renderReferenceOutlines();
|
|
2597
|
+
setStatus('Comment saved. Use Apply on the comment card when you want Codex to edit the deck.');
|
|
2271
2598
|
} catch (error) {
|
|
2272
|
-
updatePendingCommentStatus(commentId, 'failed');
|
|
2273
2599
|
reportError(error);
|
|
2274
2600
|
} finally {
|
|
2275
2601
|
state.sendingEdit = false;
|
|
@@ -2277,6 +2603,150 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2277
2603
|
}
|
|
2278
2604
|
}
|
|
2279
2605
|
|
|
2606
|
+
async function loadReviewComments() {
|
|
2607
|
+
try {
|
|
2608
|
+
const res = await fetch('/api/comments?token=' + encodeURIComponent(token), { cache: 'no-store' });
|
|
2609
|
+
const body = await res.json().catch(() => ({}));
|
|
2610
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to load comments');
|
|
2611
|
+
state.pendingComments = Array.isArray(body.comments) ? body.comments.map(commentFromRecord) : [];
|
|
2612
|
+
renderCommentThread();
|
|
2613
|
+
state.pendingComments.forEach((comment) => {
|
|
2614
|
+
if (comment.status === 'queued') pollQueuedComment(comment.id);
|
|
2615
|
+
else if (comment.status === 'applying' && comment.requestId) watchCommentProgress(comment.id, comment.requestId);
|
|
2616
|
+
});
|
|
2617
|
+
} catch (error) {
|
|
2618
|
+
reportError(error);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
function upsertPersistedComment(record) {
|
|
2623
|
+
if (!record || !record.id) return;
|
|
2624
|
+
const next = commentFromRecord(record);
|
|
2625
|
+
const index = state.pendingComments.findIndex((item) => item.id === next.id);
|
|
2626
|
+
if (index >= 0) state.pendingComments[index] = { ...state.pendingComments[index], ...next };
|
|
2627
|
+
else state.pendingComments.push(next);
|
|
2628
|
+
if (state.activeCommentId === next.id) state.activeCommentElements = Array.isArray(next.elements) ? next.elements : [];
|
|
2629
|
+
state.pendingComments.sort((a, b) => (a.slideIndex || 0) - (b.slideIndex || 0) || (a.createdAt || 0) - (b.createdAt || 0));
|
|
2630
|
+
renderCommentThread();
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
function commentFromRecord(record) {
|
|
2634
|
+
const createdAt = Date.parse(record.createdAt || '') || Date.now();
|
|
2635
|
+
return {
|
|
2636
|
+
id: record.id,
|
|
2637
|
+
persisted: true,
|
|
2638
|
+
text: record.comment || '',
|
|
2639
|
+
elements: Array.isArray(record.elements) ? record.elements : [],
|
|
2640
|
+
slideIndex: record.slideIndex,
|
|
2641
|
+
status: record.status || 'open',
|
|
2642
|
+
createdAt,
|
|
2643
|
+
baseDeckVersion: record.deckVersion || state.deckVersion,
|
|
2644
|
+
updatedVersion: null,
|
|
2645
|
+
requestId: record.lastApplyRequestId || '',
|
|
2646
|
+
progressEvent: null,
|
|
2647
|
+
eventLog: [],
|
|
2648
|
+
failureRaw: record.lastApplyRaw || '',
|
|
2649
|
+
failureMessage: record.lastApplyError || '',
|
|
2650
|
+
stopped: record.status === 'failed' && record.lastApplyError === 'Stopped by user.',
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
async function applyPersistedComment(commentId) {
|
|
2655
|
+
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
2656
|
+
if (!comment || comment.status === 'applying' || comment.status === 'queued') return;
|
|
2657
|
+
updatePendingCommentStatus(commentId, 'applying', { baseDeckVersion: state.deckVersion || comment.baseDeckVersion, progressEvent: null, eventLog: [], failureRaw: '', failureMessage: '' });
|
|
2658
|
+
setStatus(comment.status === 'applied' || comment.status === 'updated' || comment.status === 'stale' ? 'Re-applying saved comment...' : 'Applying saved comment...');
|
|
2659
|
+
try {
|
|
2660
|
+
const res = await fetch('/api/comments/' + encodeURIComponent(commentId) + '/apply?token=' + encodeURIComponent(token), {
|
|
2661
|
+
method: 'POST',
|
|
2662
|
+
headers: { 'content-type': 'application/json' },
|
|
2663
|
+
body: JSON.stringify({}),
|
|
2664
|
+
});
|
|
2665
|
+
const body = await res.json().catch(() => ({}));
|
|
2666
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to apply comment');
|
|
2667
|
+
if (body.comment) upsertPersistedComment(body.comment);
|
|
2668
|
+
if (body.status === 'queued') {
|
|
2669
|
+
updatePendingCommentStatus(commentId, 'queued', { requestId: '', progressEvent: null, failureRaw: '', failureMessage: '' });
|
|
2670
|
+
setStatus('Comment queued. It will apply after the current edit finishes.');
|
|
2671
|
+
pollQueuedComment(commentId);
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
updatePendingCommentStatus(commentId, 'applying', { requestId: body.commentRequestId || body.requestId || '', baseDeckVersion: body.deckVersion || state.deckVersion });
|
|
2675
|
+
if (body.commentRequestId || body.requestId) watchCommentProgress(commentId, body.commentRequestId || body.requestId);
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
updatePendingCommentStatus(commentId, 'failed', { failureMessage: error instanceof Error ? error.message : String(error) });
|
|
2678
|
+
reportError(error);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
async function stopPersistedComment(commentId) {
|
|
2683
|
+
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
2684
|
+
if (!comment || !canStopPersistedComment(comment.status)) return;
|
|
2685
|
+
try {
|
|
2686
|
+
const res = await fetch('/api/comments/' + encodeURIComponent(commentId) + '/stop?token=' + encodeURIComponent(token), { method: 'POST' });
|
|
2687
|
+
const body = await res.json().catch(() => ({}));
|
|
2688
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to stop comment');
|
|
2689
|
+
if (body.comment) upsertPersistedComment(body.comment);
|
|
2690
|
+
updatePendingCommentStatus(commentId, 'failed', { stopped: true, requestId: '', progressEvent: null, failureRaw: 'Stopped by user.', failureMessage: 'Stopped by user.' });
|
|
2691
|
+
setStatus('Stopped comment apply.');
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
reportError(error);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
async function deletePersistedComment(commentId) {
|
|
2698
|
+
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
2699
|
+
if (!comment || !canDeletePersistedComment(comment.status)) return;
|
|
2700
|
+
try {
|
|
2701
|
+
const res = await fetch('/api/comments/' + encodeURIComponent(commentId) + '?token=' + encodeURIComponent(token), { method: 'DELETE' });
|
|
2702
|
+
const body = await res.json().catch(() => ({}));
|
|
2703
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to delete comment');
|
|
2704
|
+
state.pendingComments = state.pendingComments.filter((item) => item.id !== commentId);
|
|
2705
|
+
if (state.activeCommentId === commentId) {
|
|
2706
|
+
state.activeCommentId = '';
|
|
2707
|
+
state.activeCommentElements = [];
|
|
2708
|
+
state.commentHighlightOutlines.forEach((outline) => renderParentBox(outline, null));
|
|
2709
|
+
}
|
|
2710
|
+
renderCommentThread();
|
|
2711
|
+
setStatus('Deleted comment.');
|
|
2712
|
+
} catch (error) {
|
|
2713
|
+
reportError(error);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
function pollQueuedComment(commentId) {
|
|
2718
|
+
if (state.queuedCommentPolls.has(commentId)) return;
|
|
2719
|
+
state.queuedCommentPolls.add(commentId);
|
|
2720
|
+
const tick = async () => {
|
|
2721
|
+
const current = state.pendingComments.find((item) => item.id === commentId);
|
|
2722
|
+
if (!current || current.status !== 'queued') {
|
|
2723
|
+
state.queuedCommentPolls.delete(commentId);
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
try {
|
|
2727
|
+
const res = await fetch('/api/comments?token=' + encodeURIComponent(token), { cache: 'no-store' });
|
|
2728
|
+
const body = await res.json().catch(() => ({}));
|
|
2729
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to refresh comments');
|
|
2730
|
+
const records = Array.isArray(body.comments) ? body.comments : [];
|
|
2731
|
+
records.forEach(upsertPersistedComment);
|
|
2732
|
+
const record = records.find((item) => item && item.id === commentId);
|
|
2733
|
+
if (record && record.status === 'applying' && record.lastApplyRequestId) {
|
|
2734
|
+
state.queuedCommentPolls.delete(commentId);
|
|
2735
|
+
watchCommentProgress(commentId, record.lastApplyRequestId);
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (record && record.status !== 'queued') {
|
|
2739
|
+
state.queuedCommentPolls.delete(commentId);
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
} catch (error) {
|
|
2743
|
+
// Keep polling: queued comments become actionable only when the server starts them.
|
|
2744
|
+
}
|
|
2745
|
+
setTimeout(tick, 1500);
|
|
2746
|
+
};
|
|
2747
|
+
setTimeout(tick, 750);
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2280
2750
|
async function searchAssets(nextBatch) {
|
|
2281
2751
|
const query = (els.assetQuery.value || '').trim();
|
|
2282
2752
|
if (!query || state.assetSearchBusy) return;
|
|
@@ -2431,7 +2901,10 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2431
2901
|
state.savedAssets.forEach((asset) => {
|
|
2432
2902
|
const card = assetCard(asset, true, 0);
|
|
2433
2903
|
card.draggable = true;
|
|
2434
|
-
card.addEventListener('click', () =>
|
|
2904
|
+
card.addEventListener('click', () => {
|
|
2905
|
+
addAssetToComment(asset);
|
|
2906
|
+
closeLocalAssetMenu();
|
|
2907
|
+
});
|
|
2435
2908
|
card.addEventListener('dragstart', (event) => {
|
|
2436
2909
|
state.draggingAsset = asset;
|
|
2437
2910
|
event.dataTransfer?.setData('application/revela-asset-id', asset.id || '');
|
|
@@ -2499,7 +2972,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2499
2972
|
insertPlainText(' ');
|
|
2500
2973
|
setMode('edit');
|
|
2501
2974
|
updateSendState();
|
|
2502
|
-
|
|
2975
|
+
closeLocalAssetMenu();
|
|
2976
|
+
setStatus('Asset added to the Edit comment. Describe where or how to use it, then Leave Comment.');
|
|
2503
2977
|
}
|
|
2504
2978
|
|
|
2505
2979
|
function onAssetDragOver(event) {
|
|
@@ -2578,22 +3052,111 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2578
3052
|
}
|
|
2579
3053
|
|
|
2580
3054
|
function elementFromPayload(payload) {
|
|
3055
|
+
const resolved = resolveElementFromPayload(payload);
|
|
3056
|
+
return resolved.target;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
function resolveElementFromPayload(payload) {
|
|
2581
3060
|
const doc = els.frame.contentDocument;
|
|
2582
|
-
if (!doc || !payload) return null;
|
|
3061
|
+
if (!doc || !payload) return { target: null, matchedSelector: false, strategy: 'none' };
|
|
2583
3062
|
const slides = getSlides(doc);
|
|
2584
3063
|
const slide = slides.find((item, index) => {
|
|
2585
3064
|
const explicit = Number(item.getAttribute('data-slide-index'));
|
|
2586
3065
|
const slideIndex = Number.isFinite(explicit) && explicit > 0 ? explicit : index + 1;
|
|
2587
3066
|
return slideIndex === payload.slideIndex;
|
|
2588
3067
|
});
|
|
2589
|
-
if (!slide) return null;
|
|
3068
|
+
if (!slide) return { target: null, matchedSelector: false, strategy: 'none' };
|
|
2590
3069
|
if (payload.selector) {
|
|
3070
|
+
try {
|
|
3071
|
+
const selected = doc.querySelector(payload.selector);
|
|
3072
|
+
if (selected && (selected === slide || slide.contains(selected))) return { target: selected, matchedSelector: true, strategy: 'selector' };
|
|
3073
|
+
} catch {}
|
|
2591
3074
|
try {
|
|
2592
3075
|
const selected = slide.querySelector(payload.selector);
|
|
2593
|
-
if (selected) return selected;
|
|
3076
|
+
if (selected) return { target: selected, matchedSelector: true, strategy: 'selector' };
|
|
2594
3077
|
} catch {}
|
|
2595
3078
|
}
|
|
2596
|
-
|
|
3079
|
+
const fallback = resolveElementByFingerprint(slide, payload);
|
|
3080
|
+
if (fallback) return { target: fallback.target, matchedSelector: false, strategy: fallback.strategy, score: fallback.score };
|
|
3081
|
+
return { target: null, matchedSelector: false, strategy: 'missing' };
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
function resolveElementByFingerprint(slide, payload) {
|
|
3085
|
+
const candidates = Array.from(slide.querySelectorAll('*'));
|
|
3086
|
+
if (payload.tagName && String(payload.tagName).toLowerCase() === slide.tagName.toLowerCase()) candidates.unshift(slide);
|
|
3087
|
+
let best = null;
|
|
3088
|
+
candidates.forEach((candidate) => {
|
|
3089
|
+
const match = scoreTargetCandidate(candidate, slide, payload);
|
|
3090
|
+
if (!match) return;
|
|
3091
|
+
if (!best || match.score > best.score) best = match;
|
|
3092
|
+
});
|
|
3093
|
+
return best && best.score >= 55 ? best : null;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
function scoreTargetCandidate(candidate, slide, payload) {
|
|
3097
|
+
const candidatePayload = targetIdentityPayload(candidate, slide);
|
|
3098
|
+
let score = 0;
|
|
3099
|
+
let strategy = '';
|
|
3100
|
+
const payloadFingerprint = payload.fingerprint || {};
|
|
3101
|
+
const candidateFingerprint = candidatePayload.fingerprint || {};
|
|
3102
|
+
if (payloadFingerprint.contentHash && payloadFingerprint.contentHash === candidateFingerprint.contentHash) {
|
|
3103
|
+
score += 120;
|
|
3104
|
+
strategy = 'fingerprint';
|
|
3105
|
+
}
|
|
3106
|
+
if (payloadFingerprint.textHash && payloadFingerprint.textHash === candidateFingerprint.textHash) {
|
|
3107
|
+
score += 80;
|
|
3108
|
+
strategy = strategy || 'fingerprint';
|
|
3109
|
+
}
|
|
3110
|
+
if (payloadFingerprint.structureHash && payloadFingerprint.structureHash === candidateFingerprint.structureHash) {
|
|
3111
|
+
score += 40;
|
|
3112
|
+
strategy = strategy || 'structure';
|
|
3113
|
+
}
|
|
3114
|
+
if (payloadFingerprint.contextHash && payloadFingerprint.contextHash === candidateFingerprint.contextHash) {
|
|
3115
|
+
score += 20;
|
|
3116
|
+
strategy = strategy || 'fingerprint';
|
|
3117
|
+
}
|
|
3118
|
+
const payloadTag = String(payload.tagName || '').toLowerCase();
|
|
3119
|
+
if (payloadTag && payloadTag === candidatePayload.tagName) score += 32;
|
|
3120
|
+
const payloadText = payload.textNormalized || normalizeTargetText(payload.text);
|
|
3121
|
+
if (payloadText && payloadText === candidatePayload.textNormalized) {
|
|
3122
|
+
score += 72;
|
|
3123
|
+
strategy = strategy || 'text';
|
|
3124
|
+
} else if (payloadText && candidatePayload.textNormalized && (payloadText.includes(candidatePayload.textNormalized) || candidatePayload.textNormalized.includes(payloadText))) {
|
|
3125
|
+
score += 34;
|
|
3126
|
+
strategy = strategy || 'text';
|
|
3127
|
+
}
|
|
3128
|
+
if (payload.semanticKind && payload.semanticKind === candidatePayload.semanticKind) score += 18;
|
|
3129
|
+
const classOverlap = overlapCount(payload.classList, candidatePayload.classList);
|
|
3130
|
+
score += Math.min(24, classOverlap * 8);
|
|
3131
|
+
if (payloadFingerprint.structureHash && payloadFingerprint.structureHash === candidateFingerprint.structureHash && (classOverlap > 0 || partialTextOverlap(payloadText, candidatePayload.textNormalized))) {
|
|
3132
|
+
score += 32;
|
|
3133
|
+
strategy = strategy || 'structure';
|
|
3134
|
+
}
|
|
3135
|
+
const distance = relativeBoxDistance(payload.slideRelativeBox, candidatePayload.slideRelativeBox);
|
|
3136
|
+
if (Number.isFinite(distance)) {
|
|
3137
|
+
const positionScore = Math.max(0, 36 - distance * 72);
|
|
3138
|
+
score += positionScore;
|
|
3139
|
+
if (!strategy && positionScore > 20) strategy = 'relativeBox';
|
|
3140
|
+
}
|
|
3141
|
+
if (payloadTag && payloadTag !== candidatePayload.tagName) score -= 20;
|
|
3142
|
+
return score > 0 ? { target: candidate, score, strategy: strategy || 'relativeBox' } : null;
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
function targetIdentityPayload(el, slide) {
|
|
3146
|
+
const semanticKind = semanticKindForElement(el);
|
|
3147
|
+
const tagName = el.tagName.toLowerCase();
|
|
3148
|
+
const classList = Array.from(el.classList || []);
|
|
3149
|
+
const textNormalized = normalizeTargetText(el.innerText || el.textContent || '');
|
|
3150
|
+
const slideTitle = slide ? ((slide.querySelector('h1,h2,h3,[data-title]') || {}).textContent || '').trim().slice(0, 160) : '';
|
|
3151
|
+
const nearbyText = slide ? (slide.innerText || slide.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 1200) : '';
|
|
3152
|
+
return {
|
|
3153
|
+
tagName,
|
|
3154
|
+
semanticKind,
|
|
3155
|
+
classList,
|
|
3156
|
+
textNormalized,
|
|
3157
|
+
slideRelativeBox: slideRelativeBox(el, slide),
|
|
3158
|
+
fingerprint: fingerprintForTarget({ tagName, semanticKind, classList, textNormalized, slideTitle, nearbyText }),
|
|
3159
|
+
};
|
|
2597
3160
|
}
|
|
2598
3161
|
|
|
2599
3162
|
function elementFromVisualChange(change) {
|
|
@@ -2615,28 +3178,25 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2615
3178
|
: 'add it near the drop point';
|
|
2616
3179
|
const comment = 'Place workspace asset ' + asset.path + ' on slide ' + placement.slideIndex + ' as a ' + (asset.purpose || 'visual asset') + '; ' + modeText + '. Preserve the current layout and do not cover existing text, charts, tables, or evidence.';
|
|
2617
3180
|
const elements = placement.target ? [placement.target] : [];
|
|
2618
|
-
|
|
2619
|
-
setStatus('Sending asset placement comment...');
|
|
3181
|
+
setStatus('Saving asset placement comment...');
|
|
2620
3182
|
try {
|
|
2621
|
-
const res = await fetch('/api/
|
|
3183
|
+
const res = await fetch('/api/comments?token=' + encodeURIComponent(token), {
|
|
2622
3184
|
method: 'POST',
|
|
2623
3185
|
headers: { 'content-type': 'application/json' },
|
|
2624
3186
|
body: JSON.stringify({ comment, elements, asset, drop: placement }),
|
|
2625
3187
|
});
|
|
2626
3188
|
const body = await res.json().catch(() => ({}));
|
|
2627
|
-
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
if (body.commentRequestId || body.requestId) watchCommentProgress(commentId, body.commentRequestId || body.requestId);
|
|
3189
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to save asset placement');
|
|
3190
|
+
upsertPersistedComment(body.comment);
|
|
3191
|
+
setStatus('Asset placement comment saved. Use Apply on the comment card when ready.');
|
|
2631
3192
|
} catch (error) {
|
|
2632
|
-
updatePendingCommentStatus(commentId, 'failed');
|
|
2633
3193
|
reportError(error);
|
|
2634
3194
|
}
|
|
2635
3195
|
}
|
|
2636
3196
|
|
|
2637
3197
|
function selectable(node) {
|
|
2638
3198
|
if (!node || node.nodeType !== 1) return null;
|
|
2639
|
-
if (node === state.hoverOutline || state.referenceOutlines.includes(node)) return null;
|
|
3199
|
+
if (node === state.hoverOutline || node === state.assetDropOutline || state.referenceOutlines.includes(node)) return null;
|
|
2640
3200
|
return findSlide(node) ? node : null;
|
|
2641
3201
|
}
|
|
2642
3202
|
|
|
@@ -2726,41 +3286,10 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2726
3286
|
if (comment.status === 'updated' && status !== 'failed') return;
|
|
2727
3287
|
comment.status = status;
|
|
2728
3288
|
if (updates) Object.assign(comment, updates);
|
|
2729
|
-
if (status === 'updated' || status === 'failed') comment.progressEvent = null;
|
|
3289
|
+
if (status === 'updated' || status === 'failed' || status === 'applied') comment.progressEvent = null;
|
|
2730
3290
|
renderCommentThread();
|
|
2731
3291
|
}
|
|
2732
3292
|
|
|
2733
|
-
function markCommentsUpdatedForVersion(version) {
|
|
2734
|
-
let changed = false;
|
|
2735
|
-
state.pendingComments.forEach((comment) => {
|
|
2736
|
-
if ((comment.status === 'sent' || comment.status === 'sending' || comment.status === 'stale') && comment.baseDeckVersion !== version) {
|
|
2737
|
-
comment.status = 'updated';
|
|
2738
|
-
comment.updatedVersion = version;
|
|
2739
|
-
comment.progressEvent = null;
|
|
2740
|
-
changed = true;
|
|
2741
|
-
}
|
|
2742
|
-
});
|
|
2743
|
-
if (changed) {
|
|
2744
|
-
renderCommentThread();
|
|
2745
|
-
setStatus('Deck file updated. Preview will refresh automatically.');
|
|
2746
|
-
}
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
|
-
function markStaleComments() {
|
|
2750
|
-
const now = Date.now();
|
|
2751
|
-
let changed = false;
|
|
2752
|
-
state.pendingComments.forEach((comment) => {
|
|
2753
|
-
if (comment.status !== 'sent' && comment.status !== 'sending') return;
|
|
2754
|
-
if (now - comment.createdAt < COMMENT_STALE_MS) return;
|
|
2755
|
-
comment.status = 'stale';
|
|
2756
|
-
changed = true;
|
|
2757
|
-
});
|
|
2758
|
-
if (changed) {
|
|
2759
|
-
renderCommentThread();
|
|
2760
|
-
setStatus('Still waiting for deck file update. The preview will refresh automatically when the file changes.');
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
3293
|
function pendingCommentStatus(id) {
|
|
2765
3294
|
return state.pendingComments.find((comment) => comment.id === id)?.status || '';
|
|
2766
3295
|
}
|
|
@@ -2780,6 +3309,10 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2780
3309
|
return;
|
|
2781
3310
|
}
|
|
2782
3311
|
if (body.status === 'completed') {
|
|
3312
|
+
if (pendingCommentStatus(commentId) === 'applying') {
|
|
3313
|
+
updatePendingCommentStatus(commentId, 'applied', { progressEvent: null });
|
|
3314
|
+
}
|
|
3315
|
+
setStatus('Codex completed.');
|
|
2783
3316
|
return;
|
|
2784
3317
|
}
|
|
2785
3318
|
} catch (error) {
|
|
@@ -2829,8 +3362,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2829
3362
|
} else if (payload.type === 'completed') {
|
|
2830
3363
|
closed = true;
|
|
2831
3364
|
source.close();
|
|
2832
|
-
|
|
2833
|
-
watchDeckVersionAfterComment(commentId);
|
|
3365
|
+
setStatus(payload.message || 'Codex completed.');
|
|
2834
3366
|
} else if (payload.message) {
|
|
2835
3367
|
setStatus(payload.message);
|
|
2836
3368
|
}
|
|
@@ -2846,11 +3378,13 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2846
3378
|
function recordCommentProgress(commentId, event) {
|
|
2847
3379
|
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
2848
3380
|
if (!comment || !event || !event.message) return;
|
|
3381
|
+
if (comment.stopped || comment.status === 'failed' && comment.failureMessage === 'Stopped by user.') return;
|
|
2849
3382
|
if (codexReview) {
|
|
2850
3383
|
appendCodexEventLog(comment, event);
|
|
2851
3384
|
}
|
|
2852
3385
|
if (event.type === 'completed') {
|
|
2853
3386
|
comment.progressEvent = null;
|
|
3387
|
+
if (comment.persisted && comment.status === 'applying') comment.status = 'applied';
|
|
2854
3388
|
if (codexReview) renderCommentThread();
|
|
2855
3389
|
return;
|
|
2856
3390
|
}
|
|
@@ -2891,6 +3425,13 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2891
3425
|
details.className = 'codex-log';
|
|
2892
3426
|
const summary = document.createElement('summary');
|
|
2893
3427
|
summary.textContent = codexLogSummary(log);
|
|
3428
|
+
const list = renderCodexLogEntries(log);
|
|
3429
|
+
details.appendChild(summary);
|
|
3430
|
+
details.appendChild(list);
|
|
3431
|
+
return details;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
function renderCodexLogEntries(log) {
|
|
2894
3435
|
const list = document.createElement('div');
|
|
2895
3436
|
list.className = 'codex-log-list';
|
|
2896
3437
|
log.forEach((item) => {
|
|
@@ -2917,27 +3458,90 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2917
3458
|
}
|
|
2918
3459
|
list.appendChild(row);
|
|
2919
3460
|
});
|
|
2920
|
-
|
|
2921
|
-
details.appendChild(list);
|
|
2922
|
-
return details;
|
|
3461
|
+
return list;
|
|
2923
3462
|
}
|
|
2924
3463
|
|
|
2925
|
-
function
|
|
3464
|
+
function openCodexLogModal(log) {
|
|
3465
|
+
if (!codexReview || !Array.isArray(log) || !log.length) return;
|
|
3466
|
+
els.codexLogTitle.textContent = codexLogSummary(log);
|
|
3467
|
+
els.codexLogBody.replaceChildren(...Array.from(renderCodexLogEntries(log).children));
|
|
3468
|
+
els.codexLogModal.classList.add('open');
|
|
3469
|
+
els.codexLogModal.setAttribute('aria-hidden', 'false');
|
|
3470
|
+
els.codexLogClose.focus();
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
function closeCodexLogModal() {
|
|
3474
|
+
els.codexLogModal.classList.remove('open');
|
|
3475
|
+
els.codexLogModal.setAttribute('aria-hidden', 'true');
|
|
3476
|
+
els.codexLogBody.textContent = '';
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
function renderCommentThread(scrollToBottom = true) {
|
|
2926
3480
|
els.commentThread.textContent = '';
|
|
2927
3481
|
state.pendingComments.forEach((comment) => {
|
|
2928
3482
|
const bubble = document.createElement('div');
|
|
2929
|
-
bubble.className = 'comment-bubble ' + comment.status;
|
|
3483
|
+
bubble.className = 'comment-bubble ' + comment.status + (comment.id === state.activeCommentId ? ' active' : '');
|
|
3484
|
+
bubble.tabIndex = 0;
|
|
3485
|
+
bubble.setAttribute('role', 'button');
|
|
3486
|
+
bubble.setAttribute('aria-pressed', comment.id === state.activeCommentId ? 'true' : 'false');
|
|
3487
|
+
bubble.addEventListener('click', () => selectPersistedComment(comment.id));
|
|
3488
|
+
bubble.addEventListener('keydown', (event) => {
|
|
3489
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
3490
|
+
event.preventDefault();
|
|
3491
|
+
selectPersistedComment(comment.id);
|
|
3492
|
+
}
|
|
3493
|
+
});
|
|
3494
|
+
|
|
3495
|
+
const meta = document.createElement('div');
|
|
3496
|
+
meta.className = 'comment-bubble-meta';
|
|
3497
|
+
meta.textContent = 'Slide ' + (comment.slideIndex || slideIndexFromElements(comment.elements) || '?') + (comment.baseDeckVersion && state.deckVersion && comment.baseDeckVersion !== state.deckVersion && comment.status === 'open' ? ' · stale deck version' : '');
|
|
2930
3498
|
|
|
2931
3499
|
const text = document.createElement('div');
|
|
2932
3500
|
text.className = 'comment-bubble-text';
|
|
2933
3501
|
text.textContent = comment.text;
|
|
2934
3502
|
|
|
3503
|
+
const refs = renderCommentReferenceChips(comment.elements);
|
|
3504
|
+
|
|
2935
3505
|
const status = document.createElement('div');
|
|
2936
3506
|
status.className = 'comment-bubble-state';
|
|
2937
3507
|
status.textContent = commentStatusLabel(comment.status);
|
|
2938
3508
|
|
|
3509
|
+
bubble.appendChild(meta);
|
|
3510
|
+
if (refs) bubble.appendChild(refs);
|
|
2939
3511
|
bubble.appendChild(text);
|
|
2940
3512
|
bubble.appendChild(status);
|
|
3513
|
+
if (comment.persisted && canApplyPersistedComment(comment.status)) {
|
|
3514
|
+
const actions = document.createElement('div');
|
|
3515
|
+
actions.className = 'comment-actions';
|
|
3516
|
+
const apply = commentActionButton(isReapplyStatus(comment.status) ? 'Re-apply' : 'Apply', isReapplyStatus(comment.status) ? '${lucideIcon("refresh-cw", "comment-action-icon")}' : '${lucideIcon("play", "comment-action-icon")}');
|
|
3517
|
+
apply.addEventListener('click', () => applyPersistedComment(comment.id));
|
|
3518
|
+
actions.appendChild(apply);
|
|
3519
|
+
bubble.appendChild(actions);
|
|
3520
|
+
}
|
|
3521
|
+
if (comment.persisted && canStopPersistedComment(comment.status)) {
|
|
3522
|
+
const actions = bubble.querySelector('.comment-actions') || document.createElement('div');
|
|
3523
|
+
actions.className = 'comment-actions';
|
|
3524
|
+
const stop = commentActionButton('Stop', '${lucideIcon("square", "comment-action-icon")}', 'stop');
|
|
3525
|
+
stop.addEventListener('click', () => stopPersistedComment(comment.id));
|
|
3526
|
+
actions.appendChild(stop);
|
|
3527
|
+
if (!actions.parentElement) bubble.appendChild(actions);
|
|
3528
|
+
}
|
|
3529
|
+
if (comment.persisted && canDeletePersistedComment(comment.status)) {
|
|
3530
|
+
const actions = bubble.querySelector('.comment-actions') || document.createElement('div');
|
|
3531
|
+
actions.className = 'comment-actions';
|
|
3532
|
+
const remove = commentActionButton('Delete', '${lucideIcon("trash-2", "comment-action-icon")}', 'danger');
|
|
3533
|
+
remove.addEventListener('click', () => deletePersistedComment(comment.id));
|
|
3534
|
+
actions.appendChild(remove);
|
|
3535
|
+
if (!actions.parentElement) bubble.appendChild(actions);
|
|
3536
|
+
}
|
|
3537
|
+
if (codexReview && Array.isArray(comment.eventLog) && comment.eventLog.length) {
|
|
3538
|
+
const actions = bubble.querySelector('.comment-actions') || document.createElement('div');
|
|
3539
|
+
actions.className = 'comment-actions';
|
|
3540
|
+
const log = commentActionButton('Execution Log', '${lucideIcon("list", "comment-action-icon")}', 'log');
|
|
3541
|
+
log.addEventListener('click', () => openCodexLogModal(comment.eventLog));
|
|
3542
|
+
actions.appendChild(log);
|
|
3543
|
+
if (!actions.parentElement) bubble.appendChild(actions);
|
|
3544
|
+
}
|
|
2941
3545
|
if (comment.progressEvent) {
|
|
2942
3546
|
const progress = document.createElement('div');
|
|
2943
3547
|
progress.className = 'comment-progress';
|
|
@@ -2958,20 +3562,104 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2958
3562
|
details.appendChild(pre);
|
|
2959
3563
|
bubble.appendChild(details);
|
|
2960
3564
|
}
|
|
2961
|
-
const codexLog = renderCodexLog(comment.eventLog);
|
|
2962
|
-
if (codexLog) bubble.appendChild(codexLog);
|
|
2963
3565
|
els.commentThread.appendChild(bubble);
|
|
2964
3566
|
});
|
|
3567
|
+
if (scrollToBottom) scrollCommentThreadToBottom();
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
function commentActionButton(label, iconHtml, variant) {
|
|
3571
|
+
const button = document.createElement('button');
|
|
3572
|
+
button.type = 'button';
|
|
3573
|
+
button.className = 'comment-action-button' + (variant ? ' ' + variant : '');
|
|
3574
|
+
button.setAttribute('aria-label', label);
|
|
3575
|
+
button.title = label;
|
|
3576
|
+
button.innerHTML = iconHtml;
|
|
3577
|
+
button.addEventListener('click', (event) => event.stopPropagation());
|
|
3578
|
+
return button;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
function renderCommentReferenceChips(elements) {
|
|
3582
|
+
if (!Array.isArray(elements) || !elements.length) return null;
|
|
3583
|
+
const wrap = document.createElement('div');
|
|
3584
|
+
wrap.className = 'selection-chips';
|
|
3585
|
+
elements.slice(0, 4).forEach((payload) => {
|
|
3586
|
+
const label = elementDisplayLabel(payload);
|
|
3587
|
+
const chip = document.createElement('span');
|
|
3588
|
+
chip.className = 'ref-chip';
|
|
3589
|
+
chip.title = label;
|
|
3590
|
+
chip.textContent = displayReferenceLabel(label);
|
|
3591
|
+
wrap.appendChild(chip);
|
|
3592
|
+
});
|
|
3593
|
+
if (elements.length > 4) {
|
|
3594
|
+
const more = document.createElement('span');
|
|
3595
|
+
more.className = 'ref-chip';
|
|
3596
|
+
more.textContent = '+' + (elements.length - 4) + ' more';
|
|
3597
|
+
more.title = more.textContent;
|
|
3598
|
+
wrap.appendChild(more);
|
|
3599
|
+
}
|
|
3600
|
+
return wrap;
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
function selectPersistedComment(commentId) {
|
|
3604
|
+
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
3605
|
+
if (!comment) return;
|
|
3606
|
+
state.activeCommentId = comment.id;
|
|
3607
|
+
state.activeCommentElements = Array.isArray(comment.elements) ? comment.elements : [];
|
|
3608
|
+
if (!state.activeCommentElements.length) setStatus('Comment has no saved deck target.');
|
|
3609
|
+
const slideIndex = comment.slideIndex || slideIndexFromElements(comment.elements);
|
|
3610
|
+
if (Number.isInteger(slideIndex) && slideIndex > 0 && slideIndex !== currentDeckSlideIndex()) {
|
|
3611
|
+
goToDeckSlide(slideIndex - 1);
|
|
3612
|
+
}
|
|
3613
|
+
renderCommentThread(false);
|
|
3614
|
+
requestAnimationFrame(renderActiveCommentHighlights);
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
function scrollCommentThreadToBottom() {
|
|
3618
|
+
requestAnimationFrame(() => {
|
|
3619
|
+
els.commentThread.scrollTop = els.commentThread.scrollHeight;
|
|
3620
|
+
});
|
|
2965
3621
|
}
|
|
2966
3622
|
|
|
2967
3623
|
function commentStatusLabel(status) {
|
|
2968
|
-
if (status === '
|
|
2969
|
-
if (status === '
|
|
2970
|
-
if (status === '
|
|
3624
|
+
if (status === 'open') return 'Saved comment';
|
|
3625
|
+
if (status === 'queued') return 'Queued for apply';
|
|
3626
|
+
if (status === 'applying') return 'Applying with Codex...';
|
|
3627
|
+
if (status === 'applied' || status === 'updated' || status === 'stale') return 'Codex completed';
|
|
3628
|
+
if (status === 'failed') return 'Failed to apply';
|
|
2971
3629
|
if (status === 'sending') return 'Sending to Review agent...';
|
|
2972
3630
|
return 'Sent to Review agent';
|
|
2973
3631
|
}
|
|
2974
3632
|
|
|
3633
|
+
function canApplyPersistedComment(status) {
|
|
3634
|
+
return status === 'open' || status === 'failed' || isReapplyStatus(status);
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
function canStopPersistedComment(status) {
|
|
3638
|
+
return status === 'applying' || status === 'queued';
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
function canDeletePersistedComment(status) {
|
|
3642
|
+
return status !== 'applying';
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
function isReapplyStatus(status) {
|
|
3646
|
+
return status === 'applied' || status === 'updated' || status === 'stale';
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
function slideIndexFromElements(elements) {
|
|
3650
|
+
if (!Array.isArray(elements)) return null;
|
|
3651
|
+
const found = elements.map((item) => item && item.slideIndex).find((value) => Number.isInteger(value) && value > 0);
|
|
3652
|
+
return found || null;
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
function currentDeckSlideIndex() {
|
|
3656
|
+
const doc = els.frame.contentDocument;
|
|
3657
|
+
const slides = doc ? getSlides(doc) : [];
|
|
3658
|
+
const currentSlide = slides[state.deckSlideIndex];
|
|
3659
|
+
const explicitSlideIndex = Number(currentSlide?.getAttribute('data-slide-index'));
|
|
3660
|
+
return Number.isFinite(explicitSlideIndex) && explicitSlideIndex > 0 ? explicitSlideIndex : state.deckSlideIndex + 1;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
2975
3663
|
function targetFromPointer(event) {
|
|
2976
3664
|
const doc = els.frame.contentDocument;
|
|
2977
3665
|
if (!doc || doc.location.href === 'about:blank') return null;
|
|
@@ -3014,6 +3702,28 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3014
3702
|
outline.style.height = rect.height + 'px';
|
|
3015
3703
|
}
|
|
3016
3704
|
|
|
3705
|
+
function createCommentHighlightBox() {
|
|
3706
|
+
const box = document.createElement('div');
|
|
3707
|
+
box.className = 'comment-highlight-box';
|
|
3708
|
+
els.commentHighlightLayer.appendChild(box);
|
|
3709
|
+
return box;
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
function renderParentBox(box, target) {
|
|
3713
|
+
if (!box || !target || !target.getBoundingClientRect) {
|
|
3714
|
+
if (box) box.style.display = 'none';
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
const frameRect = els.frame.getBoundingClientRect();
|
|
3718
|
+
const previewRect = els.commentHighlightLayer.getBoundingClientRect();
|
|
3719
|
+
const rect = target.getBoundingClientRect();
|
|
3720
|
+
box.style.display = 'block';
|
|
3721
|
+
box.style.left = (frameRect.left - previewRect.left + rect.left) + 'px';
|
|
3722
|
+
box.style.top = (frameRect.top - previewRect.top + rect.top) + 'px';
|
|
3723
|
+
box.style.width = rect.width + 'px';
|
|
3724
|
+
box.style.height = rect.height + 'px';
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3017
3727
|
function renderHoverOutline(target) {
|
|
3018
3728
|
renderBox(state.hoverOutline, target);
|
|
3019
3729
|
}
|
|
@@ -3021,10 +3731,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3021
3731
|
function renderReferenceOutlines() {
|
|
3022
3732
|
const doc = els.frame.contentDocument;
|
|
3023
3733
|
if (!doc || doc.location.href === 'about:blank') return;
|
|
3024
|
-
const
|
|
3025
|
-
const currentSlide = slides[state.deckSlideIndex];
|
|
3026
|
-
const explicitSlideIndex = Number(currentSlide?.getAttribute('data-slide-index'));
|
|
3027
|
-
const currentSlideIndex = Number.isFinite(explicitSlideIndex) && explicitSlideIndex > 0 ? explicitSlideIndex : state.deckSlideIndex + 1;
|
|
3734
|
+
const currentSlideIndex = currentDeckSlideIndex();
|
|
3028
3735
|
while (state.referenceOutlines.length < state.references.length) state.referenceOutlines.push(createOutline(doc, '#7aa6d8', 'rgba(122,166,216,.18)'));
|
|
3029
3736
|
state.referenceOutlines.forEach((outline, index) => {
|
|
3030
3737
|
const reference = state.references[index];
|
|
@@ -3037,6 +3744,36 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3037
3744
|
});
|
|
3038
3745
|
}
|
|
3039
3746
|
|
|
3747
|
+
function renderActiveCommentHighlights() {
|
|
3748
|
+
const doc = els.frame.contentDocument;
|
|
3749
|
+
if (!doc || doc.location.href === 'about:blank') return;
|
|
3750
|
+
const elements = Array.isArray(state.activeCommentElements) ? state.activeCommentElements : [];
|
|
3751
|
+
while (state.commentHighlightOutlines.length < elements.length) state.commentHighlightOutlines.push(createCommentHighlightBox());
|
|
3752
|
+
let visible = 0;
|
|
3753
|
+
let missing = 0;
|
|
3754
|
+
const currentSlideIndex = currentDeckSlideIndex();
|
|
3755
|
+
state.commentHighlightOutlines.forEach((outline, index) => {
|
|
3756
|
+
const payload = elements[index];
|
|
3757
|
+
if (!payload || payload.slideIndex !== currentSlideIndex) {
|
|
3758
|
+
renderParentBox(outline, null);
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
const resolved = resolveElementFromPayload(payload);
|
|
3762
|
+
const target = resolved.target;
|
|
3763
|
+
if (!target) {
|
|
3764
|
+
missing += 1;
|
|
3765
|
+
renderParentBox(outline, null);
|
|
3766
|
+
return;
|
|
3767
|
+
}
|
|
3768
|
+
visible += 1;
|
|
3769
|
+
renderParentBox(outline, target);
|
|
3770
|
+
});
|
|
3771
|
+
for (let i = elements.length; i < state.commentHighlightOutlines.length; i++) renderParentBox(state.commentHighlightOutlines[i], null);
|
|
3772
|
+
if (!state.activeCommentId) return;
|
|
3773
|
+
if (visible > 0) setStatus('Highlighted ' + visible + ' comment target' + (visible === 1 ? '' : 's') + '.');
|
|
3774
|
+
else if (missing > 0) setStatus('Comment target is no longer available on this deck version.');
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3040
3777
|
function clearHoverSilently() {
|
|
3041
3778
|
state.hoverEl = null;
|
|
3042
3779
|
if (state.hoverOutline) state.hoverOutline.style.display = 'none';
|
|
@@ -3052,7 +3789,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3052
3789
|
|
|
3053
3790
|
function updateSendState() {
|
|
3054
3791
|
if (state.sendingEdit) setButtonLoading(els.send, true, 'Sending...');
|
|
3055
|
-
else setButtonLoading(els.send, false,
|
|
3792
|
+
else setButtonLoading(els.send, false, sendButtonHtml, true);
|
|
3056
3793
|
els.send.disabled = state.sendingEdit || !getCommentText().trim();
|
|
3057
3794
|
if (state.inspecting) setButtonLoading(els.inspectButton, true, 'Getting insight...');
|
|
3058
3795
|
else setButtonLoading(els.inspectButton, false, 'Get Insight');
|
|
@@ -3077,7 +3814,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3077
3814
|
chip.style.setProperty('--ref-bg', reference.color.bg);
|
|
3078
3815
|
chip.style.setProperty('--ref-border', reference.color.border);
|
|
3079
3816
|
chip.style.setProperty('--ref-text', reference.color.text);
|
|
3080
|
-
|
|
3817
|
+
const label = '@' + reference.label;
|
|
3818
|
+
chip.title = label;
|
|
3819
|
+
chip.textContent = displayReferenceLabel(label);
|
|
3081
3820
|
els.selectionChips.appendChild(chip);
|
|
3082
3821
|
});
|
|
3083
3822
|
}
|
|
@@ -3340,7 +4079,14 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3340
4079
|
function escapeHtml(value) { return String(value || '').replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch])); }
|
|
3341
4080
|
|
|
3342
4081
|
function nextReferenceLabel(payload) {
|
|
3343
|
-
return
|
|
4082
|
+
return elementDisplayLabel(payload);
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
const REF_LABEL_MAX_DISPLAY_CHARS = ${REVIEW_REF_LABEL_MAX_DISPLAY_CHARS};
|
|
4086
|
+
|
|
4087
|
+
function displayReferenceLabel(label) {
|
|
4088
|
+
const text = String(label || '');
|
|
4089
|
+
return text.length > REF_LABEL_MAX_DISPLAY_CHARS ? text.slice(0, REF_LABEL_MAX_DISPLAY_CHARS - 1) + '…' : text;
|
|
3344
4090
|
}
|
|
3345
4091
|
|
|
3346
4092
|
function insertReferenceChip(reference) {
|
|
@@ -3352,7 +4098,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3352
4098
|
chip.style.setProperty('--ref-bg', reference.color.bg);
|
|
3353
4099
|
chip.style.setProperty('--ref-border', reference.color.border);
|
|
3354
4100
|
chip.style.setProperty('--ref-text', reference.color.text);
|
|
3355
|
-
|
|
4101
|
+
const label = '@' + reference.label;
|
|
4102
|
+
chip.title = label;
|
|
4103
|
+
chip.textContent = displayReferenceLabel(label);
|
|
3356
4104
|
const trailingSpace = document.createTextNode(' ');
|
|
3357
4105
|
const range = getCommentInsertRange();
|
|
3358
4106
|
if (range) {
|
|
@@ -3378,7 +4126,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3378
4126
|
chip.style.setProperty('--ref-bg', '#dcfce7');
|
|
3379
4127
|
chip.style.setProperty('--ref-border', '#86efac');
|
|
3380
4128
|
chip.style.setProperty('--ref-text', '#166534');
|
|
3381
|
-
|
|
4129
|
+
const label = '@Asset ' + (asset.id || asset.deckPath || asset.path || 'image');
|
|
4130
|
+
chip.title = label;
|
|
4131
|
+
chip.textContent = displayReferenceLabel(label);
|
|
3382
4132
|
const range = getCommentInsertRange();
|
|
3383
4133
|
if (range) {
|
|
3384
4134
|
range.insertNode(chip);
|
|
@@ -3435,7 +4185,11 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3435
4185
|
|
|
3436
4186
|
function getCommentText(editor) {
|
|
3437
4187
|
const source = editor || els.comment;
|
|
3438
|
-
|
|
4188
|
+
const clone = source.cloneNode(true);
|
|
4189
|
+
clone.querySelectorAll('.ref-chip[title]').forEach((chip) => {
|
|
4190
|
+
chip.textContent = chip.getAttribute('title') || chip.textContent || '';
|
|
4191
|
+
});
|
|
4192
|
+
return (clone.innerText || clone.textContent || '').replace(/\\u00a0/g, ' ');
|
|
3439
4193
|
}
|
|
3440
4194
|
|
|
3441
4195
|
function placeCaretAfter(node) {
|
|
@@ -3480,14 +4234,23 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3480
4234
|
function humanElementName(payload) {
|
|
3481
4235
|
const tag = payload.tagName || 'element';
|
|
3482
4236
|
const classes = payload.classList || [];
|
|
3483
|
-
if (/^h[1-6]$/.test(tag)) return 'Heading';
|
|
3484
|
-
if (tag === 'p') return 'Text
|
|
4237
|
+
if (payload.semanticKind === 'heading' || /^h[1-6]$/.test(tag)) return 'Heading';
|
|
4238
|
+
if (payload.semanticKind === 'text' || tag === 'p') return 'Text';
|
|
4239
|
+
if (payload.semanticKind === 'source-note') return 'Source';
|
|
3485
4240
|
if (classes.some((name) => /card/i.test(name))) return 'Card';
|
|
3486
|
-
if (classes.some((name) => /stat|metric|value/i.test(name))) return 'Metric';
|
|
3487
|
-
if (tag === 'img' || tag === 'svg') return 'Visual';
|
|
4241
|
+
if (payload.semanticKind === 'metric' || classes.some((name) => /stat|metric|value/i.test(name))) return 'Metric';
|
|
4242
|
+
if (payload.semanticKind === 'visual' || tag === 'img' || tag === 'svg') return 'Visual';
|
|
3488
4243
|
return 'Element';
|
|
3489
4244
|
}
|
|
3490
4245
|
|
|
4246
|
+
function elementDisplayLabel(payload) {
|
|
4247
|
+
if (!payload) return 'Element';
|
|
4248
|
+
if (payload.displayLabel) return payload.displayLabel;
|
|
4249
|
+
const kind = humanElementName(payload);
|
|
4250
|
+
const text = String(payload.text || payload.textNormalized || '').replace(/\\s+/g, ' ').trim().slice(0, 56);
|
|
4251
|
+
return text ? kind + ': ' + text : kind;
|
|
4252
|
+
}
|
|
4253
|
+
|
|
3491
4254
|
function collectPayload(el) {
|
|
3492
4255
|
const doc = els.frame.contentDocument;
|
|
3493
4256
|
const slides = getSlides(doc);
|
|
@@ -3496,22 +4259,116 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3496
4259
|
const explicitSlideIndex = slide ? Number(slide.getAttribute('data-slide-index')) : Number.NaN;
|
|
3497
4260
|
const slideIndex = slide && Number.isFinite(explicitSlideIndex) && explicitSlideIndex > 0 ? explicitSlideIndex : slide ? slides.indexOf(slide) + 1 : undefined;
|
|
3498
4261
|
const win = els.frame.contentWindow;
|
|
4262
|
+
const tagName = el.tagName.toLowerCase();
|
|
4263
|
+
const classList = Array.from(el.classList || []);
|
|
4264
|
+
const text = (el.innerText || el.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 600);
|
|
4265
|
+
const textNormalized = normalizeTargetText(text);
|
|
4266
|
+
const semanticKind = semanticKindForElement(el);
|
|
4267
|
+
const slideTitle = slide ? ((slide.querySelector('h1,h2,h3,[data-title]') || {}).textContent || '').trim().slice(0, 160) : undefined;
|
|
4268
|
+
const nearbyText = slide ? (slide.innerText || slide.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 1200) : undefined;
|
|
4269
|
+
const identity = { tagName, semanticKind, classList, textNormalized, slideTitle, nearbyText };
|
|
4270
|
+
const displayLabel = elementDisplayLabel({ ...identity, text });
|
|
3499
4271
|
return {
|
|
3500
4272
|
slideIndex,
|
|
3501
|
-
slideTitle
|
|
4273
|
+
slideTitle,
|
|
3502
4274
|
selector: buildSelector(el, slide),
|
|
3503
4275
|
domPath: buildDomPath(el, slide),
|
|
3504
|
-
tagName
|
|
4276
|
+
tagName,
|
|
3505
4277
|
id: el.id || undefined,
|
|
3506
|
-
classList
|
|
3507
|
-
|
|
4278
|
+
classList,
|
|
4279
|
+
semanticKind,
|
|
4280
|
+
displayLabel,
|
|
4281
|
+
text,
|
|
4282
|
+
textNormalized,
|
|
3508
4283
|
outerHTMLExcerpt: (el.outerHTML || '').replace(/\\s+/g, ' ').slice(0, 1200),
|
|
3509
|
-
nearbyText
|
|
4284
|
+
nearbyText,
|
|
3510
4285
|
boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
4286
|
+
slideRelativeBox: slideRelativeBox(el, slide),
|
|
4287
|
+
fingerprint: fingerprintForTarget(identity),
|
|
3511
4288
|
viewport: { width: win ? win.innerWidth : undefined, height: win ? win.innerHeight : undefined },
|
|
3512
4289
|
};
|
|
3513
4290
|
}
|
|
3514
4291
|
|
|
4292
|
+
function semanticKindForElement(el) {
|
|
4293
|
+
const tag = el?.tagName ? el.tagName.toLowerCase() : '';
|
|
4294
|
+
const classes = Array.from(el?.classList || []);
|
|
4295
|
+
if (/^h[1-6]$/.test(tag) || el?.matches?.('[data-title]')) return 'heading';
|
|
4296
|
+
if (tag === 'img' || tag === 'svg' || tag === 'canvas' || tag === 'video' || classes.some((name) => /image|media|visual|chart|figure|logo/i.test(name))) return 'visual';
|
|
4297
|
+
if (classes.some((name) => /source|citation|footnote|note/i.test(name))) return 'source-note';
|
|
4298
|
+
if (classes.some((name) => /stat|metric|kpi|value|number/i.test(name))) return 'metric';
|
|
4299
|
+
if (classes.some((name) => /card|box|panel|tile/i.test(name))) return 'card';
|
|
4300
|
+
if (tag === 'p' || tag === 'li' || tag === 'blockquote' || tag === 'span') return 'text';
|
|
4301
|
+
return 'element';
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
function normalizeTargetText(value) {
|
|
4305
|
+
return String(value || '').replace(/\\u00a0/g, ' ').replace(/\\s+/g, ' ').trim().toLowerCase().slice(0, 600);
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
function fingerprintForTarget(input) {
|
|
4309
|
+
const tagName = String(input.tagName || '').toLowerCase();
|
|
4310
|
+
const textNormalized = normalizeTargetText(input.textNormalized);
|
|
4311
|
+
const classList = Array.isArray(input.classList) ? input.classList.map(String).sort() : [];
|
|
4312
|
+
const semanticKind = input.semanticKind || 'element';
|
|
4313
|
+
const context = normalizeTargetText(String(input.slideTitle || '') + ' ' + String(input.nearbyText || '').slice(0, 400));
|
|
4314
|
+
return {
|
|
4315
|
+
textHash: textNormalized ? stableHash(textNormalized) : undefined,
|
|
4316
|
+
contentHash: stableHash(tagName + '|' + textNormalized),
|
|
4317
|
+
structureHash: stableHash(tagName + '|' + semanticKind + '|' + classList.join('.')),
|
|
4318
|
+
contextHash: context ? stableHash(context) : undefined,
|
|
4319
|
+
};
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
function stableHash(value) {
|
|
4323
|
+
let hash = 2166136261;
|
|
4324
|
+
const text = String(value || '');
|
|
4325
|
+
for (let i = 0; i < text.length; i++) {
|
|
4326
|
+
hash ^= text.charCodeAt(i);
|
|
4327
|
+
hash = Math.imul(hash, 16777619);
|
|
4328
|
+
}
|
|
4329
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
function slideRelativeBox(el, slide) {
|
|
4333
|
+
if (!el || !slide || !el.getBoundingClientRect || !slide.getBoundingClientRect) return undefined;
|
|
4334
|
+
const rect = el.getBoundingClientRect();
|
|
4335
|
+
const slideRect = slide.getBoundingClientRect();
|
|
4336
|
+
if (!slideRect.width || !slideRect.height) return undefined;
|
|
4337
|
+
return {
|
|
4338
|
+
x: roundRatio((rect.left - slideRect.left) / slideRect.width),
|
|
4339
|
+
y: roundRatio((rect.top - slideRect.top) / slideRect.height),
|
|
4340
|
+
width: roundRatio(rect.width / slideRect.width),
|
|
4341
|
+
height: roundRatio(rect.height / slideRect.height),
|
|
4342
|
+
};
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
function roundRatio(value) {
|
|
4346
|
+
return Math.round(Math.max(-2, Math.min(2, Number(value) || 0)) * 10000) / 10000;
|
|
4347
|
+
}
|
|
4348
|
+
|
|
4349
|
+
function overlapCount(left, right) {
|
|
4350
|
+
const rightSet = new Set(Array.isArray(right) ? right.map(String) : []);
|
|
4351
|
+
return (Array.isArray(left) ? left : []).map(String).filter((item) => rightSet.has(item)).length;
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
function partialTextOverlap(left, right) {
|
|
4355
|
+
if (!left || !right) return false;
|
|
4356
|
+
if (left.includes(right) || right.includes(left)) return true;
|
|
4357
|
+
const leftWords = new Set(String(left).split(' ').filter((word) => word.length > 3));
|
|
4358
|
+
const rightWords = String(right).split(' ').filter((word) => word.length > 3);
|
|
4359
|
+
return rightWords.some((word) => leftWords.has(word));
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
function relativeBoxDistance(left, right) {
|
|
4363
|
+
if (!left || !right) return Number.NaN;
|
|
4364
|
+
const dx = Number(left.x) - Number(right.x);
|
|
4365
|
+
const dy = Number(left.y) - Number(right.y);
|
|
4366
|
+
const dw = Number(left.width) - Number(right.width);
|
|
4367
|
+
const dh = Number(left.height) - Number(right.height);
|
|
4368
|
+
if (![dx, dy, dw, dh].every(Number.isFinite)) return Number.NaN;
|
|
4369
|
+
return Math.sqrt(dx * dx + dy * dy + dw * dw * .35 + dh * dh * .35);
|
|
4370
|
+
}
|
|
4371
|
+
|
|
3515
4372
|
function buildSelector(el, slide) {
|
|
3516
4373
|
if (el.id) return '#' + cssEscape(el.id);
|
|
3517
4374
|
const parts = [];
|