import type { PlanSource } from "../shared/types.js"; export type VisualQuestionsOption = { value?: string; label: string; description?: string; recommended?: boolean; preview?: "desktop" | "mobile" | "split" | "flow" | "diagram"; bullets?: string[]; }; export type VisualQuestion = { id: string; type: "single" | "multi" | "freeform" | "visual"; title: string; subtitle?: string; options?: VisualQuestionsOption[]; allowOther?: boolean; placeholder?: string; required?: boolean; }; export type BuildVisualQuestionsHtmlInput = { title: string; brief: string; source?: PlanSource; repoPath?: string; questions?: VisualQuestion[]; }; function escapeHtml(value: unknown): string { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function safeJson(value: unknown): string { return JSON.stringify(value).replace(/ ${escapeHtml(title)}

Visual intake

${escapeHtml(title)}

${questions.map(renderQuestion).join("\n")}

Generated handoff

Answer summary for the planning agent

This prompt can feed the next UI plan or visual plan step.

`; } function renderQuestion(question: VisualQuestion, index: number): string { const id = escapeHtml(question.id); const subtitle = question.subtitle ? `

${escapeHtml(question.subtitle)}

` : ""; return `
${index + 1}

${escapeHtml(question.title)}

${subtitle}
${renderQuestionControl(question)}
`; } function renderQuestionControl(question: VisualQuestion): string { if (question.type === "freeform") { return ``; } if (question.type === "visual") { return renderVisualChoice(question); } return renderChipChoices(question); } // Multiple-choice questions always offer a write-in answer unless an author // explicitly opts out with `allowOther: false`, so a reviewer can give a custom // response instead of the listed options. function renderWriteIn(question: VisualQuestion): string { if (question.allowOther === false) return ""; const placeholder = escapeHtml( question.placeholder ?? "Other — type your own answer…", ); return ``; } function renderChipChoices(question: VisualQuestion): string { const options = question.options ?? []; const other = renderWriteIn(question); return `
${options .map((option) => { const value = optionValue(option); return ``; }) .join("\n")} ${other}
`; } function renderVisualChoice(question: VisualQuestion): string { const options = question.options ?? []; return `
${options .map((option, index) => { const value = optionValue(option); return `
${renderBullets(option.bullets)}
`; }) .join("\n")} ${renderWriteIn(question)}
`; } function renderBullets(bullets: string[] | undefined): string { if (!bullets?.length) return ""; return ``; } function renderPreview(option: VisualQuestionsOption, index: number): string { const preview = option.preview ?? (index % 2 === 0 ? "desktop" : "mobile"); if (preview === "mobile") { return ``; } if (preview === "split") { return ``; } if (preview === "flow") { return ``; } if (preview === "diagram") { return ``; } return ``; } const VISUAL_QUESTIONS_CSS = ` @font-face { font-family: "Excalifont"; src: url("/fonts/Excalifont-Regular.woff2") format("woff2"); font-weight: 400; font-style: normal; font-display: swap; } :root { color-scheme: light dark; --bg: #faf9f7; --paper: #ffffff; --paper-soft: #f3f2ef; --canvas: #f3f2ef; --ink: #181817; --muted: #6f6e68; --line: #dfded9; --line-strong: #c9c8c2; --accent: #2f6fed; --accent-soft: rgba(47,111,237,.1); --sketch: #20201e; --wire-surface: #ffffff; --wire-soft: #f4f4f5; --wire-mark: #d4d4d8; --wire-line: #20201e; --wire-line-soft: #c9c8c2; --shadow: none; } :root[data-agent-native-theme="dark"] { color-scheme: dark; --bg: #1f1e1d; --paper: #242423; --paper-soft: #2b2a2a; --canvas: #1d1c1b; --ink: #f4f3ef; --muted: #aaa8a4; --line: #444341; --line-strong: #5a5955; --accent: #4d86ff; --accent-soft: rgba(77,134,255,.18); --sketch: #f0eee8; --wire-surface: #202020; --wire-soft: #2a2a2a; --wire-mark: #686868; --wire-line: rgba(244,244,242,.78); --wire-line-soft: rgba(244,244,242,.26); --shadow: none; } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--ink); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.55; } button, input, textarea { font: inherit; } .roughen-svg { position: absolute; width: 0; height: 0; overflow: hidden; } .vq-shell { width: min(1040px, calc(100vw - 96px)); margin: 0 auto; padding: 60px 0 132px; } .vq-cover { max-width: 700px; } .vq-kicker, .vq-eyebrow { margin: 0 0 12px; color: var(--muted); font-size: 12px; font-weight: 750; letter-spacing: .12em; text-transform: uppercase; } h1 { margin: 0; font-size: clamp(30px, 4vw, 52px); line-height: 1.05; letter-spacing: -.03em; } .vq-form { display: grid; gap: 46px; margin-top: 48px; } .vq-question { display: grid; gap: 17px; } .vq-question-heading { display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 14px; align-items: start; } .vq-question-heading > span { display: inline-flex; width: 18px; height: 18px; margin-top: 4px; align-items: center; justify-content: center; border-radius: 99px; background: transparent; color: var(--muted); font-size: 11px; font-weight: 650; opacity: .62; } .vq-question h2 { margin: 0; font-size: clamp(21px, 2.2vw, 28px); letter-spacing: -.02em; line-height: 1.16; } .vq-question p { max-width: 680px; margin: 6px 0 0; color: var(--muted); font-size: 15px; } .vq-chip-cloud { display: flex; flex-wrap: wrap; gap: 12px; padding-left: 34px; } .vq-chip, .vq-secondary, .vq-primary { min-height: 40px; border: 1px solid var(--line); border-radius: 999px; background: var(--paper); color: var(--ink); padding: 0 18px; font-weight: 650; cursor: pointer; transition: border-color .16s ease, background .16s ease, color .16s ease, transform .16s ease; } .vq-chip:hover { border-color: var(--line-strong); background: var(--paper-soft); } .vq-chip.is-selected { border-color: var(--accent); background: var(--accent-soft); color: var(--ink); } .vq-chip em { margin-left: 8px; color: var(--accent); font-style: normal; font-size: 11px; text-transform: uppercase; letter-spacing: .08em; } .vq-other { min-height: 40px; width: min(280px, 100%); border: 1px solid var(--line); border-radius: 999px; background: var(--paper); color: var(--ink); padding: 0 18px; outline: none; } .vq-other:focus, .vq-textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .vq-textarea { min-height: 120px; width: calc(100% - 34px); margin-left: 34px; resize: vertical; border: 1px solid var(--line); border-radius: 10px; background: var(--paper); color: var(--ink); padding: 16px 18px; outline: none; } .vq-visual-options { display: grid; gap: 34px; padding-left: 34px; } .vq-visual-option { display: grid; grid-template-columns: minmax(280px, 520px) minmax(220px, 1fr); gap: 24px; align-items: start; } .vq-visual-option + .vq-visual-option { padding-top: 34px; border-top: 1px solid color-mix(in srgb, var(--line) 68%, transparent); } .vq-visual-card { display: grid; gap: 14px; width: 100%; border: 0; border-radius: 0; background: transparent; color: var(--ink); padding: 0; text-align: left; cursor: pointer; box-shadow: none; } .vq-visual-card:hover .vq-visual-title strong { color: var(--accent); } .vq-visual-card:focus-visible { outline: 0; } .vq-visual-card:focus-visible .vq-preview { outline: 2px solid var(--accent); outline-offset: 4px; } .vq-visual-card.is-selected .vq-visual-title span { border-color: var(--accent); background: var(--accent); box-shadow: inset 0 0 0 4px var(--paper); } .vq-visual-card.is-selected .vq-preview { outline: 2px solid var(--accent-soft); outline-offset: 4px; } .vq-visual-copy { display: grid; gap: 4px; } .vq-visual-title { display: flex; align-items: center; gap: 9px; color: var(--ink); } .vq-visual-title span { width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid var(--line-strong); border-radius: 99px; background: transparent; transition: border-color .16s ease, background .16s ease, box-shadow .16s ease; } .vq-visual-copy strong { font-size: 18px; } .vq-visual-copy > span:not(.vq-visual-title), .vq-bullets { color: var(--muted); } .vq-bullets { margin: 26px 0 0; padding-left: 20px; font-size: 15px; } .vq-preview { position: relative; display: block; min-height: 190px; overflow: hidden; border: 1.5px solid var(--wire-line); border-radius: 7px; background: var(--wire-surface); filter: url(#visual-questions-roughen); } .vq-preview::after, .vq-preview-flow b::after, .vq-preview-diagram b::after { content: ""; position: absolute; inset: -2px; border: 1px solid var(--wire-line-soft); border-radius: inherit; transform: translate(2px, -1px) rotate(.18deg); pointer-events: none; } .vq-preview-desktop i, .vq-preview-split i { position: absolute; inset: 0 auto 0 0; width: 26%; border-right: 1px solid var(--wire-line-soft); background: var(--wire-soft); } .vq-preview-desktop b, .vq-preview-split b, .vq-preview-mobile b { position: absolute; left: 32%; right: 8%; height: 12px; border-radius: 99px; background: var(--wire-mark); opacity: .72; } .vq-preview-desktop b:nth-of-type(1), .vq-preview-split b:nth-of-type(1), .vq-preview-mobile b:nth-of-type(1) { top: 46px; } .vq-preview-desktop b:nth-of-type(2), .vq-preview-split b:nth-of-type(2), .vq-preview-mobile b:nth-of-type(2) { top: 82px; right: 22%; } .vq-preview-desktop b:nth-of-type(3), .vq-preview-split b:nth-of-type(3), .vq-preview-mobile b:nth-of-type(3) { top: 118px; right: 34%; } .vq-preview-desktop strong, .vq-preview-split strong { position: absolute; left: 32%; right: 8%; bottom: 18px; height: 34px; border: 1.4px solid var(--accent); border-radius: 7px; } .vq-preview-split i { inset: 0 0 auto 0; width: auto; height: 38px; border-right: 0; border-bottom: 1px solid var(--wire-line-soft); } .vq-preview-split b { left: 8%; } .vq-preview-mobile { width: min(210px, 100%); margin: 0 auto; border-radius: 28px; min-height: 230px; } .vq-preview-mobile i { position: absolute; left: 50%; top: 12px; width: 42px; height: 5px; border-radius: 99px; background: var(--wire-line-soft); transform: translateX(-50%); } .vq-preview-mobile b { left: 18%; right: 18%; } .vq-preview-mobile em { position: absolute; right: 18px; bottom: 18px; width: 42px; height: 42px; border-radius: 99px; background: var(--accent); } .vq-preview-flow { display: flex; min-height: 128px; align-items: center; justify-content: center; gap: 14px; background: var(--wire-surface); border-style: solid; } .vq-preview-flow b { position: relative; display: inline-flex; width: 54px; height: 54px; align-items: center; justify-content: center; border-radius: 7px; background: var(--wire-surface); border: 1.5px solid var(--wire-line); color: var(--accent); font: 400 24px/1 Excalifont, ui-sans-serif, system-ui; filter: url(#visual-questions-roughen); } .vq-preview-flow i { width: 54px; border-top: 2px solid var(--accent); filter: url(#visual-questions-roughen); } .vq-preview-diagram b { position: absolute; width: 118px; height: 56px; border: 1.5px solid var(--wire-line); border-radius: 7px; background: var(--wire-surface); filter: url(#visual-questions-roughen); } .vq-preview-diagram b:nth-of-type(1) { left: 32px; top: 32px; } .vq-preview-diagram b:nth-of-type(2) { right: 38px; top: 42px; } .vq-preview-diagram b:nth-of-type(3) { left: 42%; bottom: 28px; } .vq-preview-diagram i { position: absolute; width: 120px; border-top: 2px dashed var(--accent); transform: rotate(12deg); } .vq-preview-diagram i:nth-of-type(1) { left: 140px; top: 78px; } .vq-preview-diagram i:nth-of-type(2) { right: 132px; bottom: 74px; transform: rotate(-18deg); } .vq-summary { display: grid; grid-template-columns: minmax(220px, 320px) minmax(0, 1fr); gap: 36px; margin-top: 76px; padding-top: 42px; border-top: 1px solid var(--line); } .vq-summary h2 { margin: 0; font-size: clamp(24px, 3vw, 34px); letter-spacing: -.024em; } .vq-summary p { color: var(--muted); } #visual-questions-summary { min-height: 300px; border: 1px solid var(--line); border-radius: 10px; background: var(--paper-soft); color: var(--ink); padding: 18px; font: 14px/1.55 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; resize: vertical; } .vq-action-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 10; display: flex; align-items: center; justify-content: flex-end; gap: 12px; border-top: 1px solid var(--line); background: color-mix(in srgb, var(--bg) 92%, transparent); padding: 12px max(22px, calc((100vw - 1120px) / 2)); backdrop-filter: blur(16px); } .vq-action-bar div { display: grid; gap: 2px; margin-right: auto; color: var(--muted); font-size: 13px; } .vq-action-bar strong { color: var(--ink); } .vq-secondary:hover, .vq-primary:hover { transform: translateY(-1px); } .vq-primary { border-color: var(--accent); background: var(--accent); color: white; } @media (max-width: 760px) { .vq-shell { width: min(100vw - 28px, 1040px); padding-top: 44px; } .vq-summary, .vq-visual-option { grid-template-columns: 1fr; } .vq-chip-cloud, .vq-visual-options, .vq-textarea { margin-left: 0; padding-left: 0; width: 100%; } .vq-question-heading { grid-template-columns: 1fr; } .vq-action-bar { align-items: stretch; flex-wrap: wrap; } .vq-action-bar div { flex: 1 1 100%; } } `; const VISUAL_QUESTIONS_SCRIPT = ` (function () { const config = window.__VISUAL_QUESTIONS__ || { questions: [] }; const state = { answers: {}, activeVisual: {} }; function normalize(value) { return String(value || "").replace(/\\s+/g, " ").trim(); } function attr(value) { return String(value || "").replace(/\\\\/g, "\\\\\\\\").replace(/"/g, "\\\""); } function optionValue(option) { return option.value || String(option.label || "option").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "option"; } function defaultAnswer(question) { if (question.type === "multi") return { values: [], other: "" }; if (question.type === "freeform") return ""; return { value: "", other: "" }; } for (const question of config.questions) { state.answers[question.id] = defaultAnswer(question); if (question.type === "visual" && question.options?.[0]) { state.activeVisual[question.id] = optionValue(question.options[0]); } } function labelFor(question, value) { if (value === "other") return "Other"; const option = (question.options || []).find((item) => optionValue(item) === value); return option?.label || value; } function answerText(question) { const answer = state.answers[question.id]; if (question.type === "freeform") return normalize(answer) || "No answer yet"; if (question.type === "multi") { const labels = (answer.values || []).map((value) => labelFor(question, value)); if (answer.other) labels.push("Other: " + answer.other); return labels.length ? labels.join(", ") : "No answer yet"; } if (answer.value === "other" && answer.other) return "Other: " + answer.other; return answer.value ? labelFor(question, answer.value) : "No answer yet"; } function isAnswered(question) { const answer = state.answers[question.id]; if (question.type === "freeform") return Boolean(normalize(answer)); if (question.type === "multi") return Boolean((answer.values || []).length || normalize(answer.other)); return Boolean(answer.value && (answer.value !== "other" || normalize(answer.other))); } function serializedAnswers() { return config.questions.map((question) => ({ id: question.id, title: question.title, type: question.type, answer: answerText(question), raw: state.answers[question.id] })); } function buildPrompt() { const lines = [ "Use these visual intake answers to create or refine a UI-first visual plan.", "", "Project: " + config.title, "Brief: " + config.brief, config.source ? "Source: " + config.source : "", config.repoPath ? "Repo: " + config.repoPath : "", "", "Answers:" ].filter(Boolean); for (const question of config.questions) { lines.push("- " + question.title + ": " + answerText(question)); } lines.push( "", "Next step: create a visual/UI plan from these answers. Prefer a top pan/zoom wireframe canvas when screens, flows, diagrams, or visual options are useful, then continue with a restrained Notion-like document. Preserve explicit answers; make reasonable choices for anything unanswered." ); return lines.join("\\n"); } function setVisualPanel(questionId, value) { const scope = document.querySelector('[data-question-id="' + attr(questionId) + '"]'); if (!scope) return; state.activeVisual[questionId] = value; for (const tab of scope.querySelectorAll("[data-visual-tab]")) { const selected = tab.getAttribute("data-visual-tab") === value; tab.classList.toggle("is-active", selected); tab.setAttribute("aria-selected", String(selected)); } for (const panel of scope.querySelectorAll("[data-visual-panel]")) { const selected = panel.getAttribute("data-visual-panel") === value; panel.classList.toggle("is-active", selected); panel.hidden = !selected; } } function updateUi() { for (const question of config.questions) { const scope = document.querySelector('[data-question-id="' + attr(question.id) + '"]'); if (!scope) continue; const answer = state.answers[question.id]; for (const choice of scope.querySelectorAll("[data-choice]")) { const value = choice.getAttribute("data-choice"); const selected = question.type === "multi" ? (answer.values || []).includes(value) : answer.value === value; choice.classList.toggle("is-selected", selected); if (choice.getAttribute("role") === "radio") { choice.setAttribute("aria-checked", String(selected)); } else { choice.setAttribute("aria-pressed", String(selected)); } } } const answered = config.questions.filter(isAnswered).length; const progress = document.getElementById("visual-questions-progress"); if (progress) progress.textContent = answered + "/" + config.questions.length + " answered"; const summary = document.getElementById("visual-questions-summary"); if (summary) summary.value = buildPrompt(); } document.addEventListener("click", (event) => { const tab = event.target instanceof Element ? event.target.closest("[data-visual-tab]") : null; if (tab) { const question = tab.closest("[data-question-id]"); const questionId = question?.getAttribute("data-question-id"); const value = tab.getAttribute("data-visual-tab"); if (questionId && value) setVisualPanel(questionId, value); return; } const choice = event.target instanceof Element ? event.target.closest("[data-choice]") : null; if (!choice) return; const scope = choice.closest("[data-question-id]"); const questionId = scope?.getAttribute("data-question-id"); const question = config.questions.find((item) => item.id === questionId); const value = choice.getAttribute("data-choice"); if (!question || !value) return; if (question.type === "multi") { const values = state.answers[question.id].values; const index = values.indexOf(value); if (index >= 0) values.splice(index, 1); else values.push(value); } else { state.answers[question.id].value = value; if (question.type === "visual") setVisualPanel(question.id, value); if (value !== "other") state.answers[question.id].other = ""; } updateUi(); }); document.addEventListener("input", (event) => { const input = event.target instanceof Element ? event.target : null; const scope = input?.closest("[data-question-id]"); const questionId = scope?.getAttribute("data-question-id"); const question = config.questions.find((item) => item.id === questionId); if (!question) return; if (input.matches("[data-freeform-input]")) { state.answers[question.id] = input.value; } if (input.matches("[data-other-input]")) { if (question.type === "multi") { state.answers[question.id].other = input.value.trim(); } else { state.answers[question.id].value = "other"; state.answers[question.id].other = input.value.trim(); } } updateUi(); }); function parentOrigin() { const explicit = typeof window.__agentNativePlanParentOrigin === "string" ? window.__agentNativePlanParentOrigin : ""; if (explicit && explicit !== "null") return explicit; try { const referrerOrigin = new URL(document.referrer).origin; return referrerOrigin && referrerOrigin !== "null" ? referrerOrigin : ""; } catch { return ""; } } function post(type) { const targetOrigin = parentOrigin(); if (!targetOrigin) return; window.parent.postMessage({ type, title: config.title, summary: buildPrompt(), answers: serializedAnswers() }, targetOrigin); } document.querySelector("[data-vq-copy]")?.addEventListener("click", () => post("agent-native-visual-questions-copy")); document.querySelector("[data-vq-send]")?.addEventListener("click", () => post("agent-native-visual-questions-send-to-agent")); updateUi(); })(); `;