( beforePath: string, afterPath: string, )
| 17 | * Compare two images and describe the visual differences. |
| 18 | */ |
| 19 | export async function diffMockups( |
| 20 | beforePath: string, |
| 21 | afterPath: string, |
| 22 | ): Promise<DiffResult> { |
| 23 | const apiKey = requireApiKey(); |
| 24 | const beforeData = fs.readFileSync(beforePath).toString("base64"); |
| 25 | const afterData = fs.readFileSync(afterPath).toString("base64"); |
| 26 | |
| 27 | const controller = new AbortController(); |
| 28 | const timeout = setTimeout(() => controller.abort(), 60_000); |
| 29 | |
| 30 | try { |
| 31 | const response = await fetch("https://api.openai.com/v1/chat/completions", { |
| 32 | method: "POST", |
| 33 | headers: { |
| 34 | "Authorization": `Bearer ${apiKey}`, |
| 35 | "Content-Type": "application/json", |
| 36 | }, |
| 37 | body: JSON.stringify({ |
| 38 | model: "gpt-4o", |
| 39 | messages: [{ |
| 40 | role: "user", |
| 41 | content: [ |
| 42 | { |
| 43 | type: "text", |
| 44 | text: `Compare these two UI images. The first is the BEFORE (or design intent), the second is the AFTER (or actual implementation). Return valid JSON only: |
| 45 | |
| 46 | { |
| 47 | "differences": [ |
| 48 | {"area": "header", "description": "Font size changed from ~32px to ~24px", "severity": "high"}, |
| 49 | ... |
| 50 | ], |
| 51 | "summary": "one sentence overall assessment", |
| 52 | "matchScore": 85 |
| 53 | } |
| 54 | |
| 55 | severity: "high" = noticeable to any user, "medium" = visible on close inspection, "low" = minor/pixel-level. |
| 56 | matchScore: 100 = identical, 0 = completely different. |
| 57 | Focus on layout, typography, colors, spacing, and element presence/absence. Ignore rendering differences (anti-aliasing, sub-pixel).`, |
| 58 | }, |
| 59 | { |
| 60 | type: "image_url", |
| 61 | image_url: { url: `data:image/png;base64,${beforeData}` }, |
| 62 | }, |
| 63 | { |
| 64 | type: "image_url", |
| 65 | image_url: { url: `data:image/png;base64,${afterData}` }, |
| 66 | }, |
| 67 | ], |
| 68 | }], |
| 69 | max_tokens: 600, |
| 70 | response_format: { type: "json_object" }, |
| 71 | }), |
| 72 | signal: controller.signal, |
| 73 | }); |
| 74 | |
| 75 | if (!response.ok) { |
| 76 | const error = await response.text(); |
no test coverage detected