feat(inspect): local prompts resolved like the server's
A file chosen on the Inspect page now shows the prompts and resolution the server's inspector gives the same records: buildLocalPayload takes its roles from the local resolver instead of the interim open roles, so the legacy fill applies exactly where it does for Images. The resolver reads the server's own node table. The page fetches web/js/inspection-semantics.json once through the API client as it mounts, before any selection; the static route serves that one JSON file by exact path (every other JSON stays forbidden). A table that cannot be fetched answers "The inspector's node table is unavailable." as the server does, and a later selection asks again. The Copy settings summary deviation is numbered D117.
This commit is contained in:
@@ -27,13 +27,13 @@ const REVEAL_BUSY = 'The folder is already opening.';
|
||||
const REVEAL_FAILED = 'The folder could not be opened. Try again.';
|
||||
const FACTS = [['seed', 'Seed'], ['steps', 'Steps'], ['cfg', 'CFG'], ['sampler', 'Sampler'], ['scheduler', 'Scheduler'], ['model', 'Model']];
|
||||
|
||||
// D-NEW-1 (LI-4): the legacy metadata page's COPY ALL (web/metadata.html copyAllData), its lines and placeholders.
|
||||
// D117 (LI-4): the legacy metadata page's COPY ALL (web/metadata.html copyAllData), its lines and placeholders.
|
||||
const SUMMARY_LINES = [['Checkpoint', 'model'], ['Positive Prompt', 'positive'], ['Negative Prompt', 'negative'], ['Steps', 'steps'],
|
||||
['CFG Scale', 'cfg'], ['Sampler', 'sampler'], ['Seed', 'seed']];
|
||||
const SUMMARY_MISSING = {positive:'No prompt found', negative:'No negative prompt found'};
|
||||
|
||||
/**
|
||||
* The one-click settings summary (D-NEW-1): the legacy COPY ALL text for what this inspector shows. A confirmed prompt
|
||||
* The one-click settings summary (D117): the legacy COPY ALL text for what this inspector shows. A confirmed prompt
|
||||
* and a known fact are copied verbatim; anything else, an explicitly empty prompt included, takes the legacy page's
|
||||
* placeholder, as its parse does (`hasValue` treats '' as absent).
|
||||
*/
|
||||
@@ -286,7 +286,7 @@ export function mountFileInspector(container, {api, sender, parentScope, variant
|
||||
if (!full && footer.childNodes.length) container.append(footer);
|
||||
}
|
||||
/**
|
||||
* D-NEW-1 (LI-4): "Copy settings summary" after the six facts, the legacy COPY ALL. It copies as the concept's prompt
|
||||
* D117 (LI-4): "Copy settings summary" after the six facts, the legacy COPY ALL. It copies as the concept's prompt
|
||||
* copy does: the shared toast, or the select-to-copy dialog where the clipboard is unavailable or refused.
|
||||
*/
|
||||
function summaryButton(record) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { pageHeading } from './heading.mjs';
|
||||
import { createWorkspaceIcon } from './icons.mjs';
|
||||
import { createLocalSession, LOCAL_SESSION_MESSAGES } from './local-session.mjs';
|
||||
import { localFileAdapter } from './local-inspection.mjs';
|
||||
import { loadInspectionSemantics } from './local-semantics.mjs';
|
||||
import { createMediaViewer } from './media-viewer.mjs';
|
||||
import { showWorkspaceToast, TOAST_MS } from './toast.mjs';
|
||||
|
||||
@@ -84,6 +85,8 @@ export function mountInspect(main, { parentScope, api, sender }) {
|
||||
if (parentScope.signal.aborted) { scope.dispose(); return { dispose }; }
|
||||
parentScope.signal.addEventListener('abort', dispose, { once: true });
|
||||
scope.cleanup(() => parentScope.signal.removeEventListener('abort', dispose));
|
||||
// The node table the local roles resolve with, read before any file is chosen (one request per page).
|
||||
loadInspectionSemantics();
|
||||
|
||||
// The concept DOM: section.surface > h2, p; div.file-drop.spaced-20 > icon, label, input, span.small; #local-preview.
|
||||
const surface = element('section', 'surface');
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
import { readVideoTags, selectVideoCarrier } from './local-video.mjs';
|
||||
import { projectMetadata, fitInspectionPayload, MAX_RESPONSE_BYTES } from './local-projection.mjs';
|
||||
import { projectInspectionPayload } from './inspection-state.mjs';
|
||||
import { resolveLocalEvidence, unreadableEvidence } from './local-resolver.mjs';
|
||||
import { loadInspectionSemantics } from './local-semantics.mjs';
|
||||
|
||||
export const LOCAL_OBSERVATION = 'selected-local-file';
|
||||
export const LOCAL_SOURCE_LABEL = 'Selected local file';
|
||||
// Interim roles until PR L3 ports the inspection resolver: every role is open, and
|
||||
// the legacy fill in projectInspectionPayload shows what the legacy pages show.
|
||||
const LOCAL_RESOLUTION_REASON = 'Resolved in this browser from the workflow.';
|
||||
const LOCAL_LIBRARY = Object.freeze({ status: 'unavailable', scope: 'saved-library', tags: [], category_scope: null, tags_scope: null });
|
||||
const LOCAL_SCOPE = 'local';
|
||||
const PREFIX_BYTES = 32;
|
||||
@@ -88,10 +87,16 @@ export function toInspectionRead(classification, result) {
|
||||
|
||||
const dimension = value => Number.isSafeInteger(value) && value > 0 ? value : null;
|
||||
|
||||
/** The `inspection_version: 1` wire for one local read, before the response budget. */
|
||||
export function buildLocalPayload({ read, file, classification }) {
|
||||
/**
|
||||
* The `inspection_version: 1` wire for one local read, before the response budget. Its roles are the server
|
||||
* inspector's for the same records (local-resolver.mjs with `semantics`, the compiled node table; null answers
|
||||
* "unavailable" as the server does), so the legacy fill in projectInspectionPayload applies exactly where it does
|
||||
* for Images.
|
||||
*/
|
||||
export function buildLocalPayload({ read, file, classification, semantics = null }) {
|
||||
const classified = classificationOf(classification);
|
||||
const inspection = toInspectionRead(classified, read);
|
||||
const evidence = inspection.read === null ? unreadableEvidence() : resolveLocalEvidence(inspection.read.records, semantics);
|
||||
const filename = sanitizeLocalFilename(file?.name);
|
||||
const size = Number.isSafeInteger(file?.size) && file.size >= 0 ? file.size : null;
|
||||
const modified = typeof file?.lastModified === 'number' && Number.isFinite(file.lastModified) && file.lastModified >= 0 ? file.lastModified / 1000 : null;
|
||||
@@ -101,8 +106,8 @@ export function buildLocalPayload({ read, file, classification }) {
|
||||
observation: LOCAL_OBSERVATION, source_label: LOCAL_SOURCE_LABEL, media_type: classified.media, extension: localExtension(filename),
|
||||
format: inspection.format, size, width: dimension(inspection.width), height: dimension(inspection.height), modified_time: modified,
|
||||
},
|
||||
prompts: { positive: { status: 'unresolved' }, negative: { status: 'unresolved' } },
|
||||
resolution: { state: 'unresolved', sampler_id: null, reason: LOCAL_RESOLUTION_REASON, reason_code: null, node: null },
|
||||
prompts: evidence.prompts,
|
||||
resolution: evidence.resolution,
|
||||
metadata: projectMetadata(inspection.read),
|
||||
library: { ...LOCAL_LIBRARY, tags: [] },
|
||||
};
|
||||
@@ -176,7 +181,10 @@ export const localFileAdapter = Object.freeze({
|
||||
// MetadataReadError, an unreadable record. Abort and programming errors still reject.
|
||||
if (!(error instanceof DOMException) || error.name === 'AbortError' || signal?.aborted) throw error;
|
||||
}
|
||||
return fitLocalPayload(buildLocalPayload({ read: result, file: { name: secret.filename, size: secret.file.size, lastModified: secret.file.lastModified }, classification: secret.classification }), MAX_RESPONSE_BYTES, { signal });
|
||||
const semantics = await loadInspectionSemantics();
|
||||
signal?.throwIfAborted();
|
||||
const file = { name: secret.filename, size: secret.file.size, lastModified: secret.file.lastModified };
|
||||
return fitLocalPayload(buildLocalPayload({ read: result, file, classification: secret.classification, semantics }), MAX_RESPONSE_BYTES, { signal });
|
||||
},
|
||||
toRecord(result, handle, extractFacts) {
|
||||
const secret = owned(handle);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// The node table local Inspect resolves with (#249 local Inspect, PR L3 Task 10): web/js/inspection-semantics.json,
|
||||
// the very table the server's inspector reads, fetched once per page through the API client (the proxy-mount rule).
|
||||
// The Inspect page asks for it as it mounts, before any file is chosen; it is the only static file Inspect fetches,
|
||||
// and the request carries nothing about a selection. A table that cannot be fetched or used is null: the roles then
|
||||
// answer as the server does when its table is unusable, and a later selection asks again.
|
||||
import { compileSemantics } from './local-resolver.mjs';
|
||||
|
||||
export const SEMANTICS_PATH = '/prompt_manager/js/inspection-semantics.json';
|
||||
let pending = null;
|
||||
|
||||
async function fetchSemantics() {
|
||||
const client = globalThis.window?.ApiClient;
|
||||
if (typeof client?.resolveUrl !== 'function' || typeof globalThis.fetch !== 'function') return null;
|
||||
const response = await globalThis.fetch(client.resolveUrl(SEMANTICS_PATH), { credentials: 'same-origin' });
|
||||
if (!response.ok) throw new Error(`Inspection node table request failed (${response.status})`);
|
||||
return compileSemantics(await response.json());
|
||||
}
|
||||
|
||||
/** The compiled table (one request per page), or null when it is unavailable. */
|
||||
export function loadInspectionSemantics() {
|
||||
if (pending !== null) return pending;
|
||||
const current = pending = fetchSemantics().catch(() => null).then(table => {
|
||||
if (table === null && pending === current) pending = null;
|
||||
return table;
|
||||
});
|
||||
return current;
|
||||
}
|
||||
@@ -282,7 +282,7 @@ export function openImportDialog({ scope, onInspect, onRescan, onMetadataScan =
|
||||
|
||||
/**
|
||||
* "Copy prompt": the text in a readonly, pre-selected textarea for a manual copy. `title` names another copy's dialog
|
||||
* (D-NEW-1 "Copy settings summary").
|
||||
* (D117 "Copy settings summary").
|
||||
*/
|
||||
export function openCopyFallback(text, { scope, trigger = null, title = 'Copy prompt' } = {}) {
|
||||
requireScope(scope);
|
||||
|
||||
@@ -213,7 +213,7 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
|
||||
where the toolkit's `nowrap` would push them past its edge. */
|
||||
.ca-tools #viewer-details > button { white-space: normal; /* D20 */ }
|
||||
/* LI-4: "Copy settings summary" after the facts wraps the same way in the narrow rail. */
|
||||
.ca-tools #viewer-details > div > button { white-space: normal; /* D-NEW-1 */ }
|
||||
.ca-tools #viewer-details > div > button { white-space: normal; /* D117 */ }
|
||||
/* The Images page narrows to one column inside a narrow container (the Library-embedded gallery). */
|
||||
.images-main { container: images-page / inline-size; }
|
||||
@container images-page (inline-size < 600px) {
|
||||
|
||||
Reference in New Issue
Block a user