@cyber-dash-tech/revela 0.15.0 → 0.15.2
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 +6 -7
- package/README.zh-CN.md +6 -7
- package/designs/starter/DESIGN.md +168 -171
- package/designs/starter/preview.html +2 -2
- package/designs/summit/DESIGN.md +283 -129
- package/lib/commands/edit.ts +2 -21
- package/lib/commands/help.ts +1 -2
- package/lib/commands/narrative.ts +26 -0
- package/lib/commands/review.ts +49 -12
- package/lib/decks-state.ts +122 -3
- package/lib/design/designs.ts +1 -2
- package/lib/edit/prompt.ts +18 -5
- package/lib/edit/resolve-deck.ts +1 -1
- package/lib/inspect/prompt.ts +6 -1
- package/lib/media/download.ts +36 -11
- package/lib/media/save.ts +24 -0
- package/lib/media/search.ts +385 -0
- package/lib/media/types.ts +12 -0
- package/lib/narrative-state/render-plan.ts +10 -1
- package/lib/qa/artifact.ts +77 -0
- package/lib/qa/checks.ts +101 -10
- package/lib/qa/index.ts +81 -8
- package/lib/qa/measure.ts +85 -0
- package/lib/refine/open.ts +21 -1
- package/lib/refine/server.ts +884 -71
- package/lib/workspace-state/types.ts +1 -0
- package/package.json +1 -1
- package/plugin.ts +36 -130
- package/skill/NARRATIVE_SKILL.md +1 -1
- package/skill/SKILL.md +5 -10
- package/tools/decks.ts +29 -3
- package/tools/media-save.ts +6 -0
- package/tools/narrative-view.ts +1 -1
- package/tools/qa.ts +17 -11
package/lib/refine/server.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { randomBytes } from "crypto"
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "fs"
|
|
3
3
|
import { fileURLToPath } from "url"
|
|
4
|
-
import { dirname, extname, isAbsolute, resolve, sep } from "path"
|
|
4
|
+
import { dirname, extname, isAbsolute, relative, resolve, sep } from "path"
|
|
5
5
|
import type { EditableDeck } from "../edit/resolve-deck"
|
|
6
6
|
import { buildEditPrompt, type EditCommentPayload } from "../edit/prompt"
|
|
7
7
|
import type { InspectionElementSnapshot } from "../inspection-context/match"
|
|
8
8
|
import { buildInspectionPrompt } from "../inspect/prompt"
|
|
9
9
|
import { projectWorkspaceElement } from "../inspect/request"
|
|
10
10
|
import { createInspectRequest, failInspectRequest, getInspectRequest } from "../inspect/requests"
|
|
11
|
+
import { saveMediaAsset } from "../media/save"
|
|
12
|
+
import { searchRemoteImages, type ImageCandidate } from "../media/search"
|
|
13
|
+
import type { MediaAssetRecord, MediaPurpose } from "../media/types"
|
|
11
14
|
|
|
12
15
|
const TOKEN_BYTES = 24
|
|
13
16
|
const SESSION_TTL_MS = 2 * 60 * 60 * 1000
|
|
@@ -190,9 +193,148 @@ async function handleRequest(req: Request): Promise<Response> {
|
|
|
190
193
|
return handleDeckVersion(session.value)
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
if (url.pathname === "/api/assets/search" && req.method === "GET") {
|
|
197
|
+
const session = validateSession(url.searchParams.get("token"))
|
|
198
|
+
if (!session.ok) return session.response
|
|
199
|
+
return handleAssetSearch(url, session.value)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (url.pathname === "/api/assets/save" && req.method === "POST") {
|
|
203
|
+
const session = validateSession(url.searchParams.get("token"))
|
|
204
|
+
if (!session.ok) return session.response
|
|
205
|
+
return handleAssetSave(req, session.value)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (url.pathname === "/api/assets/list" && req.method === "GET") {
|
|
209
|
+
const session = validateSession(url.searchParams.get("token"))
|
|
210
|
+
if (!session.ok) return session.response
|
|
211
|
+
return handleAssetList(session.value)
|
|
212
|
+
}
|
|
213
|
+
|
|
193
214
|
return textResponse("Not found", 404)
|
|
194
215
|
}
|
|
195
216
|
|
|
217
|
+
async function handleAssetSearch(url: URL, session: EditSession): Promise<Response> {
|
|
218
|
+
const query = (url.searchParams.get("query") || "").trim()
|
|
219
|
+
if (!query) return jsonResponse({ ok: false, error: "query is required" }, 400)
|
|
220
|
+
const purpose = normalizeMediaPurpose(url.searchParams.get("purpose"))
|
|
221
|
+
const limit = Number(url.searchParams.get("limit") || 12)
|
|
222
|
+
const page = Number(url.searchParams.get("page") || 1)
|
|
223
|
+
try {
|
|
224
|
+
const candidates = await searchRemoteImages({ query, purpose, limit, page })
|
|
225
|
+
session.lastActiveAt = Date.now()
|
|
226
|
+
scheduleIdleStop()
|
|
227
|
+
return jsonResponse({ ok: true, candidates })
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
230
|
+
return jsonResponse({ ok: false, error: message }, 502)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function handleAssetSave(req: Request, session: EditSession): Promise<Response> {
|
|
235
|
+
let body: any
|
|
236
|
+
try {
|
|
237
|
+
body = await req.json()
|
|
238
|
+
} catch {
|
|
239
|
+
return jsonResponse({ ok: false, error: "Invalid JSON body" }, 400)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const candidate = normalizeImageCandidate(body?.candidate ?? body)
|
|
243
|
+
if (!candidate) return jsonResponse({ ok: false, error: "Valid image candidate is required" }, 400)
|
|
244
|
+
const purpose = normalizeMediaPurpose(body?.purpose) || candidate.purpose || "illustration"
|
|
245
|
+
const result = await saveMediaAsset({
|
|
246
|
+
topic: session.deck,
|
|
247
|
+
id: body?.id || candidate.candidateId,
|
|
248
|
+
type: "image",
|
|
249
|
+
purpose,
|
|
250
|
+
brief: body?.brief || `Saved from ${candidate.provider} for Refine asset placement.`,
|
|
251
|
+
status: "success",
|
|
252
|
+
sourceUrl: candidate.imageUrl,
|
|
253
|
+
alt: body?.alt || candidate.alt || candidate.title,
|
|
254
|
+
notes: body?.notes,
|
|
255
|
+
provider: candidate.provider,
|
|
256
|
+
sourcePageUrl: candidate.sourcePageUrl,
|
|
257
|
+
license: candidate.license,
|
|
258
|
+
attribution: candidate.attribution,
|
|
259
|
+
width: candidate.width,
|
|
260
|
+
height: candidate.height,
|
|
261
|
+
}, session.workspaceRoot)
|
|
262
|
+
|
|
263
|
+
session.lastActiveAt = Date.now()
|
|
264
|
+
scheduleIdleStop()
|
|
265
|
+
if (!result.ok) return jsonResponse({ ok: false, error: result.error }, 400)
|
|
266
|
+
if (result.status !== "success" || !result.path) {
|
|
267
|
+
return jsonResponse({ ok: false, error: `Failed to save asset: ${result.status}` }, 400)
|
|
268
|
+
}
|
|
269
|
+
const asset = savedAssetForResult(session, result.assetId)
|
|
270
|
+
if (!asset) return jsonResponse({ ok: false, error: "Saved asset was not found in workspace assets." }, 500)
|
|
271
|
+
return jsonResponse({ ok: true, asset, result })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function handleAssetList(session: EditSession): Response {
|
|
275
|
+
session.lastActiveAt = Date.now()
|
|
276
|
+
scheduleIdleStop()
|
|
277
|
+
return jsonResponse({ ok: true, assets: listSavedAssets(session) })
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function savedAssetForResult(session: EditSession, assetId: string): (MediaAssetRecord & { previewUrl?: string; deckPath?: string }) | null {
|
|
281
|
+
return listSavedAssets(session).find((asset) => asset.id === assetId) ?? null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function listSavedAssets(session: EditSession): Array<MediaAssetRecord & { previewUrl?: string; deckPath?: string }> {
|
|
285
|
+
const manifestPath = resolve(session.workspaceRoot, "assets", slugify(session.deck), "media-manifest.json")
|
|
286
|
+
if (!existsSync(manifestPath)) return []
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf-8")) as { assets?: MediaAssetRecord[] }
|
|
289
|
+
return (Array.isArray(parsed.assets) ? parsed.assets : [])
|
|
290
|
+
.filter((asset) => asset.status === "success" && asset.path)
|
|
291
|
+
.map((asset) => ({
|
|
292
|
+
...asset,
|
|
293
|
+
previewUrl: asset.path ? assetUrlForRef(asset.path, session, session.workspaceRoot) ?? undefined : undefined,
|
|
294
|
+
deckPath: asset.path ? relative(dirname(session.absoluteFile), resolve(session.workspaceRoot, asset.path)).replace(/\\/g, "/") : undefined,
|
|
295
|
+
}))
|
|
296
|
+
} catch {
|
|
297
|
+
return []
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function normalizeImageCandidate(input: any): ImageCandidate | null {
|
|
302
|
+
if (!input || typeof input !== "object") return null
|
|
303
|
+
const candidateId = typeof input.candidateId === "string" ? input.candidateId.trim() : ""
|
|
304
|
+
const provider = typeof input.provider === "string" ? input.provider.trim() : ""
|
|
305
|
+
const title = typeof input.title === "string" ? input.title.trim() : ""
|
|
306
|
+
const imageUrl = typeof input.imageUrl === "string" ? input.imageUrl.trim() : ""
|
|
307
|
+
const thumbnailUrl = typeof input.thumbnailUrl === "string" ? input.thumbnailUrl.trim() : imageUrl
|
|
308
|
+
if (!candidateId || !provider || !title || !imageUrl) return null
|
|
309
|
+
return {
|
|
310
|
+
candidateId,
|
|
311
|
+
provider,
|
|
312
|
+
title,
|
|
313
|
+
thumbnailUrl,
|
|
314
|
+
imageUrl,
|
|
315
|
+
sourcePageUrl: typeof input.sourcePageUrl === "string" ? input.sourcePageUrl : undefined,
|
|
316
|
+
width: typeof input.width === "number" ? input.width : undefined,
|
|
317
|
+
height: typeof input.height === "number" ? input.height : undefined,
|
|
318
|
+
alt: typeof input.alt === "string" ? input.alt : undefined,
|
|
319
|
+
license: typeof input.license === "string" ? input.license : undefined,
|
|
320
|
+
attribution: typeof input.attribution === "string" ? input.attribution : undefined,
|
|
321
|
+
purpose: normalizeMediaPurpose(input.purpose),
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function normalizeMediaPurpose(input: unknown): MediaPurpose | undefined {
|
|
326
|
+
return input === "hero" || input === "illustration" || input === "portrait" || input === "logo" || input === "screenshot"
|
|
327
|
+
? input
|
|
328
|
+
: undefined
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function slugify(value: string): string {
|
|
332
|
+
return value
|
|
333
|
+
.toLowerCase()
|
|
334
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
335
|
+
.replace(/^-+|-+$/g, "")
|
|
336
|
+
}
|
|
337
|
+
|
|
196
338
|
function handleDeck(session: EditSession): Response {
|
|
197
339
|
session.assets.clear()
|
|
198
340
|
session.assetKeys.clear()
|
|
@@ -460,6 +602,7 @@ async function handleInspect(req: Request, session: EditSession): Promise<Respon
|
|
|
460
602
|
|
|
461
603
|
const snapshot = normalizeSnapshot(body?.snapshot ?? body)
|
|
462
604
|
const language = normalizeInspectLanguage(body?.language)
|
|
605
|
+
const comment = typeof body?.comment === "string" ? body.comment.trim().slice(0, 2000) : ""
|
|
463
606
|
const requestId = typeof body?.requestId === "string" && body.requestId.trim() ? body.requestId.trim() : randomBytes(10).toString("base64url")
|
|
464
607
|
const version = readDeckVersion(session).version
|
|
465
608
|
const staleReason = typeof body?.deckVersion === "string" && body.deckVersion !== version
|
|
@@ -482,6 +625,7 @@ async function handleInspect(req: Request, session: EditSession): Promise<Respon
|
|
|
482
625
|
requestId,
|
|
483
626
|
file: session.file,
|
|
484
627
|
language,
|
|
628
|
+
comment,
|
|
485
629
|
projection: staleReason
|
|
486
630
|
? { ...projection, stale: { stale: true, reason: staleReason } } as any
|
|
487
631
|
: projection,
|
|
@@ -624,95 +768,167 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
624
768
|
<style>
|
|
625
769
|
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
626
770
|
* { box-sizing: border-box; }
|
|
627
|
-
body { margin: 0; background: #
|
|
771
|
+
body { margin: 0; background: #eee8dc; color: #1f2933; height: 100vh; overflow: hidden; }
|
|
628
772
|
body.resizing { cursor: col-resize; user-select: none; }
|
|
629
773
|
body.resizing iframe, body.resizing .hitbox { pointer-events: none; }
|
|
630
774
|
.app { --editor-width: 376px; position: relative; display: grid; grid-template-columns: minmax(0, 1fr) var(--editor-width); height: 100vh; }
|
|
631
|
-
.preview { position: relative; min-width: 0; background: #
|
|
775
|
+
.preview { position: relative; min-width: 0; background: #e7dfd1; }
|
|
632
776
|
.resize-handle { position: absolute; top: 0; bottom: 0; right: calc(var(--editor-width) - 7px); width: 14px; z-index: 5; cursor: col-resize; background: transparent; }
|
|
633
777
|
.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; }
|
|
634
778
|
.resize-handle:hover::before, body.resizing .resize-handle::before { height: 52px; background: #94a3b8; box-shadow: 0 0 0 4px rgba(148,163,184,.16); }
|
|
635
779
|
iframe { display: block; width: 100%; height: 100%; border: 0; background: #fff; }
|
|
636
780
|
.hitbox { position: absolute; inset: 0; z-index: 2; cursor: crosshair; background: transparent; }
|
|
637
|
-
|
|
781
|
+
.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; }
|
|
782
|
+
.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; }
|
|
783
|
+
.deck-nav button:hover:not(:disabled) { background: rgba(255,255,255,.22); }
|
|
784
|
+
.deck-nav button:disabled { opacity: .38; }
|
|
785
|
+
.deck-nav-status { min-width: 76px; color: #e2e8f0; font-size: 12px; font-weight: 900; text-align: center; font-variant-numeric: tabular-nums; }
|
|
786
|
+
aside { position: relative; display: flex; flex-direction: column; gap: 16px; padding: 20px; background: linear-gradient(180deg, #fbfaf7 0%, #f2eee6 100%); overflow: auto; border-left: 1px solid #d8d2c6; }
|
|
638
787
|
h1 { margin: 0; font-size: 18px; line-height: 1.2; letter-spacing: -.01em; color: #0f172a; }
|
|
639
788
|
.wordmark { font-family: Garamond, "Iowan Old Style", Georgia, serif; font-size: 21px; letter-spacing: .08em; font-weight: 600; }
|
|
640
|
-
.hint { margin: 0; color: #
|
|
789
|
+
.hint { margin: 0; color: #756f66; font-size: 13px; line-height: 1.5; }
|
|
641
790
|
.panel { display: flex; flex-direction: column; gap: 10px; }
|
|
642
|
-
.tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 4px; border: 1px solid #
|
|
643
|
-
.tab { padding: 9px 10px; border: 0; border-radius: 10px; background: transparent; color: #
|
|
644
|
-
.tab.active { background: #
|
|
791
|
+
.tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 4px; border: 1px solid #d8d2c6; border-radius: 14px; background: #ebe4d8; }
|
|
792
|
+
.tab { padding: 9px 10px; border: 0; border-radius: 10px; background: transparent; color: #5f594f; box-shadow: none; font-weight: 900; }
|
|
793
|
+
.tab.active { background: #fbfaf7; color: #111827; box-shadow: 0 6px 16px rgba(31,41,51,.1); }
|
|
645
794
|
.tab-panel { display: none; flex-direction: column; gap: 12px; }
|
|
646
795
|
.tab-panel.active { display: flex; }
|
|
647
|
-
.
|
|
648
|
-
.selection-summary
|
|
796
|
+
.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; }
|
|
797
|
+
.selection-summary { padding: 10px 12px; border: 1px solid #d8d2c6; border-radius: 14px; background: #fbfaf7; color: #3f3a33; font-size: 13px; line-height: 1.45; box-shadow: 0 8px 22px rgba(31,41,51,.05); }
|
|
798
|
+
.selection-summary strong { display: block; margin-bottom: 7px; color: #756f66; font-size: 11px; letter-spacing: .09em; text-transform: uppercase; }
|
|
649
799
|
.selection-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
650
|
-
.label { color: #
|
|
651
|
-
.comment-editor { width: 100%; min-height: 164px; max-height: 42vh; overflow: auto; padding: 13px 14px; border: 1px solid #
|
|
652
|
-
.comment-editor:focus { border-color: #
|
|
653
|
-
.comment-editor:empty::before { content: attr(data-placeholder); color: #
|
|
800
|
+
.label { color: #756f66; font-size: 11px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; }
|
|
801
|
+
.comment-editor { width: 100%; min-height: 164px; max-height: 42vh; overflow: auto; padding: 13px 14px; border: 1px solid #d8d2c6; border-radius: 14px; background: #fffdf8; color: #111827; font: inherit; line-height: 1.5; outline: none; white-space: pre-wrap; box-shadow: 0 10px 24px rgba(31,41,51,.06); }
|
|
802
|
+
.comment-editor:focus { border-color: #a9793f; box-shadow: 0 0 0 3px rgba(169,121,63,.14), 0 10px 24px rgba(31,41,51,.07); }
|
|
803
|
+
.comment-editor:empty::before { content: attr(data-placeholder); color: #a79d8e; pointer-events: none; }
|
|
654
804
|
.ref-chip { display: inline-flex; align-items: center; margin: 0 2px; padding: 1px 7px; border-radius: 999px; background: var(--ref-bg, #e0f2fe); color: var(--ref-text, #075985); border: 1px solid var(--ref-border, #7dd3fc); font-weight: 800; white-space: nowrap; }
|
|
655
|
-
.
|
|
656
|
-
.comment-
|
|
657
|
-
.comment-
|
|
658
|
-
.comment-bubble
|
|
659
|
-
.comment-bubble.
|
|
660
|
-
.comment-bubble.
|
|
805
|
+
.activity-panel { display: flex; flex-direction: column; gap: 8px; padding-top: 2px; }
|
|
806
|
+
.comment-thread { display: flex; flex-direction: column; gap: 8px; max-height: 24vh; overflow: auto; }
|
|
807
|
+
.comment-thread:empty { display: none; }
|
|
808
|
+
.comment-bubble { border: 1px solid #d8d2c6; border-radius: 14px; padding: 10px 12px; background: #fffdf8; color: #3f3a33; font-size: 13px; line-height: 1.45; box-shadow: 0 8px 22px rgba(31,41,51,.05); }
|
|
809
|
+
.comment-bubble.sending { border-color: #c8b88f; background: #f7f0df; }
|
|
810
|
+
.comment-bubble.updated { border-color: #9dac8a; background: #f0f2e8; }
|
|
811
|
+
.comment-bubble.stale { border-color: #c6a96a; background: #f8efd7; }
|
|
812
|
+
.comment-bubble.failed { border-color: #c58f82; background: #f7eae5; }
|
|
661
813
|
.comment-bubble-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
662
|
-
.comment-bubble-state { margin-top: 8px; color: #
|
|
663
|
-
.comment-bubble.updated .comment-bubble-state { color: #
|
|
664
|
-
.comment-bubble.stale .comment-bubble-state { color: #
|
|
665
|
-
.comment-bubble.failed .comment-bubble-state { color: #
|
|
814
|
+
.comment-bubble-state { margin-top: 8px; color: #8a6231; font-size: 12px; font-weight: 800; }
|
|
815
|
+
.comment-bubble.updated .comment-bubble-state { color: #556b3f; }
|
|
816
|
+
.comment-bubble.stale .comment-bubble-state { color: #8a6231; }
|
|
817
|
+
.comment-bubble.failed .comment-bubble-state { color: #8f4638; }
|
|
666
818
|
.inspect-actions { display: flex; flex-direction: column; gap: 8px; }
|
|
667
819
|
.inspect-options { display: flex; flex-direction: column; gap: 5px; }
|
|
668
|
-
.inspect-options label { color: #
|
|
669
|
-
.inspect-select { width: 100%; padding: 10px 11px; border: 1px solid #
|
|
820
|
+
.inspect-options label { color: #756f66; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .05em; }
|
|
821
|
+
.inspect-select { width: 100%; padding: 10px 11px; border: 1px solid #d8d2c6; border-radius: 12px; background: #fffdf8; color: #111827; font-weight: 700; }
|
|
670
822
|
.inspect-cards { display: flex; flex-direction: column; gap: 12px; }
|
|
671
|
-
.inspect-card { border: 1px solid #
|
|
823
|
+
.inspect-card { border: 1px solid #d8d2c6; border-radius: 16px; background: #fffdf8; padding: 13px; box-shadow: 0 10px 22px rgba(31,41,51,.05); }
|
|
672
824
|
.inspect-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
673
825
|
.inspect-card h2 { margin: 0; font-size: 13px; color: #0f172a; }
|
|
674
|
-
.badge { border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; background: #
|
|
675
|
-
.badge.supported, .badge.found, .badge.present, .badge.known, .badge.suggested { background: #
|
|
676
|
-
.badge.weak, .badge.missing { background: #
|
|
677
|
-
.badge.unsupported { background: #
|
|
678
|
-
.inspect-card p, .inspect-empty, .inspect-loading { margin: 0; color: #
|
|
679
|
-
.inspect-item { margin-top: 7px; padding: 8px; border-radius: 10px; background: #
|
|
826
|
+
.badge { border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; background: #e8e1d4; color: #5f594f; }
|
|
827
|
+
.badge.supported, .badge.found, .badge.present, .badge.known, .badge.suggested { background: #e7ecdc; color: #4d6138; }
|
|
828
|
+
.badge.weak, .badge.missing { background: #f1e5c8; color: #765326; }
|
|
829
|
+
.badge.unsupported { background: #f0d9d1; color: #7f3d31; }
|
|
830
|
+
.inspect-card p, .inspect-empty, .inspect-loading { margin: 0; color: #5f594f; font-size: 12px; line-height: 1.5; }
|
|
831
|
+
.inspect-item { margin-top: 7px; padding: 8px; border-radius: 10px; background: #f7f3ea; color: #3f3a33; font-size: 12px; line-height: 1.45; }
|
|
680
832
|
.inspect-warning, .inspect-stale { margin-top: 8px; padding: 8px; border-radius: 10px; background: #fff7ed; color: #9a3412; font-size: 12px; line-height: 1.45; }
|
|
681
|
-
|
|
833
|
+
.loading-row { display: inline-flex; align-items: center; gap: 8px; }
|
|
834
|
+
.spinner { width: 16px; height: 16px; border: 2px solid rgba(169,121,63,.22); border-top-color: currentColor; border-radius: 999px; animation: spin .8s linear infinite; }
|
|
835
|
+
button .spinner { width: 15px; height: 15px; border-color: rgba(255,255,255,.36); border-top-color: #fff; }
|
|
836
|
+
.skeleton-card { border: 1px solid #d8d2c6; border-radius: 16px; background: #fffdf8; padding: 13px; box-shadow: 0 10px 22px rgba(31,41,51,.05); }
|
|
837
|
+
.skeleton-line { height: 10px; margin: 8px 0; border-radius: 999px; background: linear-gradient(90deg, #ded5c6 0%, #fbfaf7 48%, #ded5c6 100%); background-size: 200% 100%; animation: shimmer 1.2s ease-in-out infinite; }
|
|
838
|
+
.skeleton-line.short { width: 42%; }
|
|
839
|
+
.skeleton-line.medium { width: 68%; }
|
|
840
|
+
.skeleton-line.long { width: 92%; }
|
|
841
|
+
.asset-card.is-saving::after { content: ""; position: absolute; inset: 0; background: rgba(15,23,42,.32); }
|
|
842
|
+
.asset-card.is-saving .asset-save { z-index: 1; }
|
|
843
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
844
|
+
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
|
845
|
+
.asset-search { display: grid; grid-template-columns: minmax(0, 1fr) 118px; gap: 8px; }
|
|
846
|
+
.asset-search input, .asset-search select { min-width: 0; padding: 10px 11px; border: 1px solid #d8d2c6; border-radius: 12px; background: #fffdf8; color: #111827; font: inherit; font-size: 12px; font-weight: 700; outline: none; }
|
|
847
|
+
.asset-search input:focus, .asset-search select:focus { border-color: #a9793f; box-shadow: 0 0 0 3px rgba(169,121,63,.14); }
|
|
848
|
+
.asset-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }
|
|
849
|
+
.asset-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
|
|
850
|
+
.asset-card { position: relative; min-width: 0; aspect-ratio: 1 / 1; overflow: hidden; border: 1px solid #d8d2c6; border-radius: 14px; background: #fffdf8; box-shadow: 0 8px 18px rgba(31,41,51,.05); }
|
|
851
|
+
.asset-card.saved { width: 64px; height: 64px; aspect-ratio: auto; border-radius: 12px; }
|
|
852
|
+
.asset-card[draggable="true"] { cursor: grab; }
|
|
853
|
+
.asset-card[draggable="true"]:active { cursor: grabbing; }
|
|
854
|
+
.asset-thumb { width: 100%; height: 100%; display: block; background: #eee8dc; object-fit: contain; }
|
|
855
|
+
.asset-tools { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
856
|
+
.asset-search-toggle { width: auto; min-width: 32px; height: 28px; padding: 0 9px; border-radius: 999px; box-shadow: 0 6px 14px rgba(31,41,51,.08); font-size: 16px; line-height: 1; }
|
|
857
|
+
.asset-search-view { position: absolute; inset: 0; z-index: 12; display: flex; flex-direction: column; gap: 14px; padding: 20px; background: linear-gradient(180deg, #fbfaf7 0%, #f2eee6 100%); overflow: auto; transform: translateX(105%); transition: transform .2s ease; box-shadow: -18px 0 44px rgba(31,41,51,.16); }
|
|
858
|
+
.asset-search-view.open { transform: translateX(0); }
|
|
859
|
+
.asset-search-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
860
|
+
.asset-search-title { display: flex; flex-direction: column; gap: 2px; }
|
|
861
|
+
.asset-search-title h2 { margin: 0; color: #0f172a; font-size: 16px; letter-spacing: -.01em; }
|
|
862
|
+
.asset-search-title span { color: #756f66; font-size: 12px; line-height: 1.35; }
|
|
863
|
+
.asset-back { width: auto; padding: 9px 11px; border-radius: 999px; background: #ebe4d8; color: #111827; box-shadow: none; }
|
|
864
|
+
.asset-save { position: absolute; left: 7px; right: 7px; bottom: 7px; width: auto; padding: 7px 8px; border-radius: 10px; font-size: 11px; background: rgba(17,24,39,.9); color: #fbfaf7; box-shadow: 0 8px 16px rgba(31,41,51,.2); opacity: .96; }
|
|
865
|
+
.asset-empty { grid-column: 1 / -1; margin: 0; color: #756f66; font-size: 12px; line-height: 1.45; }
|
|
866
|
+
.edit-assets { padding: 10px; border: 1px solid #d8d2c6; border-radius: 16px; background: #f7f3ea; }
|
|
867
|
+
.edit-assets .panel { gap: 8px; }
|
|
868
|
+
.edit-assets .asset-grid { grid-template-columns: repeat(auto-fill, 64px); align-items: start; max-height: 176px; overflow: auto; }
|
|
869
|
+
.edit-assets .asset-thumb { width: 64px; height: 64px; }
|
|
870
|
+
.drop-active .hitbox { background: rgba(169,121,63,.1); outline: 2px dashed rgba(169,121,63,.48); outline-offset: -10px; }
|
|
871
|
+
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); }
|
|
872
|
+
button:hover:not(:disabled) { background: #e3dacb; }
|
|
682
873
|
button:disabled { cursor: not-allowed; opacity: .5; }
|
|
683
|
-
.
|
|
684
|
-
|
|
874
|
+
.primary-action { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 46px; border-radius: 14px; border-color: #111827; background: linear-gradient(135deg, #111827 0%, #1f2937 100%); color: #fbfaf7; font-size: 14px; letter-spacing: .01em; box-shadow: 0 12px 24px rgba(31,41,51,.24); transition: transform .14s ease, box-shadow .14s ease, filter .14s ease; }
|
|
875
|
+
.primary-action:hover:not(:disabled) { background: linear-gradient(135deg, #0f1720 0%, #283241 100%); transform: translateY(-1px); box-shadow: 0 16px 30px rgba(31,41,51,.28); filter: saturate(1.02); }
|
|
876
|
+
.primary-action:active:not(:disabled) { transform: translateY(0); box-shadow: 0 9px 20px rgba(31,41,51,.22); }
|
|
877
|
+
.send-icon { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 2.25; stroke-linecap: round; stroke-linejoin: round; }
|
|
878
|
+
.status { min-height: 20px; color: #5f594f; font-size: 13px; line-height: 1.45; }
|
|
879
|
+
@media (max-width: 900px) { .app { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; } .resize-handle { display: none; } aside { max-height: 48vh; } .deck-nav { bottom: 10px; } }
|
|
685
880
|
</style>
|
|
686
881
|
</head>
|
|
687
882
|
<body>
|
|
688
883
|
<main class="app">
|
|
689
|
-
<section class="preview"><iframe id="deck" src="/deck?token=${encodeURIComponent(token)}"></iframe><div id="hitbox" class="hitbox" aria-label="Deck element selection layer"></div></section>
|
|
884
|
+
<section class="preview"><iframe id="deck" src="/deck?token=${encodeURIComponent(token)}"></iframe><div id="hitbox" class="hitbox" aria-label="Deck element selection layer"></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>
|
|
690
885
|
<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>
|
|
691
886
|
<aside>
|
|
692
887
|
<div>
|
|
693
888
|
<h1><span class="wordmark">REVELA</span> Refine</h1>
|
|
694
|
-
<p class="hint">
|
|
889
|
+
<p class="hint">Select refs, describe the change, then send. Use Inspect only when you need source or purpose context.</p>
|
|
695
890
|
</div>
|
|
696
|
-
<div id="selectionSummary" class="selection-summary"><strong>Selection</strong><span>No references selected.</span><div id="selectionChips" class="selection-chips"></div></div>
|
|
891
|
+
<div id="selectionSummary" class="selection-summary sr-only" aria-live="polite"><strong>Selection</strong><span>No references selected.</span><div id="selectionChips" class="selection-chips"></div></div>
|
|
697
892
|
<div class="tabs" role="tablist" aria-label="Refine mode">
|
|
698
893
|
<button id="editTab" class="tab" type="button" role="tab">Edit</button>
|
|
699
894
|
<button id="inspectTab" class="tab" type="button" role="tab">Inspect</button>
|
|
700
895
|
</div>
|
|
701
896
|
<div id="editPanel" class="tab-panel">
|
|
702
897
|
<div class="panel">
|
|
703
|
-
<div class="label">
|
|
704
|
-
<div id="comment" class="comment-editor" contenteditable="true" role="textbox" aria-multiline="true" data-placeholder="
|
|
898
|
+
<div class="label">Describe the change</div>
|
|
899
|
+
<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>
|
|
705
900
|
</div>
|
|
706
|
-
<div
|
|
707
|
-
|
|
901
|
+
<div class="edit-assets" aria-label="Edit assets">
|
|
902
|
+
<div class="panel">
|
|
903
|
+
<div class="asset-tools"><div class="label">Local Assets</div><button id="assetSearchToggle" class="asset-search-toggle" type="button" aria-expanded="false" aria-controls="assetSearchView" title="Search assets">+</button></div>
|
|
904
|
+
<div id="editSavedAssets" class="asset-grid"><p class="asset-empty">No local assets yet. Click + to search assets.</p></div>
|
|
905
|
+
</div>
|
|
906
|
+
</div>
|
|
907
|
+
<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>
|
|
908
|
+
<div class="activity-panel"><div class="label">Activity</div><div id="commentThread" class="comment-thread" aria-live="polite"></div></div>
|
|
708
909
|
</div>
|
|
709
910
|
<div id="inspectPanel" class="tab-panel">
|
|
911
|
+
<div class="panel">
|
|
912
|
+
<label class="label" for="inspectComment">Inspect comment</label>
|
|
913
|
+
<div id="inspectComment" class="comment-editor" contenteditable="true" role="textbox" aria-multiline="true" data-placeholder="Cmd/Ctrl-click slide elements to add @refs, then ask about purpose or source."></div>
|
|
914
|
+
</div>
|
|
710
915
|
<div class="inspect-actions">
|
|
711
916
|
<div class="inspect-options"><label for="inspectLanguage">Display Language</label><select id="inspectLanguage" class="inspect-select"><option>Auto</option><option>English</option><option>简体中文</option><option>繁體中文</option><option>日本語</option><option>Deutsch</option><option>Français</option><option>Español</option><option>Português</option><option>Arabic</option></select></div>
|
|
712
|
-
<button id="inspectButton" disabled>Inspect
|
|
917
|
+
<button id="inspectButton" disabled>Inspect Reference</button>
|
|
713
918
|
<div id="inspectStale"></div>
|
|
714
919
|
</div>
|
|
715
|
-
<div id="inspectCards" class="inspect-cards"><div class="inspect-empty">Select
|
|
920
|
+
<div id="inspectCards" class="inspect-cards"><div class="inspect-empty">Select a deck element to create an @ref, optionally ask a question, then Inspect. This does not edit the deck.</div></div>
|
|
921
|
+
</div>
|
|
922
|
+
<div id="assetSearchView" class="asset-search-view" aria-hidden="true">
|
|
923
|
+
<div class="asset-search-head">
|
|
924
|
+
<button id="assetSearchBack" class="asset-back" type="button">← Back</button>
|
|
925
|
+
<div class="asset-search-title"><h2>Search Assets</h2><span>Save images to Local Assets, then use them from Edit.</span></div>
|
|
926
|
+
</div>
|
|
927
|
+
<div class="panel">
|
|
928
|
+
<div class="asset-search"><input id="assetQuery" type="search" placeholder="Company logo, product photo, portrait..." /><select id="assetPurpose"><option value="logo" selected>logo</option><option value="illustration">photo</option><option value="hero">hero</option><option value="portrait">portrait</option><option value="screenshot">screenshot</option></select></div>
|
|
929
|
+
<div class="asset-actions"><button id="assetSearchButton" type="button">Search Assets</button><button id="assetShuffleButton" type="button" disabled>Refresh</button></div>
|
|
930
|
+
<div id="assetResults" class="asset-grid"><p class="asset-empty">Search image candidates, then save one to the workspace.</p></div>
|
|
931
|
+
</div>
|
|
716
932
|
</div>
|
|
717
933
|
<div id="status" class="status"></div>
|
|
718
934
|
</aside>
|
|
@@ -752,16 +968,34 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
752
968
|
bound: false,
|
|
753
969
|
commentRange: null,
|
|
754
970
|
resizeDrag: null,
|
|
971
|
+
deckSlideIndex: 0,
|
|
972
|
+
deckSlideCount: 0,
|
|
755
973
|
mode: defaultMode === 'inspect' ? 'inspect' : 'edit',
|
|
756
974
|
inspecting: false,
|
|
757
975
|
activeInspectRequestId: '',
|
|
758
976
|
inspectLanguage: 'Auto',
|
|
759
977
|
inspectFallback: null,
|
|
978
|
+
sendingEdit: false,
|
|
979
|
+
assetCandidates: [],
|
|
980
|
+
savedAssets: [],
|
|
981
|
+
selectedAsset: null,
|
|
982
|
+
draggingAsset: null,
|
|
983
|
+
assetDropTarget: null,
|
|
984
|
+
assetDropOutline: null,
|
|
985
|
+
assetSearchBusy: false,
|
|
986
|
+
assetSavingIndex: -1,
|
|
987
|
+
assetSearchPage: 1,
|
|
988
|
+
assetSearchKey: '',
|
|
989
|
+
assetVisibleCount: 0,
|
|
990
|
+
assetPendingCount: 0,
|
|
760
991
|
};
|
|
761
992
|
const els = {
|
|
762
993
|
frame: null,
|
|
763
994
|
hitbox: null,
|
|
764
995
|
resizeHandle: null,
|
|
996
|
+
deckPrev: null,
|
|
997
|
+
deckNext: null,
|
|
998
|
+
deckCounter: null,
|
|
765
999
|
selectionSummary: null,
|
|
766
1000
|
selectionChips: null,
|
|
767
1001
|
editTab: null,
|
|
@@ -771,10 +1005,20 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
771
1005
|
comment: null,
|
|
772
1006
|
commentThread: null,
|
|
773
1007
|
send: null,
|
|
1008
|
+
inspectComment: null,
|
|
774
1009
|
inspectButton: null,
|
|
775
1010
|
inspectLanguage: null,
|
|
776
1011
|
inspectCards: null,
|
|
777
1012
|
inspectStale: null,
|
|
1013
|
+
assetSearchToggle: null,
|
|
1014
|
+
assetSearchBack: null,
|
|
1015
|
+
assetSearchView: null,
|
|
1016
|
+
assetQuery: null,
|
|
1017
|
+
assetPurpose: null,
|
|
1018
|
+
assetSearchButton: null,
|
|
1019
|
+
assetShuffleButton: null,
|
|
1020
|
+
assetResults: null,
|
|
1021
|
+
editSavedAssets: null,
|
|
778
1022
|
status: null,
|
|
779
1023
|
};
|
|
780
1024
|
|
|
@@ -792,6 +1036,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
792
1036
|
els.frame = document.getElementById('deck');
|
|
793
1037
|
els.hitbox = document.getElementById('hitbox');
|
|
794
1038
|
els.resizeHandle = document.getElementById('resizeHandle');
|
|
1039
|
+
els.deckPrev = document.getElementById('deckPrev');
|
|
1040
|
+
els.deckNext = document.getElementById('deckNext');
|
|
1041
|
+
els.deckCounter = document.getElementById('deckCounter');
|
|
795
1042
|
els.selectionSummary = document.getElementById('selectionSummary');
|
|
796
1043
|
els.selectionChips = document.getElementById('selectionChips');
|
|
797
1044
|
els.editTab = document.getElementById('editTab');
|
|
@@ -801,14 +1048,24 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
801
1048
|
els.comment = document.getElementById('comment');
|
|
802
1049
|
els.commentThread = document.getElementById('commentThread');
|
|
803
1050
|
els.send = document.getElementById('send');
|
|
1051
|
+
els.inspectComment = document.getElementById('inspectComment');
|
|
804
1052
|
els.inspectButton = document.getElementById('inspectButton');
|
|
805
1053
|
els.inspectCards = document.getElementById('inspectCards');
|
|
806
1054
|
els.inspectStale = document.getElementById('inspectStale');
|
|
1055
|
+
els.assetSearchToggle = document.getElementById('assetSearchToggle');
|
|
1056
|
+
els.assetSearchBack = document.getElementById('assetSearchBack');
|
|
1057
|
+
els.assetSearchView = document.getElementById('assetSearchView');
|
|
1058
|
+
els.assetQuery = document.getElementById('assetQuery');
|
|
1059
|
+
els.assetPurpose = document.getElementById('assetPurpose');
|
|
1060
|
+
els.assetSearchButton = document.getElementById('assetSearchButton');
|
|
1061
|
+
els.assetShuffleButton = document.getElementById('assetShuffleButton');
|
|
1062
|
+
els.assetResults = document.getElementById('assetResults');
|
|
1063
|
+
els.editSavedAssets = document.getElementById('editSavedAssets');
|
|
807
1064
|
els.status = document.getElementById('status');
|
|
808
1065
|
|
|
809
1066
|
els.inspectLanguage = document.getElementById('inspectLanguage');
|
|
810
1067
|
|
|
811
|
-
if (!els.frame || !els.hitbox || !els.resizeHandle || !els.selectionSummary || !els.selectionChips || !els.editTab || !els.inspectTab || !els.editPanel || !els.inspectPanel || !els.comment || !els.commentThread || !els.send || !els.inspectButton || !els.inspectLanguage || !els.inspectCards || !els.inspectStale || !els.status) {
|
|
1068
|
+
if (!els.frame || !els.hitbox || !els.resizeHandle || !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) {
|
|
812
1069
|
throw new Error('Editor boot failed: required DOM nodes are missing.');
|
|
813
1070
|
}
|
|
814
1071
|
|
|
@@ -817,6 +1074,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
817
1074
|
setMode(state.mode);
|
|
818
1075
|
setStatus('Editor ready. Ctrl/Cmd + click deck elements to reference them.');
|
|
819
1076
|
initFrame();
|
|
1077
|
+
loadSavedAssets();
|
|
820
1078
|
startDeckVersionPolling();
|
|
821
1079
|
} catch (error) {
|
|
822
1080
|
reportError(error);
|
|
@@ -828,15 +1086,34 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
828
1086
|
state.bound = true;
|
|
829
1087
|
els.frame.addEventListener('load', initFrame);
|
|
830
1088
|
document.addEventListener('keydown', (event) => {
|
|
831
|
-
if (event.key === 'Escape')
|
|
1089
|
+
if (event.key === 'Escape') {
|
|
1090
|
+
clearHover();
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (isTextInputTarget(event.target) || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
1094
|
+
if (['ArrowDown', 'ArrowRight', ' ', 'PageDown'].includes(event.key)) {
|
|
1095
|
+
event.preventDefault();
|
|
1096
|
+
nextDeckSlide();
|
|
1097
|
+
} else if (['ArrowUp', 'ArrowLeft', 'PageUp'].includes(event.key)) {
|
|
1098
|
+
event.preventDefault();
|
|
1099
|
+
prevDeckSlide();
|
|
1100
|
+
}
|
|
832
1101
|
});
|
|
833
1102
|
els.comment.addEventListener('input', () => {
|
|
834
1103
|
saveCommentRange();
|
|
835
|
-
syncReferencesFromComment(false);
|
|
1104
|
+
syncReferencesFromComment(false, els.comment);
|
|
1105
|
+
syncSelectedAssetFromComment();
|
|
836
1106
|
updateSendState();
|
|
837
1107
|
});
|
|
838
1108
|
els.comment.addEventListener('keyup', saveCommentRange);
|
|
839
1109
|
els.comment.addEventListener('mouseup', saveCommentRange);
|
|
1110
|
+
els.inspectComment.addEventListener('input', () => {
|
|
1111
|
+
saveCommentRange();
|
|
1112
|
+
syncReferencesFromComment(false, els.inspectComment);
|
|
1113
|
+
updateSendState();
|
|
1114
|
+
});
|
|
1115
|
+
els.inspectComment.addEventListener('keyup', saveCommentRange);
|
|
1116
|
+
els.inspectComment.addEventListener('mouseup', saveCommentRange);
|
|
840
1117
|
document.addEventListener('selectionchange', saveCommentRange);
|
|
841
1118
|
els.hitbox.addEventListener('pointermove', onHover);
|
|
842
1119
|
els.hitbox.addEventListener('pointerdown', onPointerDown);
|
|
@@ -854,13 +1131,29 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
854
1131
|
}, { passive: false });
|
|
855
1132
|
els.resizeHandle.addEventListener('pointerdown', startEditorResize);
|
|
856
1133
|
els.resizeHandle.addEventListener('dblclick', resetEditorWidth);
|
|
1134
|
+
els.deckPrev.addEventListener('click', prevDeckSlide);
|
|
1135
|
+
els.deckNext.addEventListener('click', nextDeckSlide);
|
|
857
1136
|
els.send.addEventListener('click', sendComment);
|
|
858
1137
|
els.inspectButton.addEventListener('click', inspectCurrentSelection);
|
|
1138
|
+
els.assetSearchToggle.addEventListener('click', toggleAssetSearchPanel);
|
|
1139
|
+
els.assetSearchBack.addEventListener('click', closeAssetSearchPanel);
|
|
1140
|
+
els.assetSearchButton.addEventListener('click', () => searchAssets(false));
|
|
1141
|
+
els.assetShuffleButton.addEventListener('click', () => searchAssets(true));
|
|
1142
|
+
els.assetQuery.addEventListener('keydown', (event) => {
|
|
1143
|
+
if (event.key === 'Enter') {
|
|
1144
|
+
event.preventDefault();
|
|
1145
|
+
searchAssets(false);
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
els.assetPurpose.addEventListener('change', () => resetAssetSearchBatch());
|
|
859
1149
|
els.inspectLanguage.addEventListener('change', () => {
|
|
860
1150
|
state.inspectLanguage = els.inspectLanguage.value || 'Auto';
|
|
861
1151
|
});
|
|
862
1152
|
els.editTab.addEventListener('click', () => setMode('edit'));
|
|
863
1153
|
els.inspectTab.addEventListener('click', () => setMode('inspect'));
|
|
1154
|
+
els.hitbox.addEventListener('dragover', onAssetDragOver);
|
|
1155
|
+
els.hitbox.addEventListener('dragleave', onAssetDragLeave);
|
|
1156
|
+
els.hitbox.addEventListener('drop', onAssetDrop);
|
|
864
1157
|
}
|
|
865
1158
|
|
|
866
1159
|
function setMode(mode) {
|
|
@@ -869,9 +1162,31 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
869
1162
|
els.inspectTab.classList.toggle('active', state.mode === 'inspect');
|
|
870
1163
|
els.editPanel.classList.toggle('active', state.mode === 'edit');
|
|
871
1164
|
els.inspectPanel.classList.toggle('active', state.mode === 'inspect');
|
|
1165
|
+
saveCommentRange();
|
|
872
1166
|
updateSendState();
|
|
873
1167
|
}
|
|
874
1168
|
|
|
1169
|
+
function activeCommentEditor() {
|
|
1170
|
+
return state.mode === 'inspect' ? els.inspectComment : els.comment;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function toggleAssetSearchPanel() {
|
|
1174
|
+
const open = !els.assetSearchView.classList.contains('open');
|
|
1175
|
+
setAssetSearchOpen(open);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function closeAssetSearchPanel() {
|
|
1179
|
+
setAssetSearchOpen(false);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function setAssetSearchOpen(open) {
|
|
1183
|
+
els.assetSearchView.classList.toggle('open', open);
|
|
1184
|
+
els.assetSearchView.setAttribute('aria-hidden', open ? 'false' : 'true');
|
|
1185
|
+
els.assetSearchToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
1186
|
+
els.assetSearchToggle.textContent = '+';
|
|
1187
|
+
if (open) els.assetQuery.focus();
|
|
1188
|
+
}
|
|
1189
|
+
|
|
875
1190
|
function restoreEditorWidth() {
|
|
876
1191
|
try {
|
|
877
1192
|
const saved = Number(window.localStorage.getItem(EDITOR_WIDTH_KEY));
|
|
@@ -934,12 +1249,15 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
934
1249
|
clearReferences(false);
|
|
935
1250
|
state.hoverEl = null;
|
|
936
1251
|
state.hoverOutline = createOutline(doc, '#38bdf8', 'rgba(56,189,248,.12)');
|
|
1252
|
+
state.assetDropTarget = null;
|
|
1253
|
+
state.assetDropOutline = createOutline(doc, '#a9793f', 'rgba(169,121,63,.16)');
|
|
937
1254
|
state.referenceOutlines = [];
|
|
938
1255
|
doc.addEventListener('scroll', () => {
|
|
939
1256
|
renderHoverOutline(state.hoverEl);
|
|
940
1257
|
renderReferenceOutlines();
|
|
941
1258
|
}, true);
|
|
942
1259
|
const slides = getSlides(doc);
|
|
1260
|
+
syncDeckNavigation();
|
|
943
1261
|
updateSendState();
|
|
944
1262
|
if (state.pendingRefreshMessage) {
|
|
945
1263
|
state.pendingRefreshMessage = false;
|
|
@@ -952,6 +1270,102 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
952
1270
|
}
|
|
953
1271
|
}
|
|
954
1272
|
|
|
1273
|
+
function isTextInputTarget(target) {
|
|
1274
|
+
if (!target || !(target instanceof Element)) return false;
|
|
1275
|
+
const tag = target.tagName.toLowerCase();
|
|
1276
|
+
return tag === 'input' || tag === 'textarea' || tag === 'select' || target.isContentEditable || Boolean(target.closest('[contenteditable="true"]'));
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function syncDeckNavigation() {
|
|
1280
|
+
try {
|
|
1281
|
+
const doc = els.frame.contentDocument;
|
|
1282
|
+
const slides = doc ? getSlides(doc) : [];
|
|
1283
|
+
state.deckSlideCount = slides.length;
|
|
1284
|
+
state.deckSlideIndex = Math.max(0, Math.min(state.deckSlideIndex, Math.max(0, slides.length - 1)));
|
|
1285
|
+
updateDeckNavControls();
|
|
1286
|
+
} catch {
|
|
1287
|
+
state.deckSlideCount = 0;
|
|
1288
|
+
state.deckSlideIndex = 0;
|
|
1289
|
+
updateDeckNavControls();
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function updateDeckNavControls() {
|
|
1294
|
+
const total = state.deckSlideCount;
|
|
1295
|
+
const current = total > 0 ? state.deckSlideIndex + 1 : 0;
|
|
1296
|
+
els.deckCounter.textContent = total > 0 ? current + ' / ' + total : '-- / --';
|
|
1297
|
+
els.deckPrev.disabled = total <= 1 || state.deckSlideIndex <= 0;
|
|
1298
|
+
els.deckNext.disabled = total <= 1 || state.deckSlideIndex >= total - 1;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function prevDeckSlide() {
|
|
1302
|
+
goToDeckSlide(state.deckSlideIndex - 1);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function nextDeckSlide() {
|
|
1306
|
+
goToDeckSlide(state.deckSlideIndex + 1);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function goToDeckSlide(index) {
|
|
1310
|
+
try {
|
|
1311
|
+
const doc = els.frame.contentDocument;
|
|
1312
|
+
const win = els.frame.contentWindow;
|
|
1313
|
+
if (!doc || !win) return;
|
|
1314
|
+
const slides = getSlides(doc);
|
|
1315
|
+
if (!slides.length) {
|
|
1316
|
+
syncDeckNavigation();
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const clamped = Math.max(0, Math.min(slides.length - 1, index));
|
|
1320
|
+
const nav = win.RevelaDeckNav;
|
|
1321
|
+
let handled = false;
|
|
1322
|
+
if (nav && typeof nav.goTo === 'function') {
|
|
1323
|
+
try {
|
|
1324
|
+
nav.goTo(clamped);
|
|
1325
|
+
handled = true;
|
|
1326
|
+
} catch {}
|
|
1327
|
+
} else if (nav && clamped > state.deckSlideIndex && typeof nav.next === 'function') {
|
|
1328
|
+
try {
|
|
1329
|
+
nav.next();
|
|
1330
|
+
handled = true;
|
|
1331
|
+
} catch {}
|
|
1332
|
+
} else if (nav && clamped < state.deckSlideIndex && typeof nav.prev === 'function') {
|
|
1333
|
+
try {
|
|
1334
|
+
nav.prev();
|
|
1335
|
+
handled = true;
|
|
1336
|
+
} catch {}
|
|
1337
|
+
}
|
|
1338
|
+
if (!handled) applyFallbackDeckNavigation(win, doc, slides, clamped);
|
|
1339
|
+
state.deckSlideIndex = clamped;
|
|
1340
|
+
updateDeckNavControls();
|
|
1341
|
+
renderHoverOutline(state.hoverEl);
|
|
1342
|
+
renderReferenceOutlines();
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
reportError(error);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function applyFallbackDeckNavigation(win, doc, slides, index) {
|
|
1349
|
+
const target = slides[index];
|
|
1350
|
+
const usesOverlaySlides = slides.some((slide) => {
|
|
1351
|
+
const style = win.getComputedStyle(slide);
|
|
1352
|
+
return style.position === 'absolute' || style.position === 'fixed' || style.opacity === '0' || slide.style.opacity !== '';
|
|
1353
|
+
});
|
|
1354
|
+
if (usesOverlaySlides) {
|
|
1355
|
+
slides.forEach((slide, i) => {
|
|
1356
|
+
slide.style.opacity = i === index ? '1' : '0';
|
|
1357
|
+
slide.style.pointerEvents = i === index ? 'auto' : 'none';
|
|
1358
|
+
});
|
|
1359
|
+
win.scrollTo?.(0, 0);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (target && typeof target.scrollIntoView === 'function') {
|
|
1363
|
+
target.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'auto' });
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
doc.defaultView?.scrollTo?.(0, index * win.innerHeight);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
955
1369
|
function startDeckVersionPolling() {
|
|
956
1370
|
pollDeckVersion();
|
|
957
1371
|
window.setInterval(pollDeckVersion, 2000);
|
|
@@ -987,6 +1401,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
987
1401
|
clearReferences(true);
|
|
988
1402
|
state.hoverEl = null;
|
|
989
1403
|
if (state.hoverOutline) state.hoverOutline.style.display = 'none';
|
|
1404
|
+
state.assetDropTarget = null;
|
|
1405
|
+
if (state.assetDropOutline) state.assetDropOutline.style.display = 'none';
|
|
990
1406
|
state.referenceOutlines.forEach((outline) => outline.style.display = 'none');
|
|
991
1407
|
state.referenceOutlines = [];
|
|
992
1408
|
updateSendState();
|
|
@@ -1038,34 +1454,353 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1038
1454
|
}
|
|
1039
1455
|
|
|
1040
1456
|
async function sendComment() {
|
|
1041
|
-
syncReferencesFromComment(false);
|
|
1457
|
+
syncReferencesFromComment(false, els.comment);
|
|
1458
|
+
syncSelectedAssetFromComment();
|
|
1042
1459
|
const text = getCommentText().trim();
|
|
1043
1460
|
if (!text) return;
|
|
1044
1461
|
const elements = state.references.map((reference) => reference.payload);
|
|
1462
|
+
const asset = state.selectedAsset || undefined;
|
|
1045
1463
|
const commentId = addPendingComment(text, elements, 'sending');
|
|
1046
1464
|
clearReferences(false);
|
|
1465
|
+
state.selectedAsset = null;
|
|
1047
1466
|
els.comment.textContent = '';
|
|
1048
1467
|
renderReferenceOutlines();
|
|
1049
|
-
|
|
1468
|
+
state.sendingEdit = true;
|
|
1469
|
+
updateSendState();
|
|
1050
1470
|
setStatus('Sending...');
|
|
1051
1471
|
try {
|
|
1052
1472
|
const res = await fetch('/api/comment?token=' + encodeURIComponent(token), {
|
|
1053
1473
|
method: 'POST',
|
|
1054
1474
|
headers: { 'content-type': 'application/json' },
|
|
1055
|
-
body: JSON.stringify({ comment: text, elements }),
|
|
1475
|
+
body: JSON.stringify({ comment: text, elements, asset }),
|
|
1056
1476
|
});
|
|
1057
1477
|
const body = await res.json().catch(() => ({}));
|
|
1058
1478
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to send comment');
|
|
1059
1479
|
updatePendingCommentStatus(commentId, 'sent', { baseDeckVersion: body.deckVersion || state.deckVersion });
|
|
1060
1480
|
if (pendingCommentStatus(commentId) !== 'updated') setStatus('Comment sent. Waiting for deck update...');
|
|
1481
|
+
state.sendingEdit = false;
|
|
1061
1482
|
updateSendState();
|
|
1062
1483
|
} catch (error) {
|
|
1063
1484
|
updatePendingCommentStatus(commentId, 'failed');
|
|
1485
|
+
state.sendingEdit = false;
|
|
1064
1486
|
reportError(error);
|
|
1065
1487
|
updateSendState();
|
|
1066
1488
|
}
|
|
1067
1489
|
}
|
|
1068
1490
|
|
|
1491
|
+
async function searchAssets(nextBatch) {
|
|
1492
|
+
const query = (els.assetQuery.value || '').trim();
|
|
1493
|
+
if (!query || state.assetSearchBusy) return;
|
|
1494
|
+
const key = query + '\u0000' + (els.assetPurpose.value || 'illustration');
|
|
1495
|
+
if (!nextBatch || state.assetSearchKey !== key) {
|
|
1496
|
+
state.assetSearchPage = 1;
|
|
1497
|
+
state.assetSearchKey = key;
|
|
1498
|
+
} else {
|
|
1499
|
+
state.assetSearchPage += 1;
|
|
1500
|
+
}
|
|
1501
|
+
state.assetSearchBusy = true;
|
|
1502
|
+
els.assetSearchButton.disabled = true;
|
|
1503
|
+
els.assetShuffleButton.disabled = true;
|
|
1504
|
+
renderAssetSearchLoading(nextBatch ? 'Searching another batch...' : 'Searching remote image sources...');
|
|
1505
|
+
setButtonLoading(els.assetSearchButton, true, 'Searching...');
|
|
1506
|
+
setStatus(nextBatch ? 'Searching another asset batch...' : 'Searching assets...');
|
|
1507
|
+
try {
|
|
1508
|
+
const params = new URLSearchParams({ query, purpose: els.assetPurpose.value || 'illustration', limit: '24', page: String(state.assetSearchPage) });
|
|
1509
|
+
const res = await fetch('/api/assets/search?token=' + encodeURIComponent(token) + '&' + params.toString(), { cache: 'no-store' });
|
|
1510
|
+
const body = await res.json().catch(() => ({}));
|
|
1511
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Asset search failed');
|
|
1512
|
+
state.assetCandidates = Array.isArray(body.candidates) ? body.candidates : [];
|
|
1513
|
+
renderAssetCandidates();
|
|
1514
|
+
setStatus(state.assetCandidates.length ? 'Asset search complete. Click + to save an asset to the workspace.' : (nextBatch ? 'No more assets found. Try another query or purpose.' : 'No assets found. Try another query or purpose.'));
|
|
1515
|
+
} catch (error) {
|
|
1516
|
+
if (nextBatch) state.assetSearchPage = Math.max(1, state.assetSearchPage - 1);
|
|
1517
|
+
els.assetResults.innerHTML = '<p class="asset-empty">' + escapeHtml(error && error.message ? error.message : String(error)) + '</p>';
|
|
1518
|
+
reportError(error);
|
|
1519
|
+
} finally {
|
|
1520
|
+
state.assetSearchBusy = false;
|
|
1521
|
+
setButtonLoading(els.assetSearchButton, false, 'Search Assets');
|
|
1522
|
+
updateAssetShuffleState();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function renderAssetSearchLoading(message) {
|
|
1527
|
+
els.assetResults.innerHTML = '<p class="asset-empty"><span class="loading-row"><span class="spinner" aria-hidden="true"></span>' + escapeHtml(message) + '</span></p>'
|
|
1528
|
+
+ Array.from({ length: 8 }, () => '<div class="skeleton-card asset-skeleton"><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div>').join('');
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function resetAssetSearchBatch() {
|
|
1532
|
+
state.assetSearchPage = 1;
|
|
1533
|
+
state.assetSearchKey = '';
|
|
1534
|
+
updateAssetShuffleState();
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function updateAssetShuffleState() {
|
|
1538
|
+
els.assetShuffleButton.disabled = state.assetSearchBusy || !state.assetCandidates.length;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function renderAssetCandidates() {
|
|
1542
|
+
els.assetResults.textContent = '';
|
|
1543
|
+
state.assetVisibleCount = 0;
|
|
1544
|
+
state.assetPendingCount = state.assetCandidates.length;
|
|
1545
|
+
if (!state.assetCandidates.length) {
|
|
1546
|
+
els.assetResults.innerHTML = '<p class="asset-empty">No assets found. Try another query or purpose.</p>';
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
state.assetCandidates.forEach((candidate, index) => {
|
|
1550
|
+
const card = assetCard(candidate, false, index);
|
|
1551
|
+
if (state.assetSavingIndex === index) {
|
|
1552
|
+
card.classList.add('is-saving');
|
|
1553
|
+
appendAssetSaveButton(card, 'Saving...', 'Saving to workspace', () => {}, true);
|
|
1554
|
+
} else {
|
|
1555
|
+
appendAssetSaveButton(card, 'Save', 'Save to workspace', () => saveCandidate(index));
|
|
1556
|
+
}
|
|
1557
|
+
els.assetResults.appendChild(card);
|
|
1558
|
+
});
|
|
1559
|
+
updateAssetShuffleState();
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async function saveCandidate(index) {
|
|
1563
|
+
const candidate = state.assetCandidates[index];
|
|
1564
|
+
if (!candidate) return;
|
|
1565
|
+
state.assetSavingIndex = index;
|
|
1566
|
+
renderAssetCandidates();
|
|
1567
|
+
setStatus('Saving asset to workspace...');
|
|
1568
|
+
try {
|
|
1569
|
+
const res = await fetch('/api/assets/save?token=' + encodeURIComponent(token), {
|
|
1570
|
+
method: 'POST',
|
|
1571
|
+
headers: { 'content-type': 'application/json' },
|
|
1572
|
+
body: JSON.stringify({ candidate, purpose: els.assetPurpose.value || candidate.purpose || 'illustration' }),
|
|
1573
|
+
});
|
|
1574
|
+
const body = await res.json().catch(() => ({}));
|
|
1575
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to save asset');
|
|
1576
|
+
await loadSavedAssets();
|
|
1577
|
+
const path = body.asset && (body.asset.path || body.asset.deckPath);
|
|
1578
|
+
setStatus(path ? 'Saved to ' + path + '. Use it from Local Assets.' : 'Asset saved. Use it from Local Assets.');
|
|
1579
|
+
} catch (error) {
|
|
1580
|
+
reportError(error);
|
|
1581
|
+
} finally {
|
|
1582
|
+
state.assetSavingIndex = -1;
|
|
1583
|
+
renderAssetCandidates();
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
async function loadSavedAssets() {
|
|
1588
|
+
try {
|
|
1589
|
+
const res = await fetch('/api/assets/list?token=' + encodeURIComponent(token), { cache: 'no-store' });
|
|
1590
|
+
const body = await res.json().catch(() => ({}));
|
|
1591
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to list assets');
|
|
1592
|
+
state.savedAssets = Array.isArray(body.assets) ? body.assets : [];
|
|
1593
|
+
renderSavedAssets();
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
const message = '<p class="asset-empty">' + escapeHtml(error && error.message ? error.message : String(error)) + '</p>';
|
|
1596
|
+
els.editSavedAssets.innerHTML = message;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function renderSavedAssets() {
|
|
1601
|
+
renderSavedAssetGrid(els.editSavedAssets, 'No local assets yet. Click + to search assets.');
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function renderSavedAssetGrid(container, emptyMessage) {
|
|
1605
|
+
container.textContent = '';
|
|
1606
|
+
if (!state.savedAssets.length) {
|
|
1607
|
+
container.innerHTML = '<p class="asset-empty">' + escapeHtml(emptyMessage) + '</p>';
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
state.savedAssets.forEach((asset) => {
|
|
1611
|
+
const card = assetCard(asset, true, 0);
|
|
1612
|
+
card.draggable = true;
|
|
1613
|
+
card.addEventListener('click', () => addAssetToComment(asset));
|
|
1614
|
+
card.addEventListener('dragstart', (event) => {
|
|
1615
|
+
state.draggingAsset = asset;
|
|
1616
|
+
event.dataTransfer?.setData('application/revela-asset-id', asset.id || '');
|
|
1617
|
+
event.dataTransfer?.setData('text/plain', asset.path || asset.id || '');
|
|
1618
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'copy';
|
|
1619
|
+
});
|
|
1620
|
+
card.addEventListener('dragend', () => {
|
|
1621
|
+
state.draggingAsset = null;
|
|
1622
|
+
document.body.classList.remove('drop-active');
|
|
1623
|
+
});
|
|
1624
|
+
container.appendChild(card);
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function assetCard(asset, saved, index) {
|
|
1629
|
+
const card = document.createElement('div');
|
|
1630
|
+
card.className = saved ? 'asset-card saved' : 'asset-card';
|
|
1631
|
+
const image = document.createElement('img');
|
|
1632
|
+
image.className = 'asset-thumb';
|
|
1633
|
+
image.loading = !saved && index < 8 ? 'eager' : 'lazy';
|
|
1634
|
+
image.decoding = 'async';
|
|
1635
|
+
image.alt = asset.alt || asset.title || asset.id || 'Image asset';
|
|
1636
|
+
if (!saved) {
|
|
1637
|
+
image.addEventListener('load', () => markAssetImageLoaded());
|
|
1638
|
+
image.addEventListener('error', () => hideBrokenAssetCard(card));
|
|
1639
|
+
}
|
|
1640
|
+
image.src = saved ? (asset.previewUrl || '') : (asset.thumbnailUrl || asset.imageUrl || '');
|
|
1641
|
+
card.title = asset.title || asset.id || asset.alt || 'Image asset';
|
|
1642
|
+
card.appendChild(image);
|
|
1643
|
+
return card;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function markAssetImageLoaded() {
|
|
1647
|
+
state.assetVisibleCount += 1;
|
|
1648
|
+
state.assetPendingCount = Math.max(0, state.assetPendingCount - 1);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function hideBrokenAssetCard(card) {
|
|
1652
|
+
card.remove();
|
|
1653
|
+
state.assetPendingCount = Math.max(0, state.assetPendingCount - 1);
|
|
1654
|
+
if (!state.assetVisibleCount && !state.assetPendingCount) {
|
|
1655
|
+
els.assetResults.innerHTML = '<p class="asset-empty">No displayable images found. Try Refresh or another purpose.</p>';
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function appendAssetSaveButton(card, text, label, onClick, loading) {
|
|
1660
|
+
const button = document.createElement('button');
|
|
1661
|
+
button.type = 'button';
|
|
1662
|
+
button.className = 'asset-save';
|
|
1663
|
+
button.innerHTML = loading ? '<span class="spinner" aria-hidden="true"></span><span>' + escapeHtml(text) + '</span>' : escapeHtml(text);
|
|
1664
|
+
button.disabled = !!loading;
|
|
1665
|
+
button.setAttribute('aria-label', label);
|
|
1666
|
+
button.title = label;
|
|
1667
|
+
button.addEventListener('click', onClick);
|
|
1668
|
+
card.appendChild(button);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function addAssetToComment(asset) {
|
|
1672
|
+
if (!asset) return;
|
|
1673
|
+
state.selectedAsset = asset;
|
|
1674
|
+
removeAssetChip();
|
|
1675
|
+
const intro = els.comment.textContent && !/\s$/.test(els.comment.textContent) ? ' Use asset ' : 'Use asset ';
|
|
1676
|
+
insertPlainText(intro);
|
|
1677
|
+
insertAssetChip(asset);
|
|
1678
|
+
insertPlainText(' ');
|
|
1679
|
+
setMode('edit');
|
|
1680
|
+
updateSendState();
|
|
1681
|
+
setStatus('Asset added to the Edit comment. Describe where or how to use it, then Apply Fix.');
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function onAssetDragOver(event) {
|
|
1685
|
+
if (!state.draggingAsset) return;
|
|
1686
|
+
event.preventDefault();
|
|
1687
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
|
|
1688
|
+
document.body.classList.add('drop-active');
|
|
1689
|
+
const placement = collectAssetPlacement(event, state.draggingAsset);
|
|
1690
|
+
renderAssetDropTarget(placement);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function onAssetDragLeave() {
|
|
1694
|
+
document.body.classList.remove('drop-active');
|
|
1695
|
+
renderAssetDropTarget(null);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
async function onAssetDrop(event) {
|
|
1699
|
+
const asset = state.draggingAsset || findSavedAsset(event.dataTransfer?.getData('application/revela-asset-id'));
|
|
1700
|
+
if (!asset) return;
|
|
1701
|
+
event.preventDefault();
|
|
1702
|
+
document.body.classList.remove('drop-active');
|
|
1703
|
+
renderAssetDropTarget(null);
|
|
1704
|
+
const placement = collectAssetPlacement(event, asset);
|
|
1705
|
+
if (!placement) {
|
|
1706
|
+
setStatus('Drop the asset onto a deck slide.');
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
await sendAssetPlacement(asset, placement);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function findSavedAsset(id) {
|
|
1713
|
+
return state.savedAssets.find((asset) => asset.id === id) || null;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function collectAssetPlacement(event, asset) {
|
|
1717
|
+
initFrame();
|
|
1718
|
+
const doc = els.frame.contentDocument;
|
|
1719
|
+
const win = els.frame.contentWindow;
|
|
1720
|
+
if (!doc || !win) return null;
|
|
1721
|
+
const frameRect = els.frame.getBoundingClientRect();
|
|
1722
|
+
const frameX = event.clientX - frameRect.left;
|
|
1723
|
+
const frameY = event.clientY - frameRect.top;
|
|
1724
|
+
const rawTarget = doc.elementFromPoint(frameX, frameY);
|
|
1725
|
+
const target = selectable(rawTarget);
|
|
1726
|
+
const slide = findSlide(target) || findSlide(rawTarget);
|
|
1727
|
+
if (!slide) return null;
|
|
1728
|
+
const slides = getSlides(doc);
|
|
1729
|
+
const explicitSlideIndex = Number(slide.getAttribute('data-slide-index'));
|
|
1730
|
+
const slideIndex = Number.isFinite(explicitSlideIndex) && explicitSlideIndex > 0 ? explicitSlideIndex : slides.indexOf(slide) + 1;
|
|
1731
|
+
const slideRect = slide.getBoundingClientRect();
|
|
1732
|
+
const x = slideRect.width ? Math.max(0, Math.min(1, (frameX - slideRect.left) / slideRect.width)) : 0;
|
|
1733
|
+
const y = slideRect.height ? Math.max(0, Math.min(1, (frameY - slideRect.top) / slideRect.height)) : 0;
|
|
1734
|
+
const targetPayload = target && target !== slide ? collectPayload(target) : null;
|
|
1735
|
+
const targetMode = targetPayload
|
|
1736
|
+
? (target && target.tagName && target.tagName.toLowerCase() === 'img' ? 'replace' : 'insert-into')
|
|
1737
|
+
: 'add';
|
|
1738
|
+
return {
|
|
1739
|
+
slideIndex,
|
|
1740
|
+
x,
|
|
1741
|
+
y,
|
|
1742
|
+
viewport: { width: win.innerWidth, height: win.innerHeight },
|
|
1743
|
+
nearbyText: (slide.innerText || slide.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 1000),
|
|
1744
|
+
targetMode,
|
|
1745
|
+
target: targetPayload,
|
|
1746
|
+
targetLabel: targetMode === 'replace' ? 'Replace image' : targetMode === 'insert-into' ? 'Insert into this element' : 'Add near here',
|
|
1747
|
+
targetTagName: target?.tagName?.toLowerCase(),
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function renderAssetDropTarget(placement) {
|
|
1752
|
+
if (!state.assetDropOutline) return;
|
|
1753
|
+
const target = placement?.target ? elementFromPayload(placement.target) : null;
|
|
1754
|
+
state.assetDropTarget = target;
|
|
1755
|
+
renderBox(state.assetDropOutline, target);
|
|
1756
|
+
if (placement?.targetLabel) setStatus(placement.targetLabel + '. Drop to send an asset placement comment.');
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
function elementFromPayload(payload) {
|
|
1760
|
+
const doc = els.frame.contentDocument;
|
|
1761
|
+
if (!doc || !payload) return null;
|
|
1762
|
+
const slides = getSlides(doc);
|
|
1763
|
+
const slide = slides.find((item, index) => {
|
|
1764
|
+
const explicit = Number(item.getAttribute('data-slide-index'));
|
|
1765
|
+
const slideIndex = Number.isFinite(explicit) && explicit > 0 ? explicit : index + 1;
|
|
1766
|
+
return slideIndex === payload.slideIndex;
|
|
1767
|
+
});
|
|
1768
|
+
if (!slide) return null;
|
|
1769
|
+
if (payload.selector) {
|
|
1770
|
+
try {
|
|
1771
|
+
const selected = slide.querySelector(payload.selector);
|
|
1772
|
+
if (selected) return selected;
|
|
1773
|
+
} catch {}
|
|
1774
|
+
}
|
|
1775
|
+
return slide;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
async function sendAssetPlacement(asset, placement) {
|
|
1779
|
+
const modeText = placement.targetMode === 'replace'
|
|
1780
|
+
? 'replace the image at the drop target'
|
|
1781
|
+
: placement.targetMode === 'insert-into'
|
|
1782
|
+
? 'insert it into the target element'
|
|
1783
|
+
: 'add it near the drop point';
|
|
1784
|
+
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.';
|
|
1785
|
+
const elements = placement.target ? [placement.target] : [];
|
|
1786
|
+
const commentId = addPendingComment(comment, elements, 'sending');
|
|
1787
|
+
setStatus('Sending asset placement comment...');
|
|
1788
|
+
try {
|
|
1789
|
+
const res = await fetch('/api/comment?token=' + encodeURIComponent(token), {
|
|
1790
|
+
method: 'POST',
|
|
1791
|
+
headers: { 'content-type': 'application/json' },
|
|
1792
|
+
body: JSON.stringify({ comment, elements, asset, drop: placement }),
|
|
1793
|
+
});
|
|
1794
|
+
const body = await res.json().catch(() => ({}));
|
|
1795
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to send asset placement');
|
|
1796
|
+
updatePendingCommentStatus(commentId, 'sent', { baseDeckVersion: body.deckVersion || state.deckVersion });
|
|
1797
|
+
if (pendingCommentStatus(commentId) !== 'updated') setStatus('Asset placement sent. Waiting for deck update...');
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
updatePendingCommentStatus(commentId, 'failed');
|
|
1800
|
+
reportError(error);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1069
1804
|
function selectable(node) {
|
|
1070
1805
|
if (!node || node.nodeType !== 1) return null;
|
|
1071
1806
|
if (node === state.hoverOutline || state.referenceOutlines.includes(node)) return null;
|
|
@@ -1094,7 +1829,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1094
1829
|
renderReferenceOutlines();
|
|
1095
1830
|
updateSendState();
|
|
1096
1831
|
renderSelectionSummary();
|
|
1097
|
-
resetInspectCards('References ready. Open Inspect and click Inspect
|
|
1832
|
+
resetInspectCards('References ready. Open Inspect and click Inspect Reference for concise Purpose and Source context.');
|
|
1098
1833
|
setStatus('Inserted @' + label + '. ' + state.references.length + ' reference' + (state.references.length === 1 ? '' : 's') + ' will be sent.');
|
|
1099
1834
|
}
|
|
1100
1835
|
|
|
@@ -1116,8 +1851,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1116
1851
|
updateSendState();
|
|
1117
1852
|
}
|
|
1118
1853
|
|
|
1119
|
-
function syncReferencesFromComment(showStatus) {
|
|
1120
|
-
const
|
|
1854
|
+
function syncReferencesFromComment(showStatus, editor) {
|
|
1855
|
+
const source = editor || activeCommentEditor();
|
|
1856
|
+
const activeIds = new Set(Array.from(source.querySelectorAll('.ref-chip[data-ref-id]')).map((chip) => chip.getAttribute('data-ref-id')));
|
|
1121
1857
|
const before = state.references.length;
|
|
1122
1858
|
state.references = state.references.filter((reference) => activeIds.has(reference.id));
|
|
1123
1859
|
if (state.references.length !== before) {
|
|
@@ -1127,6 +1863,11 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1127
1863
|
}
|
|
1128
1864
|
}
|
|
1129
1865
|
|
|
1866
|
+
function syncSelectedAssetFromComment() {
|
|
1867
|
+
if (els.comment.querySelector('.asset-ref-chip[data-asset-id]')) return;
|
|
1868
|
+
state.selectedAsset = null;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1130
1871
|
function addPendingComment(text, elements, status) {
|
|
1131
1872
|
const id = 'comment-' + state.nextCommentId++;
|
|
1132
1873
|
state.pendingComments.push({
|
|
@@ -1271,10 +2012,20 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1271
2012
|
}
|
|
1272
2013
|
|
|
1273
2014
|
function updateSendState() {
|
|
1274
|
-
els.send
|
|
2015
|
+
if (state.sendingEdit) setButtonLoading(els.send, true, 'Sending...');
|
|
2016
|
+
else setButtonLoading(els.send, false, '<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>', true);
|
|
2017
|
+
els.send.disabled = state.sendingEdit || !getCommentText().trim();
|
|
2018
|
+
if (state.inspecting) setButtonLoading(els.inspectButton, true, 'Inspecting...');
|
|
2019
|
+
else setButtonLoading(els.inspectButton, false, 'Inspect Reference');
|
|
1275
2020
|
els.inspectButton.disabled = state.inspecting || state.references.length === 0;
|
|
1276
2021
|
}
|
|
1277
2022
|
|
|
2023
|
+
function setButtonLoading(button, loading, label, html) {
|
|
2024
|
+
if (!button) return;
|
|
2025
|
+
button.innerHTML = loading ? '<span class="spinner" aria-hidden="true"></span><span>' + escapeHtml(label) + '</span>' : (html ? label : escapeHtml(label));
|
|
2026
|
+
button.disabled = !!loading;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1278
2029
|
function renderSelectionSummary() {
|
|
1279
2030
|
const label = state.references.length
|
|
1280
2031
|
? state.references.length + ' referenced element' + (state.references.length === 1 ? '' : 's') + ' selected.'
|
|
@@ -1298,27 +2049,39 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1298
2049
|
els.inspectCards.innerHTML = '<div class="inspect-empty">' + escapeHtml(message) + '</div>';
|
|
1299
2050
|
}
|
|
1300
2051
|
|
|
2052
|
+
function renderInspectLoading(message) {
|
|
2053
|
+
els.inspectCards.innerHTML = '<div class="inspect-loading"><span class="loading-row"><span class="spinner" aria-hidden="true"></span><b>' + escapeHtml(message) + '</b></span><br>Preparing concise Purpose and Source context.</div>'
|
|
2054
|
+
+ '<div class="skeleton-card"><div class="skeleton-line short"></div><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div>'
|
|
2055
|
+
+ '<div class="skeleton-card"><div class="skeleton-line short"></div><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div>';
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
function getInspectComment() {
|
|
2059
|
+
syncReferencesFromComment(false, els.inspectComment);
|
|
2060
|
+
return getCommentText(els.inspectComment).trim().slice(0, 2000);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1301
2063
|
async function inspectCurrentSelection() {
|
|
1302
2064
|
if (!state.references.length || state.inspecting) return;
|
|
1303
2065
|
const snapshot = collectReferenceSnapshot();
|
|
2066
|
+
const comment = getInspectComment();
|
|
1304
2067
|
state.inspecting = true;
|
|
1305
2068
|
updateSendState();
|
|
1306
2069
|
setMode('inspect');
|
|
1307
2070
|
els.inspectStale.innerHTML = '';
|
|
1308
2071
|
state.inspectFallback = null;
|
|
1309
|
-
|
|
2072
|
+
renderInspectLoading('Reading selection...');
|
|
1310
2073
|
try {
|
|
1311
2074
|
const res = await fetch('/api/inspect?token=' + encodeURIComponent(token), {
|
|
1312
2075
|
method: 'POST',
|
|
1313
2076
|
headers: { 'content-type': 'application/json' },
|
|
1314
|
-
body: JSON.stringify({ snapshot, deckVersion: state.deckVersion, language: state.inspectLanguage }),
|
|
2077
|
+
body: JSON.stringify({ snapshot, deckVersion: state.deckVersion, language: state.inspectLanguage, comment }),
|
|
1315
2078
|
});
|
|
1316
2079
|
const body = await res.json().catch(() => ({}));
|
|
1317
2080
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Inspection failed');
|
|
1318
2081
|
state.deckVersion = body.deckVersion || state.deckVersion;
|
|
1319
2082
|
state.activeInspectRequestId = body.requestId;
|
|
1320
2083
|
state.inspectFallback = body.preprocess || null;
|
|
1321
|
-
|
|
2084
|
+
renderInspectLoading('Waiting for Purpose and Source...');
|
|
1322
2085
|
await pollInspectResult(body.requestId);
|
|
1323
2086
|
} catch (error) {
|
|
1324
2087
|
if (state.inspectFallback) {
|
|
@@ -1377,8 +2140,6 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1377
2140
|
else els.inspectStale.innerHTML = '';
|
|
1378
2141
|
els.inspectCards.innerHTML = [
|
|
1379
2142
|
'<div class="status">' + escapeHtml(phase || 'Inspection') + '</div>',
|
|
1380
|
-
result.cards.reading ? renderInspectCard('Narrative Reading', result.cards.reading.status, result.cards.reading.rationale, renderReading(result.cards.reading)) : '',
|
|
1381
|
-
result.cards.exploratory ? renderInspectCard('Exploratory Reading', result.cards.exploratory.status, result.cards.exploratory.rationale, renderExploratory(result.cards.exploratory)) : '',
|
|
1382
2143
|
renderInspectCard('Purpose', result.cards.purpose.status, result.cards.purpose.rationale, renderPurpose(result.cards.purpose)),
|
|
1383
2144
|
renderInspectCard('Source', result.cards.source.status, result.cards.source.rationale, renderSource(result.cards.source)),
|
|
1384
2145
|
].join('');
|
|
@@ -1456,6 +2217,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1456
2217
|
}
|
|
1457
2218
|
|
|
1458
2219
|
function insertReferenceChip(reference) {
|
|
2220
|
+
const editor = activeCommentEditor();
|
|
1459
2221
|
const chip = document.createElement('span');
|
|
1460
2222
|
chip.className = 'ref-chip';
|
|
1461
2223
|
chip.contentEditable = 'false';
|
|
@@ -1473,16 +2235,60 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1473
2235
|
range.collapse(true);
|
|
1474
2236
|
applyCommentRange(range);
|
|
1475
2237
|
} else {
|
|
1476
|
-
if (
|
|
1477
|
-
|
|
1478
|
-
|
|
2238
|
+
if (editor.textContent && !/\\s$/.test(editor.textContent)) editor.appendChild(document.createTextNode(' '));
|
|
2239
|
+
editor.appendChild(chip);
|
|
2240
|
+
editor.appendChild(trailingSpace);
|
|
1479
2241
|
placeCaretAfter(trailingSpace);
|
|
1480
2242
|
}
|
|
2243
|
+
editor.focus();
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function insertAssetChip(asset) {
|
|
2247
|
+
const chip = document.createElement('span');
|
|
2248
|
+
chip.className = 'ref-chip asset-ref-chip';
|
|
2249
|
+
chip.contentEditable = 'false';
|
|
2250
|
+
chip.dataset.assetId = asset.id || '';
|
|
2251
|
+
chip.style.setProperty('--ref-bg', '#dcfce7');
|
|
2252
|
+
chip.style.setProperty('--ref-border', '#86efac');
|
|
2253
|
+
chip.style.setProperty('--ref-text', '#166534');
|
|
2254
|
+
chip.textContent = '@Asset ' + (asset.id || asset.deckPath || asset.path || 'image');
|
|
2255
|
+
const range = getCommentInsertRange();
|
|
2256
|
+
if (range) {
|
|
2257
|
+
range.insertNode(chip);
|
|
2258
|
+
range.setStartAfter(chip);
|
|
2259
|
+
range.collapse(true);
|
|
2260
|
+
applyCommentRange(range);
|
|
2261
|
+
} else {
|
|
2262
|
+
els.comment.appendChild(chip);
|
|
2263
|
+
placeCaretAfter(chip);
|
|
2264
|
+
}
|
|
1481
2265
|
els.comment.focus();
|
|
1482
2266
|
}
|
|
1483
2267
|
|
|
2268
|
+
function insertPlainText(text) {
|
|
2269
|
+
const node = document.createTextNode(text);
|
|
2270
|
+
const range = getCommentInsertRange();
|
|
2271
|
+
if (range) {
|
|
2272
|
+
range.insertNode(node);
|
|
2273
|
+
range.setStartAfter(node);
|
|
2274
|
+
range.collapse(true);
|
|
2275
|
+
applyCommentRange(range);
|
|
2276
|
+
} else {
|
|
2277
|
+
els.comment.appendChild(node);
|
|
2278
|
+
placeCaretAfter(node);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
function removeAssetChip() {
|
|
2283
|
+
els.comment.querySelectorAll('.asset-ref-chip').forEach((chip) => {
|
|
2284
|
+
const next = chip.nextSibling;
|
|
2285
|
+
chip.remove();
|
|
2286
|
+
if (next && next.nodeType === Node.TEXT_NODE && next.textContent === ' ') next.remove();
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
|
|
1484
2290
|
function removeReferenceChip(id) {
|
|
1485
|
-
const chip =
|
|
2291
|
+
const chip = activeCommentEditor().querySelector('.ref-chip[data-ref-id="' + cssEscape(id) + '"]');
|
|
1486
2292
|
if (!chip) return;
|
|
1487
2293
|
const next = chip.nextSibling;
|
|
1488
2294
|
chip.remove();
|
|
@@ -1491,13 +2297,18 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1491
2297
|
|
|
1492
2298
|
function clearReferences(removeChips) {
|
|
1493
2299
|
state.references = [];
|
|
1494
|
-
if (removeChips)
|
|
2300
|
+
if (removeChips) {
|
|
2301
|
+
activeCommentEditor().querySelectorAll('.ref-chip[data-ref-id]').forEach((chip) => chip.remove());
|
|
2302
|
+
state.selectedAsset = null;
|
|
2303
|
+
removeAssetChip();
|
|
2304
|
+
}
|
|
1495
2305
|
renderSelectionSummary();
|
|
1496
|
-
resetInspectCards('Select
|
|
2306
|
+
resetInspectCards('Select a deck element to create an @ref, optionally ask a question, then Inspect. This does not edit the deck.');
|
|
1497
2307
|
}
|
|
1498
2308
|
|
|
1499
|
-
function getCommentText() {
|
|
1500
|
-
|
|
2309
|
+
function getCommentText(editor) {
|
|
2310
|
+
const source = editor || els.comment;
|
|
2311
|
+
return (source.innerText || source.textContent || '').replace(/\\u00a0/g, ' ');
|
|
1501
2312
|
}
|
|
1502
2313
|
|
|
1503
2314
|
function placeCaretAfter(node) {
|
|
@@ -1510,18 +2321,20 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1510
2321
|
function saveCommentRange() {
|
|
1511
2322
|
const selection = window.getSelection();
|
|
1512
2323
|
if (!selection || selection.rangeCount === 0) return;
|
|
1513
|
-
|
|
2324
|
+
const editor = activeCommentEditor();
|
|
2325
|
+
if (!editor || !editor.contains(selection.anchorNode)) return;
|
|
1514
2326
|
state.commentRange = selection.getRangeAt(0).cloneRange();
|
|
1515
2327
|
}
|
|
1516
2328
|
|
|
1517
2329
|
function getCommentInsertRange() {
|
|
2330
|
+
const editor = activeCommentEditor();
|
|
1518
2331
|
const selection = window.getSelection();
|
|
1519
|
-
if (selection && selection.rangeCount > 0 &&
|
|
2332
|
+
if (selection && selection.rangeCount > 0 && editor.contains(selection.anchorNode)) {
|
|
1520
2333
|
const range = selection.getRangeAt(0).cloneRange();
|
|
1521
2334
|
range.deleteContents();
|
|
1522
2335
|
return range;
|
|
1523
2336
|
}
|
|
1524
|
-
if (state.commentRange &&
|
|
2337
|
+
if (state.commentRange && editor.contains(state.commentRange.commonAncestorContainer)) {
|
|
1525
2338
|
const range = state.commentRange.cloneRange();
|
|
1526
2339
|
range.deleteContents();
|
|
1527
2340
|
return range;
|