/** * `generate_image` — structured image-generation prompt plus the produced * image(s). Generated images travel in `details.images` (kept out of model * context), so the body merges them into the result before thumbnailing. */ import type { ReactNode } from "react"; import { Badge, Badges, InvalidArg, Kv, KvGrid, Note, PathText, ResultImages, ResultText, Row } from "../parts"; import type { ToolRenderer, ToolRenderProps, ToolResultBlock, ToolResultLike } from "../types"; import { detailsRecord, isRecord, normalizeWs, resultImagesOf, str, truncate } from "../util"; /** Result with `details.images` (`{data, mimeType}[]`) appended as image blocks. */ function withDetailImages(result: ToolResultLike | undefined): ToolResultLike | undefined { const details = detailsRecord(result); if (!result || !details || !Array.isArray(details.images)) return result; const extra: ToolResultBlock[] = []; for (const img of details.images) { if (isRecord(img) && typeof img.data === "string" && typeof img.mimeType === "string") { extra.push({ type: "image", data: img.data, mimeType: img.mimeType }); } } if (extra.length === 0) return result; return { content: [...result.content, ...extra], details: result.details, isError: result.isError }; } function Summary({ args }: ToolRenderProps): ReactNode { const subject = str(args.subject); const aspect = str(args.aspect_ratio); const changes = Array.isArray(args.changes) ? args.changes.length : 0; return ( <> {subject ? ( {truncate(normalizeWs(subject), 80)} ) : ( args.subject !== undefined && )}{" "} {aspect && {aspect}} {changes > 0 && edit ×{changes}} ); } const PROMPT_FIELDS = [ ["subject", "subject"], ["action", "action"], ["scene", "scene"], ["composition", "composition"], ["lighting", "lighting"], ["style", "style"], ["text", "text"], ["aspect_ratio", "aspect"], ["image_size", "size"], ] as const; function Body({ args, result }: ToolRenderProps): ReactNode { const changes = Array.isArray(args.changes) ? args.changes : null; const inputs = Array.isArray(args.input) ? args.input : null; const details = detailsRecord(result); const provider = str(details?.provider); const model = str(details?.model); const revised = str(details?.revisedPrompt); const paths: string[] = []; if (details && Array.isArray(details.imagePaths)) { for (const p of details.imagePaths) { if (typeof p === "string") paths.push(p); } } const merged = withDetailImages(result); const hasImages = resultImagesOf(merged).length > 0; return ( <> {PROMPT_FIELDS.map(([arg, label]) => { const value = args[arg]; return ( {value === undefined ? null : (str(value) ?? )} ); })} {changes && changes.length > 0 && (
{changes.map((change, i) => ( {typeof change === "string" ? change : } ))}
)} {inputs && inputs.length > 0 && (
{inputs.map((input, i) => { const path = isRecord(input) ? str(input.path) : null; const mime = isRecord(input) ? str(input.mime_type) : null; return ( {!isRecord(input) ? ( ) : path ? ( ) : ( `base64 image${mime ? ` (${mime})` : ""}` )} ); })}
)} {(provider || model) && } {revised && revised: {truncate(revised, 400)}} {paths.length > 0 && (
{paths.map((p, i) => ( ))}
)} {!hasImages && } ); } export const generateImageRenderer: ToolRenderer = { Summary, Body };