iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
ChatGPT & Codex

ChatGPT + Codex 打造高效能 AI 開發工作流系列 第 8

Day 08: 多模態工作流 (Multimodal Workflow):從 UI/UX 設計圖自動生成前端程式碼

  • 分享至 

  • xImage
  •  

Day 08: 多模態工作流 (Multimodal Workflow):從 UI/UX 設計圖自動生成前端程式碼 (Multimodal UI-to-Code)

本日核心價值 (Core Focus): 把設計圖(screenshot / Figma export)當成視覺規格,而不是「看圖說故事」。Workflow 固定三階段:vision 盤點元件 JSON → 用 tokens / spacing / states 約束生成 → 只輸出一張訂單狀態卡的 HTML/CSS,不生成整站 SPA。

概念說明與實戰情境 (Overview)

設計稿進工程最常失敗的點是:模型直接吐一個「大概長得像」的頁面,顏色、間距、狀態全是幻覺。正確做法是拆階段。先把 PNG(Figma export 或 screenshot)用 Chat Completions 的 image_url 送給 vision 模型,強制輸出元件盤點 JSON(元件名、層級、顏色 token、間距、狀態)。人工或程式檢視 JSON 後,第二輪只生成一個元件——本日是訂單狀態卡,不是整個 checkout SPA。範圍越小,spacing 與 hover/disabled 才對得上稿。

關鍵操作與範例 (Implementation & Example)

建議輸入:Figma 將 Frame 匯出 2x PNG,或瀏覽器截「單一元件」而非整頁。圖太雜,盤點 JSON 會漏狀態。Chat Completions 的 image 放在 user content 陣列,與文字 Prompt 並列:

{"type": "image_url", "image_url": {"url": image_url, "detail": "high"}}

url 可以是 https://...data:image/png;base64,...。本機檔案請自行讀 bytes 再 base64,不要把磁碟路徑傳給 API。detail: "high" 適合元件稿;整頁縮圖可用 low 降 Token。模型用 gpt-4o(可換團隊現用的 vision 模型)。

兩段 Prompt 分開:第一段禁止產出 HTML,只准 JSON;第二段禁止發明盤點裡沒有的顏色與間距。合併成「看圖寫出完整頁面」時,模型會同時幻覺色票、漏掉 disabled、再順便生一個假的 React router。分階段才能在 JSON 上做機械檢查(hex 格式、spacing 是否為 4 的倍數、states 鍵是否齊全),不過關就不要進 HTML。

視覺規格要逼模型講「看得到的數值」,不要講形容詞。tokens 是單一真相:顏色、字級、行高、radius、padding。states 則防止只做 default:稿上有 hover 就要寫;沒有就 null,階段 2 不准發明 drop-shadow。unknowns 是逃生口——看不清的 1px 線列入清單,交給設計,而不是讓 CSS 用 box-shadow: 0 8px 32px 填洞。

階段 1 Prompt(元件盤點,貼在文字 content):

你是 UI 規格抽取器,不是前端工程師。只根據圖片,輸出一個 JSON 物件,不要 Markdown。

Schema:
{
  "frame": {"width_px": number, "height_px": number, "background": "#RRGGBB"},
  "tokens": {
    "color": {"name": "#RRGGBB"},
    "space": {"name": number},
    "radius": {"name": number},
    "font": {"name": {"family": string, "size_px": number, "weight": number, "line_height_px": number}}
  },
  "components": [
    {
      "id": string,
      "type": "card|badge|button|text|row",
      "copy": string,
      "layout": {"padding_px": [t,r,b,l], "gap_px": number},
      "tokens_used": [string],
      "states": {
        "default": "可見樣式摘要",
        "hover": "若無則 null",
        "focus": "若無則 null",
        "disabled": "若無則 null",
        "loading": "若無則 null",
        "error": "若無則 null",
        "empty": "若無則 null"
      }
    }
  ],
  "unknowns": [string]
}

硬性規則:
- spacing 必須是 4 的倍數(4/8/12/16/24/32)。看不出來就列入 unknowns,禁止猜 13px。
- 顏色寫 hex;無法確定則 unknowns,禁止發明品牌色。
- 每個可互動元件必須填 states;圖上沒有 hover 就填 null,不要編造陰影。
- 只盤點「訂單狀態卡」範圍;忽略瀏覽器 chrome 與其他頁面區塊。

階段 2 Prompt(只准一張卡):

根據下列 component inventory JSON 產出單一檔案:訂單狀態卡。
只允許 HTML + 一個 <style>。禁止 React/Vue、禁止路由、禁止整頁 layout、禁止假資料 API。

硬性規則:
- 所有顏色、font-size、padding、gap、border-radius 必須引用 JSON 的 tokens;禁止新增 token。
- 卡片結構:標題列(訂單編號 + 狀態 badge)→ 商品列 → 底部 CTA。
- 必須實作 JSON 裡非 null 的 states(:hover / :focus-visible / :disabled / [data-state=loading|error|empty])。
- 互動只用 CSS;CTA 用 <button type="button">。
- 若 JSON.unknowns 非空,在 HTML 註解列出,不要用猜測填洞。

階段 1 的 Python 呼叫(本機 PNG → base64 image_url):

把規則從 Prompt 再翻譯成驗收條件,避免「Prompt 很完整、產出卻沒人查」:

  • 階段 1 JSON 必須能 json.loads,且含 tokens / components / unknowns
  • tokens.space 每個值是 4 的倍數;否則退回重抽,不要在 CSS 裡四捨五入。
  • 每個 button / ctastates.hoverstates.focus 若為 null,階段 2 仍至少做 :focus-visible(無障礙底線),但不得發明稿上沒有的漸層。
  • 階段 2 HTML 只能有一個根元件(article.cardform),禁止 nav、禁止第二張卡。
  • image_url 與文字 Prompt 必須在同一則 user message 的 content 陣列;只傳圖不傳規則,盤點會變成形容詞。
"""vision_inventory.py
依賴: pip install openai
環境: OPENAI_API_KEY;OPENAI_MODEL 預設 gpt-4o(須支援 vision,可換)
執行: python vision_inventory.py path/to/order-card.png
"""
from __future__ import annotations

import base64
import json
import mimetypes
import os
import sys
from pathlib import Path

from openai import OpenAI

MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o")
INVENTORY_PROMPT = """你是 UI 規格抽取器。只輸出 JSON,不要 Markdown。
(此處貼上階段 1 完整規則)"""


def file_to_data_url(path: Path) -> str:
    mime = mimetypes.guess_type(path.name)[0] or "image/png"
    b64 = base64.b64encode(path.read_bytes()).decode("ascii")
    return f"data:{mime};base64,{b64}"


def extract_inventory(image_path: Path) -> dict:
    client = OpenAI()
    resp = client.chat.completions.create(
        model=MODEL,
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": INVENTORY_PROMPT},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": file_to_data_url(image_path),
                            "detail": "high",
                        },
                    },
                ],
            }
        ],
        response_format={"type": "json_object"},
    )
    return json.loads(resp.choices[0].message.content or "{}")


if __name__ == "__main__":
    inv = extract_inventory(Path(sys.argv[1]))
    print(json.dumps(inv, ensure_ascii=False, indent=2))

第二輪把 json.dumps(inventory) 貼進階段 2 Prompt 即可。以下是符合「tokens + spacing + states」的訂單狀態卡範例(可當階段 2 的驗收基準,而不是唯一答案):

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8" />
  <title>Order status card</title>
  <style>
    :root {
      --color-bg: #f4f1ea;
      --color-card: #fffaf2;
      --color-ink: #1f1b16;
      --color-muted: #6b6258;
      --color-line: #e6dccf;
      --color-paid: #0f7a4a;
      --color-paid-bg: #e3f6ec;
      --color-cta: #c45c26;
      --color-cta-ink: #fffaf2;
      --space-8: 8px;
      --space-12: 12px;
      --space-16: 16px;
      --space-24: 24px;
      --radius-8: 8px;
      --radius-16: 16px;
      --font: 16px/24px "Segoe UI", "Noto Sans TC", sans-serif;
      --font-sm: 13px/20px "Segoe UI", "Noto Sans TC", sans-serif;
    }
    body { margin: 24px; background: var(--color-bg); font: var(--font); color: var(--color-ink); }
    .card {
      width: 360px; background: var(--color-card); border: 1px solid var(--color-line);
      border-radius: var(--radius-16); padding: var(--space-24); display: flex;
      flex-direction: column; gap: var(--space-16);
    }
    .row { display: flex; justify-content: space-between; align-items: center; gap: var(--space-12); }
    .muted { color: var(--color-muted); font: var(--font-sm); }
    .badge {
      background: var(--color-paid-bg); color: var(--color-paid); border-radius: 999px;
      padding: 4px 12px; font: var(--font-sm);
    }
    .item { border-top: 1px solid var(--color-line); padding-top: var(--space-16); }
    .cta {
      border: 0; border-radius: var(--radius-8); background: var(--color-cta);
      color: var(--color-cta-ink); padding: var(--space-12) var(--space-16); cursor: pointer;
    }
    .cta:hover { filter: brightness(1.05); }
    .cta:focus-visible { outline: 2px solid var(--color-ink); outline-offset: 2px; }
    .cta:disabled { opacity: 0.45; cursor: not-allowed; }
    .card[data-state="loading"] .cta::after { content: " …"; }
    .card[data-state="error"] { border-color: #b42318; }
    .card[data-state="empty"] .item { display: none; }
    .card[data-state="empty"]::after { content: "尚無商品列"; color: var(--color-muted); }
  </style>
</head>
<body>
  <article class="card" data-state="default">
    <div class="row">
      <strong>ORD-1001</strong>
      <span class="badge">已付款</span>
    </div>
    <div class="item row">
      <span>Widget M × 2</span>
      <span class="muted">NT$ 1,280</span>
    </div>
    <button class="cta" type="button">查看物流</button>
  </article>
</body>
</html>

驗收清單(對稿用,不要靠「感覺很像」):badge 是否 pill、padding 是否 24、CTA hover / focus-visible / disabled 是否存在、error / empty 是否用 data-state 而不是另做一頁。差 4px 就是沒對上 token。把驗收寫成 checklist,下一張卡(例如退貨表單)才能複用同一套 Prompt,而不是每次重新「請寫得精緻一點」。

階段 2 的輸出應可直接開瀏覽器核對,不要再丟回模型問「像不像」。人眼對稿比 vision 二次評分便宜且可重現:量 padding、對 hex、Tab 一次看 focus ring。若 JSON 寫 space-24 但 HTML 寫死 padding: 22px,視為生成失敗並重跑階段 2,不要手工改到「差不多」。Figma 若有 Variables / Design Tokens,匯出後可把官方 token 名預先寫進階段 1 Schema,減少模型自創 --color-brand-1

注意事項與常見失敗 (Pitfalls)

  • 一輪 Prompt 就要 HTML: 模型會跳過盤點、發明 #4A90D9。必須先 JSON 後程式碼;JSON 未過關不要進階段 2。機械檢查失敗時把 JSON 與錯誤鍵名一併存檔,下一輪只修規格、不要同時改 CSS。
  • 整頁截圖當輸入: nav、modal、browser chrome 會污染 tokens。Figma 只 export 目標 Frame。若必須用 screenshot,先裁切到卡片外框再送 image_url,否則背景灰與瀏覽器主題會變成「品牌色」。
  • 忽略 image_urldetail 小字、1px 邊框在 low 下會消失,spacing 全變成猜的。元件稿用 high。圖超過 API 尺寸上限時先縮小長邊、不要改 detail 來省 Token 而犧牲對稿。
  • 要模型做完整 SPA: 路由、狀態管理、假 API 會稀釋視覺精度。本日範圍是一張卡或一個 form。需要列表頁時,重複「一張卡」工作流,用同一套 tokens,而不是一次生成整個 dashboard。
  • 把 Figma 連結直接當 image_url API 需要可抓的圖(公開 URL 或 data URL),不是 Figma 檔案權限連結。先 export PNG。團隊若用私有 CDN,確認 OpenAI 能抓該 URL,否則改走 base64。
  • 沒有 unknowns 看不清的陰影硬編造。規定列 unknowns,階段 2 用註解標出,交設計確認。註解應寫「無法從稿讀取的項目」,不要寫「可能是某某色」,以免下一個人把猜測當規格。

本日總結 (Takeaways)

  • 多模態工作流是「圖 → 規格 JSON → 單一元件程式碼」,不是「圖 → 整個網站」。
  • Chat Completions 用 content 陣列傳 image_url(https 或 base64 data URL),detail 依稿件精細度選擇。
  • Prompt 必須強制 tokens、4 的倍數 spacing、以及 default/hover/focus/disabled/loading/error/empty。
  • 輸出範圍鎖死一張訂單狀態卡;用 CSS 狀態選擇器驗收,而不是再截一張圖請模型「看看像不像」。
  • gpt-4o 可換現用 vision 模型;換模型時用同一張 PNG + 同一 Schema 做回歸。

明日預告 (Next)

明日進入 後端系統整合實戰:將 ChatGPT API 嵌入 C# .NET / PHP Web 架構,用 ASP.NET Core Minimal API 包一層 /ai/summarize-order,並附對等 PHP 片段。


上一篇
Day 07: Function Calling 實戰 (下):處理複雜參數、錯誤捕捉與 Tool Chaining
系列文
ChatGPT + Codex 打造高效能 AI 開發工作流8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言