import type { PreviewAnnotationPayload } from "@t3tools/contracts";import { buildElementContextBlock, normalizeElementContextSelection } from "./elementContext"; const TRAILING_PREVIEW_ANNOTATION_BLOCK_PATTERN = /\n*<preview_annotation>\n((?:(?!<preview_annotation>)[\s\S])*)\n<\/preview_annotation>\s*$/; export interface ParsedPreviewAnnotation { id: string; title: string; comment: string; targetSummary: string; styleChanges: string[]; hasScreenshot: boolean;} export interface ExtractedPreviewAnnotation { promptText: string; annotation: ParsedPreviewAnnotation | null;} export function buildPreviewAnnotationPrompt(annotation: PreviewAnnotationPayload): string { const lines = ["Preview annotation:"]; lines.push(`Id: ${annotation.id}`); const title = annotation.pageTitle?.trim() || annotation.pageUrl.trim() || "Preview"; lines.push(`Page: ${title}`); if (annotation.comment.trim()) lines.push(`Comment: ${annotation.comment.trim()}`); const targets: string[] = []; if (annotation.elements.length > 0) { targets.push( `${annotation.elements.length} selected element${annotation.elements.length === 1 ? "" : "s"}`, ); } if (annotation.regions.length > 0) { targets.push( `${annotation.regions.length} marked region${annotation.regions.length === 1 ? "" : "s"}`, ); } if (annotation.strokes.length > 0) { targets.push( `${annotation.strokes.length} drawing${annotation.strokes.length === 1 ? "" : "s"}`, ); } if (targets.length > 0) lines.push(`Targets: ${targets.join(", ")}.`); if (annotation.styleChanges.length > 0) { lines.push("Requested visual changes:"); for (const change of annotation.styleChanges) { lines.push(`- ${change.property}: ${change.previousValue || "(unset)"} → ${change.value}`); } } if (annotation.screenshot) { lines.push("The attached screenshot is the annotated preview crop."); } const elementContexts = annotation.elements .map((target) => normalizeElementContextSelection(target.element)) .filter((context) => context !== null); const elementBlock = buildElementContextBlock(elementContexts); if (elementBlock) lines.push(elementBlock); return ["<preview_annotation>", ...lines, "</preview_annotation>"].join("\n");} export function appendPreviewAnnotationPrompt( prompt: string, annotation: PreviewAnnotationPayload,): string { const annotationText = buildPreviewAnnotationPrompt(annotation); const trimmed = prompt.trim(); return trimmed ? `${trimmed}\n\n${annotationText}` : annotationText;} export function extractTrailingPreviewAnnotation(prompt: string): ExtractedPreviewAnnotation { const match = TRAILING_PREVIEW_ANNOTATION_BLOCK_PATTERN.exec(prompt); if (!match) return { promptText: prompt, annotation: null }; const body = match[1] ?? ""; const lines = body.split("\n"); const pageLine = lines.find((line) => line.startsWith("Page: ")); const idLine = lines.find((line) => line.startsWith("Id: ")); const commentLine = lines.find((line) => line.startsWith("Comment: ")); const targetsLine = lines.find((line) => line.startsWith("Targets: ")); const styleHeadingIndex = lines.indexOf("Requested visual changes:"); const linesAfterStyleHeading = lines.slice(styleHeadingIndex + 1); const elementContextIndex = linesAfterStyleHeading.indexOf("<element_context>"); const styleChanges = styleHeadingIndex < 0 ? [] : linesAfterStyleHeading .slice(0, elementContextIndex < 0 ? undefined : elementContextIndex) .filter((line) => line.startsWith("- ")) .map((line) => line.slice(2)); return { promptText: prompt.slice(0, match.index).replace(/\n+$/, ""), annotation: { id: idLine?.slice("Id: ".length).trim() || `${match.index}`, title: pageLine?.slice("Page: ".length).trim() || "Preview annotation", comment: commentLine?.slice("Comment: ".length).trim() || "", targetSummary: targetsLine?.slice("Targets: ".length).trim() || "", styleChanges, hasScreenshot: body.includes("The attached screenshot is the annotated preview crop."), }, };} async function previewAnnotationScreenshotFile( annotation: PreviewAnnotationPayload,): Promise<File | null> { if (!annotation.screenshot) return null; const response = await fetch(annotation.screenshot.dataUrl); const blob = await response.blob(); return new File([blob], `preview-annotation-${annotation.id}.png`, { type: blob.type || "image/png", });} /** Upper bound on turning a picked element's crop into a composer attachment. */const PREVIEW_ANNOTATION_CAPTURE_TIMEOUT_MS = 5_000; export type PreviewAnnotationCapture = /** The crop is ready to attach. */ | { readonly status: "captured"; readonly file: File } /** The pick carried no crop, which is normal for comment-only annotations. */ | { readonly status: "none" } /** The crop stalled or threw. Send the annotation without it. */ | { readonly status: "failed" }; /** * Bounded wrapper around `previewAnnotationScreenshotFile`. The picker holds the * composer while this runs, so it must always settle: a stalled crop resolves as * `failed` instead of leaving the caller waiting. */export async function capturePreviewAnnotationScreenshot( annotation: PreviewAnnotationPayload, timeoutMs: number = PREVIEW_ANNOTATION_CAPTURE_TIMEOUT_MS,): Promise<PreviewAnnotationCapture> { if (!annotation.screenshot) return { status: "none" }; let timer: ReturnType<typeof setTimeout> | undefined; try { const file = await Promise.race([ previewAnnotationScreenshotFile(annotation), new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), timeoutMs); }), ]); return file ? { status: "captured", file } : { status: "failed" }; } catch { return { status: "failed" }; } finally { clearTimeout(timer); }}
import type { PreviewAnnotationPayload } from "@t3tools/contracts";
import { buildElementContextBlock, normalizeElementContextSelection } from "./elementContext";
const TRAILING_PREVIEW_ANNOTATION_BLOCK_PATTERN =
/\n*<preview_annotation>\n((?:(?!<preview_annotation>)[\s\S])*)\n<\/preview_annotation>\s*$/;
export interface ParsedPreviewAnnotation {
id: string;
title: string;
comment: string;
targetSummary: string;
styleChanges: string[];
hasScreenshot: boolean;
}
export interface ExtractedPreviewAnnotation {
promptText: string;
annotation: ParsedPreviewAnnotation | null;
}
export function buildPreviewAnnotationPrompt(annotation: PreviewAnnotationPayload): string {
const lines = ["Preview annotation:"];
lines.push(`Id: ${annotation.id}`);
const title = annotation.pageTitle?.trim() || annotation.pageUrl.trim() || "Preview";
lines.push(`Page: ${title}`);
if (annotation.comment.trim()) lines.push(`Comment: ${annotation.comment.trim()}`);
const targets: string[] = [];
if (annotation.elements.length > 0) {
targets.push(
`${annotation.elements.length} selected element${annotation.elements.length === 1 ? "" : "s"}`,
);
}
if (annotation.regions.length > 0) {
targets.push(
`${annotation.regions.length} marked region${annotation.regions.length === 1 ? "" : "s"}`,
);
}
if (annotation.strokes.length > 0) {
targets.push(
`${annotation.strokes.length} drawing${annotation.strokes.length === 1 ? "" : "s"}`,
);
}
if (targets.length > 0) lines.push(`Targets: ${targets.join(", ")}.`);
if (annotation.styleChanges.length > 0) {
lines.push("Requested visual changes:");
for (const change of annotation.styleChanges) {
lines.push(`- ${change.property}: ${change.previousValue || "(unset)"} → ${change.value}`);
}
}
if (annotation.screenshot) {
lines.push("The attached screenshot is the annotated preview crop.");
}
const elementContexts = annotation.elements
.map((target) => normalizeElementContextSelection(target.element))
.filter((context) => context !== null);
const elementBlock = buildElementContextBlock(elementContexts);
if (elementBlock) lines.push(elementBlock);
return ["<preview_annotation>", ...lines, "</preview_annotation>"].join("\n");
}
export function appendPreviewAnnotationPrompt(
prompt: string,
annotation: PreviewAnnotationPayload,
): string {
const annotationText = buildPreviewAnnotationPrompt(annotation);
const trimmed = prompt.trim();
return trimmed ? `${trimmed}\n\n${annotationText}` : annotationText;
}
export function extractTrailingPreviewAnnotation(prompt: string): ExtractedPreviewAnnotation {
const match = TRAILING_PREVIEW_ANNOTATION_BLOCK_PATTERN.exec(prompt);
if (!match) return { promptText: prompt, annotation: null };
const body = match[1] ?? "";
const lines = body.split("\n");
const pageLine = lines.find((line) => line.startsWith("Page: "));
const idLine = lines.find((line) => line.startsWith("Id: "));
const commentLine = lines.find((line) => line.startsWith("Comment: "));
const targetsLine = lines.find((line) => line.startsWith("Targets: "));
const styleHeadingIndex = lines.indexOf("Requested visual changes:");
const linesAfterStyleHeading = lines.slice(styleHeadingIndex + 1);
const elementContextIndex = linesAfterStyleHeading.indexOf("<element_context>");
const styleChanges =
styleHeadingIndex < 0
? []
: linesAfterStyleHeading
.slice(0, elementContextIndex < 0 ? undefined : elementContextIndex)
.filter((line) => line.startsWith("- "))
.map((line) => line.slice(2));
return {
promptText: prompt.slice(0, match.index).replace(/\n+$/, ""),
annotation: {
id: idLine?.slice("Id: ".length).trim() || `${match.index}`,
title: pageLine?.slice("Page: ".length).trim() || "Preview annotation",
comment: commentLine?.slice("Comment: ".length).trim() || "",
targetSummary: targetsLine?.slice("Targets: ".length).trim() || "",
styleChanges,
hasScreenshot: body.includes("The attached screenshot is the annotated preview crop."),
},
};
}
async function previewAnnotationScreenshotFile(
annotation: PreviewAnnotationPayload,
): Promise<File | null> {
if (!annotation.screenshot) return null;
const response = await fetch(annotation.screenshot.dataUrl);
const blob = await response.blob();
return new File([blob], `preview-annotation-${annotation.id}.png`, {
type: blob.type || "image/png",
});
}
/** Upper bound on turning a picked element's crop into a composer attachment. */
const PREVIEW_ANNOTATION_CAPTURE_TIMEOUT_MS = 5_000;
export type PreviewAnnotationCapture =
/** The crop is ready to attach. */
| { readonly status: "captured"; readonly file: File }
/** The pick carried no crop, which is normal for comment-only annotations. */
| { readonly status: "none" }
/** The crop stalled or threw. Send the annotation without it. */
| { readonly status: "failed" };
/**
* Bounded wrapper around `previewAnnotationScreenshotFile`. The picker holds the
* composer while this runs, so it must always settle: a stalled crop resolves as
* `failed` instead of leaving the caller waiting.
*/
export async function capturePreviewAnnotationScreenshot(
annotation: PreviewAnnotationPayload,
timeoutMs: number = PREVIEW_ANNOTATION_CAPTURE_TIMEOUT_MS,
): Promise<PreviewAnnotationCapture> {
if (!annotation.screenshot) return { status: "none" };
let timer: ReturnType<typeof setTimeout> | undefined;
try {
const file = await Promise.race([
previewAnnotationScreenshotFile(annotation),
new Promise<null>((resolve) => {
timer = setTimeout(() => resolve(null), timeoutMs);
}),
]);
return file ? { status: "captured", file } : { status: "failed" };
} catch {
return { status: "failed" };
} finally {
clearTimeout(timer);
}
}