產品原型

prototype

先釐清原型要回答的是狀態邏輯或視覺方向,再建立可丟棄、容易啟動且會呈現完整狀態的互動成品,用最少工程成本驗證單一問題。

查看來源 ↗
作者/來源mattpocock★ 244,490 GitHub Stars
測試版本6654f6b60cd9更新日期:2026-08-24T14:19:57Z
支援平台OpenAI · Claude本次實測平台
安裝方式npx skills add mattpocock/skills --skill prototype
公平比較方式

三個問題,分別測載入前與載入後。

每次回答都在全新環境中產生。兩邊使用相同模型與設定,唯一差別是右側載入了這個 Skill。

模型版本gpt-5.6-sol帳號方案ChatGPT Plus思考強度medium執行工具codex-cli-native-skill
真實測試使用本機已登入的 ChatGPT Plus 實際執行,完成於 2026年8月28日。所有檢查通過後才公開,回答未經人工改寫。
三階段測試3 個問題全部完成版本核對3/3 通過環境清理9/9 通過測試隔離每回合全新環境
01
基礎題

預約保留狀態模型

測試問題

我們想驗證『座位保留』狀態是否容易理解,而不是決定視覺風格。狀態只有 available、held、confirmed、expired;hold 可轉 confirmed 或 expired,expired 可重新 hold,confirmed 不可逆。請直接輸出一份不超過 180 行、可雙擊開啟的單一 HTML 原型,讓非工程師可自由操作並用引導分頁走過兩條困難流程;每次操作後必須顯示完整狀態。不要加入持久化、測試或正式架構。

載入前 · 0 個 Skill載入後 · 版本已核對再次確認 · 0 個 Skill已確認模型套用 Skill三個環境皆已清理
未載入 Skill1896 tokens · 41328 ms
<!doctype html> <html lang="zh-Hant"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>座位保留狀態理解原型</title> <style> body{font:16px system-ui,sans-serif;max-width:900px;margin:24px auto;padding:0 16px;color:#222} button{font:inherit;padding:8px 12px;margin:3px;cursor:pointer} button:disabled{cursor:not-allowed;opacity:.45} .tabs button[aria-selected="true"]{outline:3px solid #222} .guide,.snapshot{border:1px solid #999;padding:14px;margin:12px 0} .seats{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:12px 0} .seat{min-height:72px} .seat.selected{outline:4px solid #222} .available{background:#fff}.held{background:#ffe89a} .confirmed{background:#b8edc2}.expired{background:#ddd} .status{font-weight:700}.message{min-height:24px} pre{white-space:pre-wrap;margin:0} small{display:block;color:#555} </style> </head> <body> <h1>座位保留狀態理解原型</h1> <p>選擇座位後執行允許的操作。這是狀態驗證原型,逾時由按鈕模擬。</p> <nav class="tabs" aria-label="困難流程"> <button id="tab1" aria-selected="true" onclick="openFlow(1)">流程一:保留後確認</button> <button id="tab2" aria-selected="false" onclick="openFlow(2)">流程二:逾時後重保留</button> </nav> <section class="guide"> <h2 id="flowTitle"></h2> <p id="flowText"></p> <p><strong id="flowProgress"></strong></p> <button onclick="previousStep()">上一步</button> <button onclick="nextStep()">下一步</button> <button onclick="resetFlow()">重設本流程</button> </section> <h2>所有座位</h2> <div id="seats" class="seats"></div> <section> <h2>所選座位操作</h2> <p id="selection">尚未選擇座位</p> <button id="hold" onclick="transition('held')">保留(hold)</button> <button id="confirm" onclick="transition('confirmed')">確認(confirm)</button> <button id="expire" onclick="transition('expired')">模擬逾時(expire)</button> </section> <p id="message" class="message" role="status" aria-live="polite"></p> <section class="snapshot"> <h2>完整狀態(每次操作後更新)</h2> <pre id="snapshot"></pre> </section> <script> const initial={A1:"available",A2:"available",A3:"held",A4:"confirmed", B1:"expired",B2:"available",B3:"available",B4:"available"}; const labels={available:"available|可用",held:"held|保留中", confirmed:"confirmed|已確認",expired:"expired|已逾時"}; const flows={ 1:[ ["步驟 1:選一個 available 座位","點選任一狀態為 available 的座位。"], ["步驟 2:建立 hold","按「保留(hold)」,確認完整狀態變成 held。"], ["步驟 3:完成確認","按「確認(confirm)」,確認狀態變成 confirmed 且不可逆。"] ], 2:[ ["步驟 1:選一個 available 座位","點選任一狀態為 available 的座位。"], ["步驟 2:建立 hold","按「保留(hold)」,確認狀態變成 held。"], ["步驟 3:讓 hold 逾時","按「模擬逾時(expire)」,確認狀態變成 expired。"], ["步驟 4:重新 hold","按「保留(hold)」,確認 expired 可再次變成 held。"] ] }; let state={...initial},selected=null,flow=1,step=0,lastAction="尚無操作"; function render(){ seats.innerHTML=""; Object.entries(state).forEach(([id,status])=>{ const b=document.createElement("button"); b.className=`seat ${status}${selected===id?" selected":""}`; b.innerHTML=`<strong>${id}</strong><small>${labels[status]}</small>`; b.setAttribute("aria-pressed",selected===id); b.onclick=()=>selectSeat(id); seats.appendChild(b); }); const status=selected?state[selected]:null; selection.textContent=selected?`${selected}:${labels[status]}`:"尚未選擇座位"; hold.disabled=!selected||!["available","expired"].includes(status); confirm.disabled=!selected||status!=="held"; expire.disabled=!selected||status!=="held"; const complete=Object.fromEntries(Object.entries(state).map(([id,s])=>[id,{ status:s,allowedActions:allowed(s) }])); snapshot.textContent=JSON.stringify({ selectedSeat:selected, selectedStatus:status, lastAction, seats:complete },null,2); renderGuide(); } function allowed(s){ if(s==="available"||s==="expired")return["hold"]; if(s==="held")return["confirm","expire"]; return[]; } function selectSeat(id){ selected=id; lastAction=`選擇座位 ${id}`; message.textContent=`已選擇 ${id};完整狀態已更新。`; render(); } function transition(target){ if(!selected)return; const from=state[selected]; const valid=(target==="held"&&["available","expired"].includes(from))|| (from==="held"&&["confirmed","expired"].includes(target)); if(!valid){ message.textContent=`不允許 ${from} → ${target};狀態未改變。`; lastAction=`拒絕 ${selected}:${from} → ${target}`; }else{ state[selected]=target; lastAction=`${selected}:${from} → ${target}`; message.textContent=`操作完成:${lastAction}。`; } render(); } function openFlow(n){ flow=n;step=0; tab1.setAttribute("aria-selected",n===1); tab2.setAttribute("aria-selected",n===2); renderGuide(); } function renderGuide(){ const item=flows[flow][step]; flowTitle.textContent=item[0]; flowText.textContent=item[1]; flowProgress.textContent=`流程 ${flow},第 ${step+1}/${flows[flow].length} 步`; } function previousStep(){step=Math.max(0,step-1);renderGuide()} function nextStep(){step=Math.min(flows[flow].length-1,step+1);renderGuide()} function resetFlow(){ state={...initial};selected=null;step=0;lastAction=`重設流程 ${flow}`; message.textContent="已恢復所有初始狀態。";render(); } render(); </script> </body> </html>
已載入 Skill +2925 tokens · 64907 ms
<!doctype html> <html lang="zh-Hant"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>座位保留狀態原型</title> <style> *{box-sizing:border-box} body{margin:0;background:#f5f7fa;color:#17202a;font:16px/1.5 system-ui,sans-serif} main{max-width:820px;margin:auto;padding:32px 20px 60px} h1{margin:0 0 8px;font-size:28px} h2{margin:30px 0 12px;font-size:19px} p{margin:6px 0 16px;color:#52606d} .card{background:white;border:1px solid #d9e2ec;border-radius:10px;padding:18px} .state{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px} .field{padding:12px;background:#f7f9fc;border-radius:8px} .label{display:block;color:#697786;font-size:13px} .value{display:block;margin-top:3px;font-weight:700} #status{font-size:24px;color:#155eef} #message.ok{color:#137333} #message.no{color:#b42318} .buttons,.tabs,.steps{display:flex;flex-wrap:wrap;gap:9px} button{border:1px solid #aeb8c4;border-radius:7px;background:white;padding:9px 13px; color:#17202a;font:inherit;cursor:pointer} button:hover{border-color:#155eef} button.primary,.tab.active{background:#155eef;border-color:#155eef;color:white} button:disabled{cursor:default;opacity:.45} .note{font-size:14px} .guide{margin-top:12px} .guide-panel{display:none} .guide-panel.active{display:block} .step{display:flex;gap:12px;padding:11px 0;border-top:1px solid #e6ebf0} .step:first-of-type{border-top:0} .step-number{flex:0 0 25px;height:25px;border-radius:50%;background:#e8eef8;text-align:center} .step.done .step-number{background:#137333;color:white} .step.current{font-weight:700} .rules{font-size:14px;color:#52606d} @media(max-width:560px){.state{grid-template-columns:1fr}} </style> </head> <body> <main> <h1>座位保留狀態原型</h1> <p>用來驗證四種狀態與不可逆規則是否容易理解;這是一次性邏輯原型,不代表最終視覺。</p> <h2>完整狀態</h2> <section class="card state" aria-live="polite"> <div class="field"><span class="label">目前狀態</span><span class="value" id="status"></span></div> <div class="field"><span class="label">現在允許</span><span class="value" id="allowed"></span></div> <div class="field"><span class="label">上次操作</span><span class="value" id="action"></span></div> <div class="field"><span class="label">操作結果</span><span class="value" id="message"></span></div> </section> <p class="rules">available → held;held → confirmed 或 expired;expired → held;confirmed 不可逆。</p> <h2>自由操作</h2> <section class="card"> <div class="buttons"> <button data-action="HOLD">保留座位</button> <button data-action="CONFIRM">確認座位</button> <button data-action="EXPIRE">讓保留到期</button> <button id="reset">重新開始</button> </div> <p class="note">按鈕刻意不隱藏;不合法的操作會被拒絕,並再次顯示完整狀態。</p> </section> <h2>困難流程引導</h2> <div class="tabs" role="tablist"> <button class="tab active" data-tab="recover">到期後重新保留</button> <button class="tab" data-tab="locked">確認後不可逆</button> </div> <section class="card guide"> <div class="guide-panel active" id="recover"> <p>觀察到期不是終點:座位可再次保留,然後確認。</p> <button class="primary start" data-guide="recover">開始此流程</button> <div class="steps"></div> </div> <div class="guide-panel" id="locked"> <p>觀察確認後的任何變更嘗試,都不會改變 confirmed。</p> <button class="primary start" data-guide="locked">開始此流程</button> <div class="steps"></div> </div> </section> </main> <script> const SeatMachine=(()=>{ const transitions={ available:{HOLD:"held"}, held:{CONFIRM:"confirmed",EXPIRE:"expired"}, expired:{HOLD:"held"}, confirmed:{} }; const labels={HOLD:"保留座位",CONFIRM:"確認座位",EXPIRE:"讓保留到期"}; function reduce(state,action){ const next=transitions[state.status][action]; return next ? {state:{status:next},accepted:true,message:`已從 ${state.status} 變為 ${next}`} : {state,accepted:false,message:`${state.status} 不允許「${labels[action]}」`}; } function allowed(state){return Object.keys(transitions[state.status]);} return {reduce,allowed,labels}; })(); let state={status:"available"}; let last={action:"尚未操作",message:"等待操作",accepted:true}; let guide=null; const scenarios={ recover:[ ["HOLD","先保留座位:available → held"], ["EXPIRE","讓保留到期:held → expired"], ["HOLD","重新保留:expired → held"], ["CONFIRM","完成確認:held → confirmed"] ], locked:[ ["HOLD","先保留座位:available → held"], ["CONFIRM","確認座位:held → confirmed"], ["EXPIRE","嘗試讓它到期:應被拒絕"], ["HOLD","嘗試再次保留:應被拒絕"] ] }; function render(){ status.textContent=state.status; const actions=SeatMachine.allowed(state).map(x=>SeatMachine.labels[x]); allowed.textContent=actions.length?actions.join("、"):"無(不可逆)"; action.textContent=last.action; message.textContent=last.message; message.className="value "+(last.accepted?"ok":"no"); document.querySelectorAll(".guide-panel").forEach(panel=>{ const box=panel.querySelector(".steps"); box.innerHTML=""; scenarios[panel.id].forEach(([type,text],i)=>{ const row=document.createElement("div"); const active=guide?.id===panel.id&&guide.step===i; row.className="step "+(guide?.id===panel.id&&i<guide.step?"done ":"")+(active?"current":""); row.innerHTML=`<span class="step-number">${i+1}</span><div>${text}<br></div>`; const b=document.createElement("button"); b.textContent=SeatMachine.labels[type]; b.disabled=!active; b.onclick=()=>act(type,true); row.lastChild.appendChild(b); box.appendChild(row); }); }); } function act(type,fromGuide=false){ const result=SeatMachine.reduce(state,type); state=result.state; last={action:SeatMachine.labels[type],message:result.message,accepted:result.accepted}; if(fromGuide&&guide) guide.step++; render(); } function reset(message="已回到流程起點"){ state={status:"available"}; last={action:"重新開始",message,accepted:true}; guide=null; render(); } document.querySelectorAll("[data-action]").forEach(b=>b.onclick=()=>act(b.dataset.action)); document.querySelector("#reset").onclick=()=>reset(); document.querySelectorAll(".tab").forEach(tab=>tab.onclick=()=>{ document.querySelectorAll(".tab,.guide-panel").forEach(x=>x.classList.remove("active")); tab.classList.add("active"); document.querySelector("#"+tab.dataset.tab).classList.add("active"); }); document.querySelectorAll(".start").forEach(b=>b.onclick=()=>{ state={status:"available"}; guide={id:b.dataset.guide,step:0}; last={action:"開始引導",message:"已回到 available,請執行第 1 步",accepted:true}; render(); }); render(); </script> </body> </html>
02
實務題

探索報表空狀態外觀

測試問題

問題是『資料報表第一次使用時應該長什麼樣』,不是驗證後端邏輯。現有專案是 Next.js App Router,頁面預計位於 app/reports/page.tsx。請提出三個差異明顯、可用 URL search param 切換的 UI 原型方向,並提供一個精簡可執行的 page.tsx 骨架與底部切換列;資料只放記憶體,不做 API、測試、錯誤處理或生產抽象。

載入前 · 0 個 Skill載入後 · 版本已核對再次確認 · 0 個 Skill已確認模型套用 Skill三個環境皆已清理
未載入 Skill3324 tokens · 66110 ms
## 三個 UI 原型方向 1. **總覽儀表板 `?view=overview`** 以 KPI、趨勢與分類占比為主,適合第一次快速掌握整體狀態。 2. **資料表優先 `?view=table`** 直接呈現可掃描的明細表與摘要,適合重視核對、比較資料的使用者。 3. **洞察摘要 `?view=insights`** 將資料轉成敘事式重點與建議行動,適合不熟悉報表或只想知道「接下來要做什麼」的使用者。 ```tsx // app/reports/page.tsx type View = "overview" | "table" | "insights"; const rows = [ { channel: "自然搜尋", visits: 4280, conversions: 312, revenue: 186400 }, { channel: "付費廣告", visits: 3150, conversions: 246, revenue: 143200 }, { channel: "電子郵件", visits: 1760, conversions: 198, revenue: 121800 }, { channel: "社群媒體", visits: 2340, conversions: 124, revenue: 79600 }, ]; const money = new Intl.NumberFormat("zh-TW", { style: "currency", currency: "TWD", maximumFractionDigits: 0, }); const number = new Intl.NumberFormat("zh-TW"); function Overview() { const visits = rows.reduce((sum, row) => sum + row.visits, 0); const conversions = rows.reduce((sum, row) => sum + row.conversions, 0); const revenue = rows.reduce((sum, row) => sum + row.revenue, 0); return ( <> <header> <p style={styles.eyebrow}>本月營運總覽</p> <h1 style={styles.title}>資料表現一眼看懂</h1> <p style={styles.muted}>先看關鍵結果,再決定是否深入明細。</p> </header> <section style={styles.cardGrid}> <Metric label="總造訪" value={number.format(visits)} note="較上月 +8.4%" /> <Metric label="轉換數" value={number.format(conversions)} note="較上月 +5.1%" /> <Metric label="營收" value={money.format(revenue)} note="較上月 +11.2%" /> </section> <section style={styles.panel}> <h2 style={styles.subtitle}>各管道營收</h2> <div style={styles.barList}> {rows.map((row) => ( <div key={row.channel}> <div style={styles.barLabel}> <span>{row.channel}</span> <strong>{money.format(row.revenue)}</strong> </div> <div style={styles.track}> <div style={{ ...styles.bar, width: `${(row.revenue / 186400) * 100}%`, }} /> </div> </div> ))} </div> </section> </> ); } function TableView() { return ( <> <header style={styles.splitHeader}> <div> <p style={styles.eyebrow}>管道明細</p> <h1 style={styles.title}>本月資料表</h1> </div> <div style={styles.summary}>共 {rows.length} 筆管道資料</div> </header> <section style={{ ...styles.panel, overflowX: "auto" }}> <table style={styles.table}> <thead> <tr> <th style={styles.th}>管道</th> <th style={styles.thRight}>造訪</th> <th style={styles.thRight}>轉換</th> <th style={styles.thRight}>轉換率</th> <th style={styles.thRight}>營收</th> </tr> </thead> <tbody> {rows.map((row) => ( <tr key={row.channel}> <td style={styles.td}>{row.channel}</td> <td style={styles.tdRight}>{number.format(row.visits)}</td> <td style={styles.tdRight}>{number.format(row.conversions)}</td> <td style={styles.tdRight}> {((row.conversions / row.visits) * 100).toFixed(1)}% </td> <td style={styles.tdRight}>{money.format(row.revenue)}</td> </tr> ))} </tbody> </table> </section> </> ); } function Insights() { const best = [...rows].sort((a, b) => b.revenue - a.revenue)[0]; const efficient = [...rows].sort( (a, b) => b.conversions / b.visits - a.conversions / a.visits, )[0]; return ( <> <header> <p style={styles.eyebrow}>自動摘要</p> <h1 style={styles.title}>這份資料告訴你什麼?</h1> <p style={styles.muted}>用重點與下一步取代密集圖表。</p> </header> <section style={styles.insightGrid}> <article style={styles.darkCard}> <span style={styles.badge}>最重要發現</span> <h2 style={styles.insightTitle}> {best.channel}帶來最高營收,貢獻 {money.format(best.revenue)}。 </h2> <p style={styles.darkMuted}> 可優先檢視其內容與流量來源,找出能複製到其他管道的做法。 </p> </article> <article style={styles.panel}> <p style={styles.eyebrow}>效率亮點</p> <h2 style={styles.subtitle}>{efficient.channel}的轉換率最高</h2> <p style={styles.muted}> 每 100 次造訪約產生{" "} {((efficient.conversions / efficient.visits) * 100).toFixed(1)} 次轉換。 </p> </article> <article style={styles.panel}> <p style={styles.eyebrow}>建議下一步</p> <ol style={styles.actions}> <li>比較自然搜尋與電子郵件的高轉換內容。</li> <li>確認付費廣告的獲客成本是否符合目標。</li> <li>為社群媒體設定下一期改善基準。</li> </ol> </article> </section> </> ); } function Metric({ label, value, note, }: { label: string; value: string; note: string; }) { return ( <article style={styles.panel}> <p style={styles.metricLabel}>{label}</p> <strong style={styles.metricValue}>{value}</strong> <p style={styles.positive}>{note}</p> </article> ); } export default async function ReportsPage({ searchParams, }: { searchParams: Promise<{ view?: string }>; }) { const params = await searchParams; const view: View = params.view === "table" || params.view === "insights" ? params.view : "overview"; return ( <main style={styles.page}> <div style={styles.content}> {view === "overview" && <Overview />} {view === "table" && <TableView />} {view === "insights" && <Insights />} </div> <nav aria-label="切換報表原型" style={styles.switcher}> <SwitchLink view="overview" current={view} label="總覽儀表板" /> <SwitchLink view="table" current={view} label="資料表優先" /> <SwitchLink view="insights" current={view} label="洞察摘要" /> </nav> </main> ); } function SwitchLink({ view, current, label, }: { view: View; current: View; label: string; }) { const active = view === current; return ( <a href={`/reports?view=${view}`} aria-current={active ? "page" : undefined} style={{ ...styles.switchLink, ...(active ? styles.switchLinkActive : {}), }} > {label} </a> ); } const styles: Record<string, React.CSSProperties> = { page: { minHeight: "100vh", background: "#f4f5f7", color: "#18202b", fontFamily: "Arial, sans-serif", padding: "56px 24px 120px", }, content: { width: "min(1040px, 100%)", margin: "0 auto" }, eyebrow: { margin: "0 0 8px", color: "#617083", fontSize: 13, fontWeight: 700, letterSpacing: 1.2, textTransform: "uppercase", }, title: { margin: 0, fontSize: "clamp(32px, 5vw, 52px)", lineHeight: 1.1 }, subtitle: { margin: "0 0 12px", fontSize: 22 }, muted: { color: "#687587", lineHeight: 1.7 }, cardGrid: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(210px, 1fr))", gap: 16, margin: "36px 0 16px", }, panel: { padding: 24, background: "#fff", border: "1px solid #e1e5ea", borderRadius: 18, }, metricLabel: { margin: 0, color: "#687587" }, metricValue: { display: "block", margin: "12px 0", fontSize: 30 }, positive: { margin: 0, color: "#16834b", fontSize: 14 }, barList: { display: "grid", gap: 20, marginTop: 24 }, barLabel: { display: "flex", justifyContent: "space-between", gap: 16, marginBottom: 8, }, track: { height: 10, overflow: "hidden", background: "#edf0f3", borderRadius: 99 }, bar: { height: "100%", background: "#3167e3", borderRadius: 99 }, splitHeader: { display: "flex", alignItems: "end", justifyContent: "space-between", gap: 24, marginBottom: 28, }, summary: { padding: "10px 14px", background: "#e7edff", color: "#274ea5", borderRadius: 99, fontSize: 14, }, table: { width: "100%", borderCollapse: "collapse", minWidth: 660 }, th: { padding: "14px 12px", borderBottom: "2px solid #dfe4ea", textAlign: "left", color: "#617083", }, thRight: { padding: "14px 12px", borderBottom: "2px solid #dfe4ea", textAlign: "right", color: "#617083", }, td: { padding: "18px 12px", borderBottom: "1px solid #edf0f3" }, tdRight: { padding: "18px 12px", borderBottom: "1px solid #edf0f3", textAlign: "right", }, insightGrid: { display: "grid", gap: 16, marginTop: 36 }, darkCard: { padding: 32, color: "#fff", background: "#172033", borderRadius: 24, }, badge: { display: "inline-block", padding: "7px 10px", color: "#bcd0ff", background: "#293756", borderRadius: 99, fontSize: 13, }, insightTitle: { maxWidth: 700, margin: "22px 0 12px", fontSize: "clamp(26px, 4vw, 42px)", lineHeight: 1.2, }, darkMuted: { maxWidth: 700, color: "#bac4d6", lineHeight: 1.7 }, actions: { margin: "12px 0 0", paddingLeft: 22, lineHeight: 2 }, switcher: { position: "fixed", left: "50%", bottom: 22, zIndex: 10, display: "flex", gap: 6, width: "max-content", maxWidth: "calc(100% - 32px)", padding: 6, overflowX: "auto", background: "#172033", borderRadius: 16, boxShadow: "0 12px 36px rgba(23, 32, 51, .25)", transform: "translateX(-50%)", }, switchLink: { padding: "11px 15px", color: "#bbc4d4", borderRadius: 11, textDecoration: "none", whiteSpace: "nowrap", fontSize: 14, }, switchLinkActive: { color: "#172033", background: "#fff", fontWeight: 700 }, }; ```
已載入 Skill +4643 tokens · 98409 ms
## 三個 UI 原型方向 1. **A|引導式建立** 用三個簡短問題帶使用者建立第一份報表,適合需要降低空白頁焦慮的產品。 2. **B|範本優先** 先展示常見報表範本,使用者挑一個再調整,適合已有明確使用情境的產品。 3. **C|範例資料預覽** 首次進入便看到完整示範報表,再引導替換成自己的資料,適合讓使用者先理解成果。 切換網址: - `/reports?variant=A` - `/reports?variant=B` - `/reports?variant=C` ```tsx // app/reports/page.tsx "use client"; /** * PROTOTYPE: * Three first-use report variants on /reports, switchable via ?variant=. * Throw this file away after choosing a direction. */ import { Suspense, useCallback, useEffect, useState, type CSSProperties, } from "react"; import { useRouter, useSearchParams } from "next/navigation"; const variants = [ { key: "A", name: "引導式建立" }, { key: "B", name: "範本優先" }, { key: "C", name: "範例資料預覽" }, ] as const; type VariantKey = (typeof variants)[number]["key"]; const templates = [ { name: "營運概覽", detail: "收入、訂單與轉換率" }, { name: "行銷成效", detail: "流量、活動與獲客成本" }, { name: "客戶健康度", detail: "活躍、留存與流失風險" }, ]; const sampleRows = [ ["本月收入", "NT$ 1,284,000", "+12.4%"], ["新客戶", "328", "+8.1%"], ["轉換率", "4.7%", "-0.3%"], ]; export default function ReportsPage() { return ( <Suspense fallback={<main style={pageStyle}>載入原型⋯</main>}> <ReportsPrototype /> </Suspense> ); } function ReportsPrototype() { const searchParams = useSearchParams(); const rawVariant = searchParams.get("variant")?.toUpperCase(); const variant: VariantKey = rawVariant === "B" || rawVariant === "C" ? rawVariant : "A"; return ( <> {variant === "A" && <GuidedSetup />} {variant === "B" && <TemplateGallery />} {variant === "C" && <SampleReport />} <PrototypeSwitcher current={variant} /> </> ); } function GuidedSetup() { return ( <main style={pageStyle}> <header style={headerStyle}> <div> <span style={eyebrowStyle}>報表</span> <h1 style={titleStyle}>建立第一份資料報表</h1> <p style={mutedStyle}>回答三個問題,我們先替你組出一個版本。</p> </div> <span style={badgeStyle}>步驟 1 / 3</span> </header> <section style={splitStyle}> <div style={panelStyle}> <h2>你最想追蹤什麼?</h2> <div style={choiceGridStyle}> {["營收成長", "行銷成效", "客戶留存", "團隊效率"].map( (item) => ( <button key={item} style={choiceStyle}> <strong>{item}</strong> <span style={mutedStyle}>選擇這個目標 →</span> </button> ), )} </div> </div> <aside style={{ ...panelStyle, background: "#111827", color: "white" }}> <span style={{ opacity: 0.65 }}>即將產生</span> <h2>你的管理摘要</h2> <ul style={{ lineHeight: 2 }}> <li>3 個核心指標</li> <li>一張趨勢圖</li> <li>本週值得注意的變化</li> </ul> </aside> </section> </main> ); } function TemplateGallery() { const [selected, setSelected] = useState<string | null>(null); return ( <main style={pageStyle}> <header style={headerStyle}> <div> <span style={eyebrowStyle}>報表範本庫</span> <h1 style={titleStyle}>你想從哪一份開始?</h1> </div> <button style={secondaryButtonStyle}>從空白建立</button> </header> <section style={templateLayoutStyle}> <nav style={sidebarStyle}> <strong>分類</strong> {["推薦", "營運", "行銷", "客戶", "財務"].map((item) => ( <button key={item} style={navButtonStyle}> {item} </button> ))} </nav> <div style={choiceGridStyle}> {templates.map((template, index) => ( <article key={template.name} style={panelStyle}> <div style={previewStyle}>範本預覽 {index + 1}</div> <h2>{template.name}</h2> <p style={mutedStyle}>{template.detail}</p> <button style={primaryButtonStyle} onClick={() => setSelected(template.name)} > 使用這個範本 </button> </article> ))} </div> </section> {selected && <p style={stateStyle}>目前選擇:{selected}</p>} </main> ); } function SampleReport() { const [showSample, setShowSample] = useState(true); return ( <main style={pageStyle}> <header style={headerStyle}> <div> <span style={eyebrowStyle}>報表 · 示範資料</span> <h1 style={titleStyle}>本月營運概覽</h1> <p style={mutedStyle}>先看看完成後的樣子,再換成你的資料。</p> </div> <button style={primaryButtonStyle} onClick={() => setShowSample((value) => !value)} > {showSample ? "換成我的資料" : "恢復示範資料"} </button> </header> <section style={metricGridStyle}> {showSample ? ( sampleRows.map(([label, value, change]) => ( <article key={label} style={panelStyle}> <span style={mutedStyle}>{label}</span> <h2 style={{ fontSize: 30 }}>{value}</h2> <strong style={{ color: change.startsWith("+") ? "#15803d" : "#b45309" }}> {change} </strong> </article> )) ) : ( <article style={{ ...panelStyle, gridColumn: "1 / -1" }}> <h2>這裡會顯示你的資料</h2> <p style={mutedStyle}>此原型不連接 API;資料替換流程尚未實作。</p> </article> )} </section> <section style={panelStyle}> <h2>最近六個月趨勢</h2> <div style={chartStyle}> {[38, 55, 46, 70, 66, 88].map((height, index) => ( <div key={index} style={{ ...barStyle, height: `${height}%` }} title={`${height}`} /> ))} </div> </section> </main> ); } function PrototypeSwitcher({ current }: { current: VariantKey }) { const router = useRouter(); const searchParams = useSearchParams(); const currentIndex = variants.findIndex((item) => item.key === current); const move = useCallback( (offset: number) => { const next = variants[ (currentIndex + offset + variants.length) % variants.length ]; const params = new URLSearchParams(searchParams.toString()); params.set("variant", next.key); router.replace(`/reports?${params.toString()}`, { scroll: false }); }, [currentIndex, router, searchParams], ); useEffect(() => { function handleKeyDown(event: KeyboardEvent) { const target = event.target as HTMLElement; if ( target.matches("input, textarea, select") || target.isContentEditable ) { return; } if (event.key === "ArrowLeft") move(-1); if (event.key === "ArrowRight") move(1); } window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [move]); if (process.env.NODE_ENV === "production") return null; const active = variants[currentIndex]; return ( <div style={switcherStyle} aria-label="報表原型切換列"> <button style={switchButtonStyle} onClick={() => move(-1)} aria-label="上一個"> ← </button> <span> {active.key} · {active.name} </span> <button style={switchButtonStyle} onClick={() => move(1)} aria-label="下一個"> → </button> </div> ); } const pageStyle: CSSProperties = { minHeight: "100vh", padding: "48px clamp(20px, 6vw, 88px) 120px", background: "#f6f7f9", color: "#111827", fontFamily: "Arial, sans-serif", }; const headerStyle: CSSProperties = { display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 24, marginBottom: 40, }; const titleStyle: CSSProperties = { margin: "8px 0", fontSize: "clamp(32px, 5vw, 56px)", lineHeight: 1.05, }; const eyebrowStyle: CSSProperties = { color: "#4f46e5", fontSize: 13, fontWeight: 700, letterSpacing: 1.5, textTransform: "uppercase", }; const mutedStyle: CSSProperties = { color: "#6b7280" }; const panelStyle: CSSProperties = { padding: 24, border: "1px solid #e5e7eb", borderRadius: 18, background: "white", }; const splitStyle: CSSProperties = { display: "grid", gridTemplateColumns: "minmax(0, 2fr) minmax(240px, 1fr)", gap: 24, }; const choiceGridStyle: CSSProperties = { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(210px, 1fr))", gap: 16, }; const choiceStyle: CSSProperties = { display: "grid", gap: 10, padding: 20, textAlign: "left", border: "1px solid #d1d5db", borderRadius: 14, background: "white", cursor: "pointer", }; const templateLayoutStyle: CSSProperties = { display: "grid", gridTemplateColumns: "160px minmax(0, 1fr)", gap: 28, }; const sidebarStyle: CSSProperties = { display: "flex", flexDirection: "column", gap: 8, }; const navButtonStyle: CSSProperties = { padding: "10px 12px", border: 0, borderRadius: 8, background: "transparent", textAlign: "left", cursor: "pointer", }; const previewStyle: CSSProperties = { height: 120, display: "grid", placeItems: "center", borderRadius: 12, background: "#eef2ff", color: "#4f46e5", }; const metricGridStyle: CSSProperties = { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 16, marginBottom: 24, }; const chartStyle: CSSProperties = { height: 240, display: "flex", alignItems: "flex-end", gap: 18, paddingTop: 24, }; const barStyle: CSSProperties = { flex: 1, minWidth: 20, borderRadius: "8px 8px 0 0", background: "#6366f1", }; const primaryButtonStyle: CSSProperties = { padding: "11px 16px", border: 0, borderRadius: 10, background: "#4f46e5", color: "white", cursor: "pointer", }; const secondaryButtonStyle: CSSProperties = { ...primaryButtonStyle, border: "1px solid #d1d5db", background: "white", color: "#111827", }; const badgeStyle: CSSProperties = { padding: "8px 12px", borderRadius: 999, background: "#e0e7ff", color: "#4338ca", }; const stateStyle: CSSProperties = { marginTop: 20, padding: 12, borderRadius: 10, background: "#ecfdf5", color: "#166534", }; const switcherStyle: CSSProperties = { position: "fixed", left: "50%", bottom: 24, zIndex: 100, transform: "translateX(-50%)", display: "flex", alignItems: "center", gap: 16, padding: "8px 12px", borderRadius: 999, background: "#111827", color: "white", boxShadow: "0 12px 30px rgb(0 0 0 / 25%)", }; const switchButtonStyle: CSSProperties = { width: 36, height: 36, border: 0, borderRadius: "50%", background: "#374151", color: "white", cursor: "pointer", }; ```
03
壓力題

辨識模糊原型問題

測試問題

使用者只說『幫我做一個退款流程 prototype』;周邊證據只有 src/refund-state.ts,沒有頁面或元件,也暫時聯絡不到使用者。請說明你選哪一種 prototype 分支、依據、產物形式、如何執行、明確不會做什麼,以及完成後如何保存驗證結論而不把原型混入 main。不要假裝已建立檔案。

載入前 · 0 個 Skill載入後 · 版本已核對再次確認 · 0 個 Skill已確認模型套用 Skill三個環境皆已清理
未載入 Skill1650 tokens · 45709 ms
## Prototype 決策 選擇「流程/狀態機 prototype」,不做高擬真 UI。 依據: - 唯一已知證據是 `src/refund-state.ts`,最可靠的驗證目標是退款狀態、事件、轉移條件與例外路徑。 - 沒有既有頁面、元件或設計規範;直接製作 UI 會大量臆測版型、文案和操作模式。 - 目前工作區實際為空,未找到該檔案、`package.json` 或 Git 資料,因此尚無法確認狀態定義及專案工具鏈。 ## 預定產物 在獨立分支 `prototype/refund-flow-state-machine` 中建立: - 一個最小可操作流程殼層,呈現目前狀態、合法操作、操作結果與錯誤原因。 - 直接引用 `src/refund-state.ts`,不複製或另造退款規則。 - 覆蓋正常退款、拒絕/失敗、重試、取消等路徑;實際路徑以該檔案內容為準。 - 一份驗證紀錄,列出已驗證路徑、發現、未決問題與假設。 - 必要時附截圖或測試輸出,但不把它們視為正式產品設計。 產物是可丟棄的功能探針,不是 production-ready 頁面。 ## 執行方式 待原始 repository 與 `src/refund-state.ts` 可用後,先沿用專案既有 package manager、測試框架和啟動方式,並提供統一入口: ```sh npm run prototype:refund ``` 若專案使用 pnpm 或 yarn,則改為相應命令。由於目前沒有 `package.json`,這只是預定的執行介面,不能聲稱現在可執行。 ## 明確不會做 - 不會猜測 `refund-state.ts` 中不存在或尚未讀到的狀態與商業規則。 - 不會串接真實付款、退款 API、資料庫或客戶資料。 - 不會建立正式路由、正式頁面、共用元件或設計系統。 - 不會自行決定權限、退款期限、金額上限、通知或稽核政策。 - 不會把 prototype 合併、cherry-pick 或複製進 `main`。 - 在檔案缺失且環境唯讀的現況下,不會聲稱已建立或驗證任何檔案。 ## 保存驗證結論 - 使用獨立 worktree 與 `prototype/refund-flow-state-machine` 分支開發。 - 將原型、驗證紀錄及執行證據提交在該分支。 - 建立不合併的 Draft PR,於 PR 描述保存結論、限制、假設及後續產品決策。 - 驗證完成後保留分支或加上 `refund-prototype-validated-YYYYMMDD` 標籤;關閉 Draft PR,但不 merge。 - 若結論需要進入正式開發,另開新分支,僅依結論重新實作;不直接沿用原型程式碼。
已載入 Skill +1412 tokens · 38353 ms
選擇「邏輯/狀態模型 prototype」分支。 依據是題目提供的唯一周邊證據為 `src/refund-state.ts`,而沒有頁面或元件;因此合理假設目前要驗證的是退款狀態、合法轉移與例外情境,而不是視覺設計。由於我未取得該檔案內容,具體狀態與操作仍須以實際程式碼為準。 產物規劃為放在相關模組旁、名稱明確標示為原型的單一自包含 HTML,例如 `src/refund-state.prototype.html`。內容應包括: - 頂端明示上述假設及待回答的問題。 - 與 DOM 分離的純 reducer 或狀態機。 - 每次操作後呈現完整退款狀態。 - 可任意操作的按鈕。 - 分頁式引導情境:正常退款、棘手邊界案例、非法操作。 - 全部資料只存在記憶體中。 執行方式是不安裝套件、不啟動伺服器,直接雙擊 HTML,或用瀏覽器開啟: ```text open src/refund-state.prototype.html ``` 明確不會做: - 不臆測或設計正式退款 UI。 - 不新增正式頁面、路由或元件。 - 不接真實 API、付款系統、資料庫或持久化。 - 不引入框架、bundler、抽象層、完整錯誤處理或測試。 - 不把 prototype HTML 當成產品程式碼。 - 在取得驗證結論前,不修改正式 `refund-state` 邏輯。 - 不直接把原型合併進 `main`。 完成後,應在獨立 throwaway branch(例如 `prototype/refund-state-flow`)提交原型。驗證完成時,把「驗證的問題、操作情境、觀察結果、最終結論」記錄在 implementation issue 或該分支的提交中,並由 issue 留下 branch/commit 指標。只有經確認的狀態模型決策才另行實作進正式程式碼並合入 `main`;原型本身繼續留在 throwaway branch,作為可重跑的第一手驗證材料。 以上僅是分支選擇與產物規劃;未建立或修改任何 prototype 檔案。
測試紀錄

這次測試記錄了什麼?

固定不變兩邊使用相同的模型版本、帳號方案、系統指令、思考強度與測試問題。

分開測試每次回答都使用全新環境。載入前不含任何 Skill,載入後只含本頁指定的 Skill。

完整才公開三題的所有步驟與環境清理都成功後,才顯示結果。若無法確認模型是否套用 Skill,也會清楚標示。