import { QuestionConfig } from '../types.js';
export function generateQuestionUI(config: QuestionConfig): string {
const { question, inputType, options = [], placeholder = '', questionId = '' } = config;
const renderOptions = () => {
if (inputType === 'text') {
return `
<div class="input-section">
<textarea
id="textInput"
placeholder="${placeholder || 'Type your answer...'}"
rows="4"
></textarea>
</div>
`;
}
if (inputType === 'select') {
return `
<div class="options-section">
${options
.map(
(opt, idx) => `
<button class="option-btn" data-value="${opt.value}" data-index="${idx}">
<span class="option-label">${opt.label}</span>
${opt.description ? `<span class="option-desc">${opt.description}</span>` : ''}
</button>
`,
)
.join('')}
</div>
`;
}
if (inputType === 'multiselect') {
return `
<div class="options-section multiselect">
${options
.map(
(opt, idx) => `
<label class="option-checkbox" data-value="${opt.value}" data-index="${idx}">
<input type="checkbox" value="${opt.value}" />
<span class="checkmark"></span>
<span class="option-content">
<span class="option-label">${opt.label}</span>
${opt.description ? `<span class="option-desc">${opt.description}</span>` : ''}
</span>
</label>
`,
)
.join('')}
</div>
<button id="submitMulti" class="submit-btn">Submit Selection</button>
`;
}
return `
<div class="confirm-section">
<button class="confirm-btn yes" data-value="yes">
<span class="icon">✓</span>
<span>Yes</span>
</button>
<button class="confirm-btn no" data-value="no">
<span class="icon">✕</span>
<span>No</span>
</button>
</div>
`;
};
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-size-text: 14px;
--color-text: #1a1a1a;
--color-text-secondary: #666;
--color-background: #ffffff;
--color-surface: #f5f5f5;
--color-primary: #0066cc;
--color-primary-hover: #0052a3;
--color-border: #e0e0e0;
--color-success: #22c55e;
--color-danger: #ef4444;
--radius: 8px;
--spacing: 12px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-text: #f0f0f0;
--color-text-secondary: #a0a0a0;
--color-background: #1a1a1a;
--color-surface: #2a2a2a;
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-border: #3a3a3a;
}
}
body {
font-family: var(--font-family);
font-size: var(--font-size-text);
color: var(--color-text);
background: var(--color-background);
padding: var(--spacing);
line-height: 1.5;
}
.container {
max-width: 600px;
}
.question {
font-size: 16px;
font-weight: 600;
margin-bottom: var(--spacing);
color: var(--color-text);
}
.input-section textarea {
width: 100%;
padding: var(--spacing);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text);
font-family: inherit;
font-size: inherit;
resize: vertical;
min-height: 100px;
}
.input-section textarea:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.input-section textarea::placeholder {
color: var(--color-text-secondary);
}
.options-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.option-btn {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: var(--spacing);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text);
cursor: pointer;
transition: all 0.15s ease;
text-align: left;
width: 100%;
}
.option-btn:hover {
border-color: var(--color-primary);
background: var(--color-background);
}
.option-btn.selected {
border-color: var(--color-primary);
background: rgba(59, 130, 246, 0.1);
}
.option-label {
font-weight: 500;
}
.option-desc {
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 2px;
}
.option-checkbox {
display: flex;
align-items: flex-start;
padding: var(--spacing);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
cursor: pointer;
transition: all 0.15s ease;
gap: 12px;
}
.option-checkbox:hover {
border-color: var(--color-primary);
}
.option-checkbox input {
display: none;
}
.checkmark {
width: 20px;
height: 20px;
border: 2px solid var(--color-border);
border-radius: 4px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
margin-top: 2px;
}
.checkmark::after {
content: '✓';
color: white;
font-size: 12px;
font-weight: bold;
opacity: 0;
transition: opacity 0.15s ease;
}
.option-checkbox input:checked + .checkmark {
background: var(--color-primary);
border-color: var(--color-primary);
}
.option-checkbox input:checked + .checkmark::after {
opacity: 1;
}
.option-content {
display: flex;
flex-direction: column;
}
.confirm-section {
display: flex;
gap: var(--spacing);
}
.confirm-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
border: 2px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text);
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.confirm-btn .icon {
font-size: 20px;
}
.confirm-btn.yes:hover {
border-color: var(--color-success);
background: rgba(34, 197, 94, 0.1);
color: var(--color-success);
}
.confirm-btn.no:hover {
border-color: var(--color-danger);
background: rgba(239, 68, 68, 0.1);
color: var(--color-danger);
}
.submit-btn {
margin-top: var(--spacing);
width: 100%;
padding: var(--spacing) 20px;
border: none;
border-radius: var(--radius);
background: var(--color-primary);
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease;
}
.submit-btn:hover {
background: var(--color-primary-hover);
}
.submit-btn:disabled {
background: var(--color-border);
cursor: not-allowed;
}
#textSubmit {
margin-top: var(--spacing);
}
.sent-message {
padding: var(--spacing);
background: rgba(34, 197, 94, 0.1);
border: 1px solid var(--color-success);
border-radius: var(--radius);
color: var(--color-success);
text-align: center;
font-weight: 500;
}
</style>
</head>
<body>
<div class="container">
<div class="question">${question}</div>
<div id="answerSection">
${renderOptions()}
${inputType === 'text' ? '<button id="textSubmit" class="submit-btn">Submit</button>' : ''}
</div>
</div>
<script>
const inputType = "${inputType}";
const questionId = "${questionId}";
let initialized = false;
let pendingRequests = new Map();
let nextId = 1;
let hostContext = null;
function sendRequest(method, params) {
const id = nextId++;
return new Promise((resolve, reject) => {
pendingRequests.set(id, { resolve, reject });
window.parent.postMessage({ jsonrpc: '2.0', id, method, params: params || {} }, '*');
});
}
function sendNotification(method, params) {
window.parent.postMessage({ jsonrpc: '2.0', method, params: params || {} }, '*');
}
async function initialize() {
if (initialized) return;
try {
const result = await sendRequest('ui/initialize', {
protocolVersion: '2026-01-26',
appInfo: { name: 'ask-question-mcp', version: '1.0.0' },
appCapabilities: {}
});
hostContext = result.hostContext || {};
if (hostContext.theme) applyTheme(hostContext.theme);
if (hostContext.styles?.variables) applyStyles(hostContext.styles.variables);
sendNotification('ui/notifications/initialized', {});
initialized = true;
} catch (e) {
console.error('Init failed:', e);
}
}
async function submitAnswer(answer) {
await initialize();
try {
await sendRequest('ui/invoke-tool', {
name: 'submit_answer',
arguments: {
questionId: questionId,
answer: answer
}
});
showSentConfirmation();
} catch (e) {
console.error('Submit failed:', e);
await sendRequest('ui/message', {
role: 'user',
content: [{ type: 'text', text: answer }]
});
showSentConfirmation();
}
}
function showSentConfirmation() {
const section = document.getElementById('answerSection');
section.innerHTML = '<div class="sent-message">Answer sent!</div>';
}
function applyTheme(theme) {
if (theme === 'dark') {
document.documentElement.style.setProperty('--color-text', '#f0f0f0');
document.documentElement.style.setProperty('--color-text-secondary', '#a0a0a0');
document.documentElement.style.setProperty('--color-background', '#1a1a1a');
document.documentElement.style.setProperty('--color-surface', '#2a2a2a');
document.documentElement.style.setProperty('--color-primary', '#3b82f6');
document.documentElement.style.setProperty('--color-primary-hover', '#2563eb');
document.documentElement.style.setProperty('--color-border', '#3a3a3a');
} else {
document.documentElement.style.setProperty('--color-text', '#1a1a1a');
document.documentElement.style.setProperty('--color-text-secondary', '#666');
document.documentElement.style.setProperty('--color-background', '#ffffff');
document.documentElement.style.setProperty('--color-surface', '#f5f5f5');
document.documentElement.style.setProperty('--color-primary', '#0066cc');
document.documentElement.style.setProperty('--color-primary-hover', '#0052a3');
document.documentElement.style.setProperty('--color-border', '#e0e0e0');
}
}
function applyStyles(variables) {
if (!variables) return;
const root = document.documentElement;
Object.entries(variables).forEach(([key, value]) => {
root.style.setProperty(key, value);
});
}
window.addEventListener('message', (event) => {
const msg = event.data;
if (!msg?.jsonrpc) return;
if (msg.id !== undefined && pendingRequests.has(msg.id)) {
const pending = pendingRequests.get(msg.id);
pendingRequests.delete(msg.id);
if (msg.error) {
pending.reject(new Error(msg.error.message));
} else {
pending.resolve(msg.result);
}
return;
}
if (msg.method === 'ui/notifications/host-context-changed') {
const ctx = msg.params || {};
hostContext = { ...hostContext, ...ctx };
if (ctx.theme) applyTheme(ctx.theme);
if (ctx.styles?.variables) applyStyles(ctx.styles.variables);
}
if (msg.method === 'ui/notifications/tool-input') {
console.log('Tool input received:', msg.params);
}
});
document.addEventListener('DOMContentLoaded', () => {
initialize();
if (inputType === 'text') {
const textarea = document.getElementById('textInput');
const submitBtn = document.getElementById('textSubmit');
submitBtn.addEventListener('click', () => {
const value = textarea.value.trim();
if (value) {
submitAnswer(value);
}
});
textarea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
const value = textarea.value.trim();
if (value) {
submitAnswer(value);
}
}
});
}
if (inputType === 'select') {
document.querySelectorAll('.option-btn').forEach(btn => {
btn.addEventListener('click', () => {
const value = btn.dataset.value;
const label = btn.querySelector('.option-label').textContent;
submitAnswer(label);
});
});
}
if (inputType === 'multiselect') {
const submitBtn = document.getElementById('submitMulti');
submitBtn.addEventListener('click', () => {
const checked = document.querySelectorAll('.option-checkbox input:checked');
if (checked.length > 0) {
const values = Array.from(checked).map(input => {
const label = input.closest('.option-checkbox').querySelector('.option-label').textContent;
return label;
});
submitAnswer(values.join(', '));
}
});
}
if (inputType === 'confirm') {
document.querySelectorAll('.confirm-btn').forEach(btn => {
btn.addEventListener('click', () => {
const value = btn.dataset.value;
submitAnswer(value === 'yes' ? 'Yes' : 'No');
});
});
}
});
</script>
</body>
</html>`;
}