dist-mcp / mcp / resultMaterializer.js
dist-mcp / mcp / resultMaterializer.js
import fs from "node:fs/promises";
import path from "node:path";
import { escapeHtml, renderReportShell } from "./core-bundle.mjs";
export { snapshotImageIndices, materializeNewImages, extractSummary, extractSummaryText } from "./core-bundle.mjs";
/** crop/mask are pure pixel operations (see core/tools.ts) — their summary carries crop coordinates, not a Draw Things render. */
function isCropLikeSummary(context) {
return context.tool === "crop" || context.tool === "mask";
}
/** Same compact "<b>Label:</b> value • ..." single-line style as find-images-mcp-bridge's buildMetadataCell(). */
function metadataCell(context, result) {
const parts = [`<span class="rank">${escapeHtml(result.notation)}</span>`];
const add = (label, value) => {
if (value === undefined || value === null || value === "")
return;
parts.push(`<strong>${escapeHtml(label)}:</strong> ${escapeHtml(String(value))}`);
};
const summary = context.summary ?? {};
if (isCropLikeSummary(context)) {
// crop/mask summary shape: { originalWidth, originalHeight, crop: { left, right, top, bottom: { pct, px } } } (see core/tools.ts).
if (typeof summary.originalWidth === "number" && typeof summary.originalHeight === "number")
add("Size", `${summary.originalWidth}\u00d7${summary.originalHeight}`);
const crop = summary.crop;
if (crop)
add("Crop L/R/T/B", `${crop.left?.pct ?? 0}% / ${crop.right?.pct ?? 0}% / ${crop.top?.pct ?? 0}% / ${crop.bottom?.pct ?? 0}%`);
add("Source", context.canvas);
return parts.join(" • ");
}
// zoom-in/inpaint/outpaint delegate to handleGenerateImage internally, so their summary
// has the same shape as generate_image/upscale's (see core/tools.ts).
add("Prompt", context.prompt);
const width = summary.width;
const height = summary.height;
if (typeof width === "number" && typeof height === "number")
add("Size", `${width}\u00d7${height}`);
add("Model", summary.model_used);
add("Mode", summary.mode_effective);
add("Quality", summary.quality);
add("Steps", summary.steps);
add("Format", summary.image_format);
add("Source", summary.canvas_source ?? context.canvas);
if (typeof summary.inference_time_ms === "number")
add("Render time", `${(summary.inference_time_ms / 1000).toFixed(1)}s`);
add("Origin", "Draw Things");
return parts.join(" • ");
}
/** Writes `<requestId>.html` into the scratchpad and returns its absolute path. */
export async function writeHtmlReport(scratchpadPath, requestId, context, results) {
const rows = results
.map((result) => {
const previewSrc = result.previewFilename ?? result.filename;
return `<tr><td><img src="${escapeHtml(previewSrc)}" alt="Preview ${escapeHtml(result.notation)}"></td><td>${metadataCell(context, result)}</td></tr>`;
})
.join("\n");
const html = renderReportShell(`${context.tool} ${requestId}`, "<tr><th>Preview</th><th>Details</th></tr>", rows);
const reportPath = path.join(scratchpadPath, `${requestId}.html`);
await fs.writeFile(reportPath, html, "utf8");
return reportPath;
}
import fs from "node:fs/promises";
import path from "node:path";
import { escapeHtml, renderReportShell } from "./core-bundle.mjs";
export { snapshotImageIndices, materializeNewImages, extractSummary, extractSummaryText } from "./core-bundle.mjs";
/** crop/mask are pure pixel operations (see core/tools.ts) — their summary carries crop coordinates, not a Draw Things render. */
function isCropLikeSummary(context) {
return context.tool === "crop" || context.tool === "mask";
}
/** Same compact "<b>Label:</b> value • ..." single-line style as find-images-mcp-bridge's buildMetadataCell(). */
function metadataCell(context, result) {
const parts = [`<span class="rank">${escapeHtml(result.notation)}</span>`];
const add = (label, value) => {
if (value === undefined || value === null || value === "")
return;
parts.push(`<strong>${escapeHtml(label)}:</strong> ${escapeHtml(String(value))}`);
};
const summary = context.summary ?? {};
if (isCropLikeSummary(context)) {
// crop/mask summary shape: { originalWidth, originalHeight, crop: { left, right, top, bottom: { pct, px } } } (see core/tools.ts).
if (typeof summary.originalWidth === "number" && typeof summary.originalHeight === "number")
add("Size", `${summary.originalWidth}\u00d7${summary.originalHeight}`);
const crop = summary.crop;
if (crop)
add("Crop L/R/T/B", `${crop.left?.pct ?? 0}% / ${crop.right?.pct ?? 0}% / ${crop.top?.pct ?? 0}% / ${crop.bottom?.pct ?? 0}%`);
add("Source", context.canvas);
return parts.join(" • ");
}
// zoom-in/inpaint/outpaint delegate to handleGenerateImage internally, so their summary
// has the same shape as generate_image/upscale's (see core/tools.ts).
add("Prompt", context.prompt);
const width = summary.width;
const height = summary.height;
if (typeof width === "number" && typeof height === "number")
add("Size", `${width}\u00d7${height}`);
add("Model", summary.model_used);
add("Mode", summary.mode_effective);
add("Quality", summary.quality);
add("Steps", summary.steps);
add("Format", summary.image_format);
add("Source", summary.canvas_source ?? context.canvas);
if (typeof summary.inference_time_ms === "number")
add("Render time", `${(summary.inference_time_ms / 1000).toFixed(1)}s`);
add("Origin", "Draw Things");
return parts.join(" • ");
}
/** Writes `<requestId>.html` into the scratchpad and returns its absolute path. */
export async function writeHtmlReport(scratchpadPath, requestId, context, results) {
const rows = results
.map((result) => {
const previewSrc = result.previewFilename ?? result.filename;
return `<tr><td><img src="${escapeHtml(previewSrc)}" alt="Preview ${escapeHtml(result.notation)}"></td><td>${metadataCell(context, result)}</td></tr>`;
})
.join("\n");
const html = renderReportShell(`${context.tool} ${requestId}`, "<tr><th>Preview</th><th>Details</th></tr>", rows);
const reportPath = path.join(scratchpadPath, `${requestId}.html`);
await fs.writeFile(reportPath, html, "utf8");
return reportPath;
}