@cyber-dash-tech/revela 0.17.0 → 0.17.1
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/lib/commands/narrative.ts +13 -4
- package/lib/commands/review.ts +14 -10
- package/lib/media/download.ts +23 -3
- package/lib/media/save.ts +1 -0
- package/lib/media/types.ts +1 -0
- package/lib/narrative-state/display.ts +74 -4
- package/lib/narrative-state/map-html.ts +242 -107
- package/lib/narrative-state/render-plan.ts +125 -9
- package/lib/qa/checks.ts +206 -5
- package/lib/qa/measure.ts +63 -1
- package/lib/refine/server.ts +157 -20
- package/package.json +1 -1
- package/skill/SKILL.md +6 -1
- package/tools/narrative-view.ts +16 -0
package/lib/refine/server.ts
CHANGED
|
@@ -253,35 +253,105 @@ async function handleAssetSave(req: Request, session: EditSession): Promise<Resp
|
|
|
253
253
|
const candidate = normalizeImageCandidate(body?.candidate ?? body)
|
|
254
254
|
if (!candidate) return jsonResponse({ ok: false, error: "Valid image candidate is required" }, 400)
|
|
255
255
|
const purpose = normalizeMediaPurpose(body?.purpose) || candidate.purpose || "illustration"
|
|
256
|
-
const
|
|
257
|
-
|
|
256
|
+
const brief = body?.brief || `Saved from ${candidate.provider} for Review asset placement.`
|
|
257
|
+
const saved = await saveAssetCandidateUrls({
|
|
258
|
+
session,
|
|
259
|
+
candidate,
|
|
258
260
|
id: body?.id || candidate.candidateId,
|
|
259
|
-
type: "image",
|
|
260
261
|
purpose,
|
|
261
|
-
brief
|
|
262
|
-
status: "success",
|
|
263
|
-
sourceUrl: candidate.imageUrl,
|
|
262
|
+
brief,
|
|
264
263
|
alt: body?.alt || candidate.alt || candidate.title,
|
|
265
264
|
notes: body?.notes,
|
|
266
|
-
|
|
267
|
-
sourcePageUrl: candidate.sourcePageUrl,
|
|
268
|
-
license: candidate.license,
|
|
269
|
-
attribution: candidate.attribution,
|
|
270
|
-
width: candidate.width,
|
|
271
|
-
height: candidate.height,
|
|
272
|
-
}, session.workspaceRoot)
|
|
265
|
+
})
|
|
273
266
|
|
|
274
267
|
session.lastActiveAt = Date.now()
|
|
275
268
|
scheduleIdleStop()
|
|
269
|
+
const result = saved.result
|
|
276
270
|
if (!result.ok) return jsonResponse({ ok: false, error: result.error }, 400)
|
|
277
271
|
if (result.status !== "success" || !result.path) {
|
|
278
|
-
return jsonResponse({ ok: false, error:
|
|
272
|
+
return jsonResponse({ ok: false, error: failedAssetSaveMessage(result.status, saved.failures) }, 400)
|
|
279
273
|
}
|
|
280
274
|
const asset = savedAssetForResult(session, result.assetId)
|
|
275
|
+
?? savedAssetFallback(session, {
|
|
276
|
+
id: result.assetId,
|
|
277
|
+
path: result.path,
|
|
278
|
+
sourceUrl: saved.sourceUrl,
|
|
279
|
+
purpose,
|
|
280
|
+
brief,
|
|
281
|
+
candidate,
|
|
282
|
+
})
|
|
281
283
|
if (!asset) return jsonResponse({ ok: false, error: "Saved asset was not found in workspace assets." }, 500)
|
|
282
284
|
return jsonResponse({ ok: true, asset, result })
|
|
283
285
|
}
|
|
284
286
|
|
|
287
|
+
async function saveAssetCandidateUrls(input: {
|
|
288
|
+
session: EditSession
|
|
289
|
+
candidate: ImageCandidate
|
|
290
|
+
id: string
|
|
291
|
+
purpose: MediaPurpose
|
|
292
|
+
brief: string
|
|
293
|
+
alt?: string
|
|
294
|
+
notes?: string
|
|
295
|
+
}): Promise<{
|
|
296
|
+
result: Awaited<ReturnType<typeof saveMediaAsset>>
|
|
297
|
+
sourceUrl?: string
|
|
298
|
+
failures: Array<{ url: string; status: string }>
|
|
299
|
+
}> {
|
|
300
|
+
const urls = uniqueAssetUrls([input.candidate.imageUrl, input.candidate.thumbnailUrl])
|
|
301
|
+
const failures: Array<{ url: string; status: string }> = []
|
|
302
|
+
let lastResult: Awaited<ReturnType<typeof saveMediaAsset>> | undefined
|
|
303
|
+
|
|
304
|
+
for (const sourceUrl of urls) {
|
|
305
|
+
const result = await saveMediaAsset({
|
|
306
|
+
topic: input.session.deck,
|
|
307
|
+
id: input.id,
|
|
308
|
+
type: "image",
|
|
309
|
+
purpose: input.purpose,
|
|
310
|
+
brief: input.brief,
|
|
311
|
+
status: "success",
|
|
312
|
+
sourceUrl,
|
|
313
|
+
alt: input.alt,
|
|
314
|
+
notes: input.notes,
|
|
315
|
+
provider: input.candidate.provider,
|
|
316
|
+
sourcePageUrl: input.candidate.sourcePageUrl,
|
|
317
|
+
license: input.candidate.license,
|
|
318
|
+
attribution: input.candidate.attribution,
|
|
319
|
+
width: input.candidate.width,
|
|
320
|
+
height: input.candidate.height,
|
|
321
|
+
}, input.session.workspaceRoot)
|
|
322
|
+
lastResult = result
|
|
323
|
+
if (result.ok && result.status === "success" && result.path) return { result, sourceUrl, failures }
|
|
324
|
+
failures.push({ url: sourceUrl, status: result.ok ? result.failureReason ?? result.status : result.error })
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
result: lastResult ?? { ok: false, error: "No downloadable image URL was provided" },
|
|
329
|
+
failures,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function uniqueAssetUrls(values: Array<string | undefined>): string[] {
|
|
334
|
+
const seen = new Set<string>()
|
|
335
|
+
return values.flatMap((value) => {
|
|
336
|
+
const url = value?.trim()
|
|
337
|
+
if (!url || seen.has(url)) return []
|
|
338
|
+
seen.add(url)
|
|
339
|
+
return [url]
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function failedAssetSaveMessage(status: string, failures: Array<{ url: string; status: string }>): string {
|
|
344
|
+
if (!failures.length) return `Failed to save asset: ${status}`
|
|
345
|
+
const details = failures
|
|
346
|
+
.map((failure) => `${shortUrl(failure.url)}: ${failure.status}`)
|
|
347
|
+
.join("; ")
|
|
348
|
+
return `Failed to save asset: ${status} (${details})`
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function shortUrl(value: string): string {
|
|
352
|
+
return value.length <= 96 ? value : `${value.slice(0, 93)}...`
|
|
353
|
+
}
|
|
354
|
+
|
|
285
355
|
function handleAssetList(session: EditSession): Response {
|
|
286
356
|
session.lastActiveAt = Date.now()
|
|
287
357
|
scheduleIdleStop()
|
|
@@ -292,6 +362,39 @@ function savedAssetForResult(session: EditSession, assetId: string): (MediaAsset
|
|
|
292
362
|
return listSavedAssets(session).find((asset) => asset.id === assetId) ?? null
|
|
293
363
|
}
|
|
294
364
|
|
|
365
|
+
function savedAssetFallback(
|
|
366
|
+
session: EditSession,
|
|
367
|
+
input: {
|
|
368
|
+
id: string
|
|
369
|
+
path: string | null
|
|
370
|
+
sourceUrl?: string
|
|
371
|
+
purpose: MediaPurpose
|
|
372
|
+
brief: string
|
|
373
|
+
candidate: ImageCandidate
|
|
374
|
+
},
|
|
375
|
+
): (MediaAssetRecord & { previewUrl?: string; deckPath?: string }) | null {
|
|
376
|
+
if (!input.path) return null
|
|
377
|
+
return {
|
|
378
|
+
id: input.id,
|
|
379
|
+
type: "image",
|
|
380
|
+
purpose: input.purpose,
|
|
381
|
+
brief: input.brief,
|
|
382
|
+
status: "success",
|
|
383
|
+
path: input.path,
|
|
384
|
+
sourceUrl: input.sourceUrl ?? input.candidate.imageUrl,
|
|
385
|
+
alt: input.candidate.alt || input.candidate.title,
|
|
386
|
+
provider: input.candidate.provider,
|
|
387
|
+
sourcePageUrl: input.candidate.sourcePageUrl,
|
|
388
|
+
license: input.candidate.license,
|
|
389
|
+
attribution: input.candidate.attribution,
|
|
390
|
+
width: input.candidate.width,
|
|
391
|
+
height: input.candidate.height,
|
|
392
|
+
savedAt: new Date().toISOString(),
|
|
393
|
+
previewUrl: assetUrlForRef(input.path, session, session.workspaceRoot) ?? undefined,
|
|
394
|
+
deckPath: relative(dirname(session.absoluteFile), resolve(session.workspaceRoot, input.path)).replace(/\\/g, "/"),
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
295
398
|
function listSavedAssets(session: EditSession): Array<MediaAssetRecord & { previewUrl?: string; deckPath?: string }> {
|
|
296
399
|
const manifestPath = resolve(session.workspaceRoot, "assets", slugify(session.deck), "media-manifest.json")
|
|
297
400
|
if (!existsSync(manifestPath)) return []
|
|
@@ -891,6 +994,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
891
994
|
.skeleton-line.long { width: 92%; }
|
|
892
995
|
.asset-card.is-saving::after { content: ""; position: absolute; inset: 0; background: rgba(15,23,42,.32); }
|
|
893
996
|
.asset-card.is-saving .asset-save { z-index: 1; }
|
|
997
|
+
.asset-card.is-saved-candidate .asset-thumb { opacity: .72; }
|
|
894
998
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
895
999
|
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
|
896
1000
|
.asset-search { display: grid; grid-template-columns: minmax(0, 1fr) 118px; gap: 8px; }
|
|
@@ -913,6 +1017,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
913
1017
|
.asset-search-title span { color: #756f66; font-size: 12px; line-height: 1.35; }
|
|
914
1018
|
.asset-back { width: auto; padding: 9px 11px; border-radius: 999px; background: #ebe4d8; color: #111827; box-shadow: none; }
|
|
915
1019
|
.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; }
|
|
1020
|
+
.asset-save.saved { background: rgba(77,97,56,.94); color: #fbfaf7; cursor: default; }
|
|
916
1021
|
.asset-empty { grid-column: 1 / -1; margin: 0; color: #756f66; font-size: 12px; line-height: 1.45; }
|
|
917
1022
|
.edit-assets { padding: 10px; border: 1px solid #d8d2c6; border-radius: 16px; background: #f7f3ea; }
|
|
918
1023
|
.edit-assets .panel { gap: 8px; }
|
|
@@ -2000,9 +2105,13 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2000
2105
|
}
|
|
2001
2106
|
state.assetCandidates.forEach((candidate, index) => {
|
|
2002
2107
|
const card = assetCard(candidate, false, index);
|
|
2108
|
+
const savedAsset = savedAssetForCandidate(candidate);
|
|
2003
2109
|
if (state.assetSavingIndex === index) {
|
|
2004
2110
|
card.classList.add('is-saving');
|
|
2005
2111
|
appendAssetSaveButton(card, 'Saving...', 'Saving to workspace', () => {}, true);
|
|
2112
|
+
} else if (savedAsset) {
|
|
2113
|
+
card.classList.add('is-saved-candidate');
|
|
2114
|
+
appendAssetSaveButton(card, '✅ Saved', 'Asset already saved to Local Assets', () => {}, false, 'saved');
|
|
2006
2115
|
} else {
|
|
2007
2116
|
appendAssetSaveButton(card, 'Save', 'Save to workspace', () => saveCandidate(index));
|
|
2008
2117
|
}
|
|
@@ -2025,7 +2134,11 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2025
2134
|
});
|
|
2026
2135
|
const body = await res.json().catch(() => ({}));
|
|
2027
2136
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to save asset');
|
|
2028
|
-
await loadSavedAssets();
|
|
2137
|
+
const listed = await loadSavedAssets();
|
|
2138
|
+
if (body.asset && (!listed || !findSavedAsset(body.asset.id))) {
|
|
2139
|
+
mergeSavedAsset(body.asset);
|
|
2140
|
+
renderSavedAssets();
|
|
2141
|
+
}
|
|
2029
2142
|
const path = body.asset && (body.asset.path || body.asset.deckPath);
|
|
2030
2143
|
setStatus(path ? 'Saved to ' + path + '. Use it from Local Assets.' : 'Asset saved. Use it from Local Assets.');
|
|
2031
2144
|
} catch (error) {
|
|
@@ -2043,12 +2156,36 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2043
2156
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to list assets');
|
|
2044
2157
|
state.savedAssets = Array.isArray(body.assets) ? body.assets : [];
|
|
2045
2158
|
renderSavedAssets();
|
|
2159
|
+
return true;
|
|
2046
2160
|
} catch (error) {
|
|
2047
|
-
|
|
2048
|
-
|
|
2161
|
+
if (!state.savedAssets.length) {
|
|
2162
|
+
const message = '<p class="asset-empty">' + escapeHtml(error && error.message ? error.message : String(error)) + '</p>';
|
|
2163
|
+
els.editSavedAssets.innerHTML = message;
|
|
2164
|
+
}
|
|
2165
|
+
return false;
|
|
2049
2166
|
}
|
|
2050
2167
|
}
|
|
2051
2168
|
|
|
2169
|
+
function mergeSavedAsset(asset) {
|
|
2170
|
+
if (!asset || !asset.id) return;
|
|
2171
|
+
const next = state.savedAssets.filter((existing) => existing.id !== asset.id);
|
|
2172
|
+
next.unshift(asset);
|
|
2173
|
+
state.savedAssets = next;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
function savedAssetForCandidate(candidate) {
|
|
2177
|
+
const id = slugifyAssetId(candidate && candidate.candidateId);
|
|
2178
|
+
if (!id) return null;
|
|
2179
|
+
return state.savedAssets.find((asset) => asset.id === id) || null;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
function slugifyAssetId(value) {
|
|
2183
|
+
return String(value || '')
|
|
2184
|
+
.toLowerCase()
|
|
2185
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
2186
|
+
.replace(/^-+|-+$/g, '');
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2052
2189
|
function renderSavedAssets() {
|
|
2053
2190
|
renderSavedAssetGrid(els.editSavedAssets, 'No local assets yet. Click + to search assets.');
|
|
2054
2191
|
}
|
|
@@ -2108,12 +2245,12 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2108
2245
|
}
|
|
2109
2246
|
}
|
|
2110
2247
|
|
|
2111
|
-
function appendAssetSaveButton(card, text, label, onClick, loading) {
|
|
2248
|
+
function appendAssetSaveButton(card, text, label, onClick, loading, variant) {
|
|
2112
2249
|
const button = document.createElement('button');
|
|
2113
2250
|
button.type = 'button';
|
|
2114
|
-
button.className = 'asset-save';
|
|
2251
|
+
button.className = variant ? 'asset-save ' + variant : 'asset-save';
|
|
2115
2252
|
button.innerHTML = loading ? '<span class="spinner" aria-hidden="true"></span><span>' + escapeHtml(text) + '</span>' : escapeHtml(text);
|
|
2116
|
-
button.disabled = !!loading;
|
|
2253
|
+
button.disabled = !!loading || variant === 'saved';
|
|
2117
2254
|
button.setAttribute('aria-label', label);
|
|
2118
2255
|
button.title = label;
|
|
2119
2256
|
button.addEventListener('click', onClick);
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -95,7 +95,8 @@ Before writing HTML, the confirmed plan must include:
|
|
|
95
95
|
non-structural slides assigned to each chapter.
|
|
96
96
|
- One row per slide with title, purpose, narrative role, content summary, layout,
|
|
97
97
|
components, primary/supporting claim ids, evidence binding ids or source
|
|
98
|
-
summary,
|
|
98
|
+
summary, `content.data.visualIntent`, `visuals[]`, and caveats/unsupported
|
|
99
|
+
scope.
|
|
99
100
|
- A low-fidelity layout sketch for every slide when requested by the handoff
|
|
100
101
|
prompt.
|
|
101
102
|
|
|
@@ -110,6 +111,10 @@ Rules for the slide plan:
|
|
|
110
111
|
"overview of topic".
|
|
111
112
|
- Every content slide must carry a distinct claim, evidence item, comparison,
|
|
112
113
|
risk, or action.
|
|
114
|
+
- Treat `content.data.visualIntent` and `visuals[]` as required render
|
|
115
|
+
instructions, not optional decoration. Do not downgrade a planned metric card,
|
|
116
|
+
evidence table, comparison grid, risk matrix, steps view, chart, or media brief
|
|
117
|
+
into generic bullets unless the user revises and reconfirms the plan.
|
|
113
118
|
- Normal content slides should usually contain 2-4 semantic boxes/cards unless
|
|
114
119
|
intentionally using a focus layout.
|
|
115
120
|
- If a chapter lacks enough substance for its allocated slides, reduce the slide
|
package/tools/narrative-view.ts
CHANGED
|
@@ -22,6 +22,18 @@ export default tool({
|
|
|
22
22
|
claimFlow: tool.schema.string().optional(),
|
|
23
23
|
flowNote: tool.schema.string().optional(),
|
|
24
24
|
selectedClaim: tool.schema.string().optional(),
|
|
25
|
+
selectedEvidence: tool.schema.string().optional(),
|
|
26
|
+
evidenceList: tool.schema.string().optional(),
|
|
27
|
+
gap: tool.schema.string().optional(),
|
|
28
|
+
gaps: tool.schema.string().optional(),
|
|
29
|
+
noEvidence: tool.schema.string().optional(),
|
|
30
|
+
selectEvidencePrompt: tool.schema.string().optional(),
|
|
31
|
+
sourceTrace: tool.schema.string().optional(),
|
|
32
|
+
evidenceSource: tool.schema.string().optional(),
|
|
33
|
+
whyThisSupports: tool.schema.string().optional(),
|
|
34
|
+
linkedGaps: tool.schema.string().optional(),
|
|
35
|
+
selectedGap: tool.schema.string().optional(),
|
|
36
|
+
noLinkedGaps: tool.schema.string().optional(),
|
|
25
37
|
claim: tool.schema.string().optional(),
|
|
26
38
|
claimId: tool.schema.string().optional(),
|
|
27
39
|
status: tool.schema.string().optional(),
|
|
@@ -51,6 +63,10 @@ export default tool({
|
|
|
51
63
|
riskOrGapSummary: tool.schema.string().optional(),
|
|
52
64
|
researchGapsSummary: tool.schema.string().optional().describe("Display-only localized summary of research gaps tied to this claim."),
|
|
53
65
|
})).optional(),
|
|
66
|
+
researchGapCards: tool.schema.array(tool.schema.object({
|
|
67
|
+
gapId: tool.schema.string().describe("Existing canonical research gap id. Must match the deterministic map."),
|
|
68
|
+
displayQuestion: tool.schema.string().optional().describe("Display-only localized gap question. Do not change source facts, ids, targets, or gap meaning."),
|
|
69
|
+
})).optional(),
|
|
54
70
|
relations: tool.schema.array(tool.schema.object({
|
|
55
71
|
fromClaimId: tool.schema.string(),
|
|
56
72
|
toClaimId: tool.schema.string(),
|