Update dashboard to render rich multi-option prompts
This commit is contained in:
parent
5404598a5e
commit
d3fac77f6f
1 changed files with 390 additions and 8 deletions
|
|
@ -144,7 +144,7 @@
|
||||||
|
|
||||||
.prompt {
|
.prompt {
|
||||||
background: #2a2a2a;
|
background: #2a2a2a;
|
||||||
border: 1px solid #ff9800;
|
border: 2px solid #ff9800;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
@ -156,6 +156,93 @@
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prompt-header {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-tool {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-tool-name {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-tool-input {
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: "SF Mono", Monaco, "Cascadia Code", "Courier New", monospace;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-question {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-options {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
min-height: 48px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-option:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-option input[type="radio"],
|
||||||
|
.prompt-option input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-option-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-other-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-other-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
.prompt-actions {
|
.prompt-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
@ -169,7 +256,7 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-height: 44px;
|
min-height: 48px;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,6 +264,11 @@
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-approve {
|
.btn-approve {
|
||||||
background: #4CAF50;
|
background: #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
@ -187,6 +279,34 @@
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #3a3a3a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary[data-tooltip]:hover::after {
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 32px 16px;
|
padding: 32px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -208,7 +328,7 @@
|
||||||
.session,
|
.session,
|
||||||
.prompt {
|
.prompt {
|
||||||
background: white;
|
background: white;
|
||||||
border-color: #e0e0e0;
|
border-color: #ff9800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-output {
|
.session-output {
|
||||||
|
|
@ -219,6 +339,24 @@
|
||||||
.terminal {
|
.terminal {
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prompt-option {
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-option:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-other-input {
|
||||||
|
background: white;
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-tool-input {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -328,6 +466,7 @@
|
||||||
id: data.prompt_id,
|
id: data.prompt_id,
|
||||||
session_id: data.session_id,
|
session_id: data.session_id,
|
||||||
text: data.prompt_text,
|
text: data.prompt_text,
|
||||||
|
json: data.prompt_json ? JSON.parse(data.prompt_json) : null,
|
||||||
});
|
});
|
||||||
renderPrompts();
|
renderPrompts();
|
||||||
});
|
});
|
||||||
|
|
@ -397,17 +536,260 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$prompts.innerHTML = Array.from(state.prompts.values()).map(p => `
|
$prompts.innerHTML = Array.from(state.prompts.values()).map(p => {
|
||||||
|
if (p.json) {
|
||||||
|
return renderRichPrompt(p);
|
||||||
|
} else {
|
||||||
|
// Fallback for old-style prompts
|
||||||
|
return `
|
||||||
|
<div class="prompt" data-prompt="${p.id}">
|
||||||
|
<div class="prompt-text">${escapeHtml(p.text)}</div>
|
||||||
|
<div class="prompt-actions">
|
||||||
|
<button class="btn btn-approve" onclick="respondToPrompt('${p.id}', 'approve')">Approve</button>
|
||||||
|
<button class="btn btn-reject" onclick="respondToPrompt('${p.id}', 'reject')">Reject</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRichPrompt(p) {
|
||||||
|
const json = p.json;
|
||||||
|
const promptType = json.prompt_type;
|
||||||
|
|
||||||
|
if (promptType === 'permission') {
|
||||||
|
return renderPermissionPrompt(p);
|
||||||
|
} else if (promptType === 'question') {
|
||||||
|
return renderQuestionPrompt(p);
|
||||||
|
} else if (promptType === 'exit_plan') {
|
||||||
|
return renderExitPlanPrompt(p);
|
||||||
|
} else {
|
||||||
|
// Unknown type, fallback
|
||||||
|
return `
|
||||||
|
<div class="prompt" data-prompt="${p.id}">
|
||||||
|
<div class="prompt-text">${escapeHtml(p.text)}</div>
|
||||||
|
<div class="prompt-actions">
|
||||||
|
<button class="btn btn-approve" onclick="respondToPrompt('${p.id}', 'approve')">Approve</button>
|
||||||
|
<button class="btn btn-reject" onclick="respondToPrompt('${p.id}', 'reject')">Reject</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPermissionPrompt(p) {
|
||||||
|
const json = p.json;
|
||||||
|
const toolName = json.tool_name || 'Unknown';
|
||||||
|
const toolInput = json.tool_input || {};
|
||||||
|
const options = json.options || [];
|
||||||
|
const selectedOption = json.selected_option;
|
||||||
|
const allowsTabInstructions = json.allows_tab_instructions || false;
|
||||||
|
|
||||||
|
// Extract relevant tool input info
|
||||||
|
let toolInputDisplay = '';
|
||||||
|
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'Read') {
|
||||||
|
toolInputDisplay = toolInput.file_path || '';
|
||||||
|
} else if (toolName === 'Bash') {
|
||||||
|
toolInputDisplay = toolInput.command || '';
|
||||||
|
} else if (toolInput.path) {
|
||||||
|
toolInputDisplay = toolInput.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionsHtml = options.map((opt, idx) => `
|
||||||
|
<label class="prompt-option" onclick="selectOption('${p.id}', ${idx})">
|
||||||
|
<input type="radio" name="prompt-${p.id}" value="${escapeHtml(opt.value)}" ${opt.value === selectedOption ? 'checked' : ''}>
|
||||||
|
<span class="prompt-option-label">${escapeHtml(opt.label)}</span>
|
||||||
|
</label>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
<div class="prompt" data-prompt="${p.id}">
|
<div class="prompt" data-prompt="${p.id}">
|
||||||
<div class="prompt-text">${escapeHtml(p.text)}</div>
|
<div class="prompt-tool">Tool: <span class="prompt-tool-name">${escapeHtml(toolName)}</span></div>
|
||||||
|
${toolInputDisplay ? `<div class="prompt-tool-input">${escapeHtml(toolInputDisplay)}</div>` : ''}
|
||||||
|
<div class="prompt-question">${escapeHtml(p.text)}</div>
|
||||||
|
<div class="prompt-options">
|
||||||
|
${optionsHtml}
|
||||||
|
</div>
|
||||||
<div class="prompt-actions">
|
<div class="prompt-actions">
|
||||||
<button class="btn btn-approve" onclick="respondToPrompt('${p.id}', 'approve')">Approve</button>
|
<button class="btn btn-submit" onclick="submitRichPrompt('${p.id}')">Submit</button>
|
||||||
<button class="btn btn-reject" onclick="respondToPrompt('${p.id}', 'reject')">Reject</button>
|
${allowsTabInstructions ? '<button class="btn btn-secondary" disabled data-tooltip="Coming soon">Add instructions</button>' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderQuestionPrompt(p) {
|
||||||
|
const json = p.json;
|
||||||
|
const header = json.header || '';
|
||||||
|
const question = json.question || p.text;
|
||||||
|
const options = json.options || [];
|
||||||
|
const multiSelect = json.multi_select || false;
|
||||||
|
const allowsOther = json.allows_other || false;
|
||||||
|
const inputType = multiSelect ? 'checkbox' : 'radio';
|
||||||
|
|
||||||
|
const optionsHtml = options.map((opt, idx) => `
|
||||||
|
<label class="prompt-option" onclick="selectOption('${p.id}', ${idx})">
|
||||||
|
<input type="${inputType}" name="prompt-${p.id}" value="${escapeHtml(opt)}">
|
||||||
|
<span class="prompt-option-label">${escapeHtml(opt)}</span>
|
||||||
|
</label>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const otherHtml = allowsOther ? `
|
||||||
|
<label class="prompt-option">
|
||||||
|
<input type="${inputType}" name="prompt-${p.id}" value="__other__" onclick="enableOtherInput('${p.id}')">
|
||||||
|
<span class="prompt-option-label">Other</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="prompt-other-input" id="other-${p.id}" placeholder="Enter your answer..." disabled>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="prompt" data-prompt="${p.id}">
|
||||||
|
${header ? `<div class="prompt-header">${escapeHtml(header)}</div>` : ''}
|
||||||
|
<div class="prompt-question">${escapeHtml(question)}</div>
|
||||||
|
<div class="prompt-options">
|
||||||
|
${optionsHtml}
|
||||||
|
${otherHtml}
|
||||||
|
</div>
|
||||||
|
<div class="prompt-actions">
|
||||||
|
<button class="btn btn-submit" onclick="submitRichPrompt('${p.id}')">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExitPlanPrompt(p) {
|
||||||
|
const json = p.json;
|
||||||
|
const options = json.options || [];
|
||||||
|
|
||||||
|
const optionsHtml = options.map((opt, idx) => `
|
||||||
|
<label class="prompt-option" onclick="selectOption('${p.id}', ${idx})">
|
||||||
|
<input type="radio" name="prompt-${p.id}" value="${escapeHtml(opt)}">
|
||||||
|
<span class="prompt-option-label">${escapeHtml(opt)}</span>
|
||||||
|
</label>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="prompt" data-prompt="${p.id}">
|
||||||
|
<div class="prompt-header">Plan Complete</div>
|
||||||
|
<div class="prompt-question">${escapeHtml(p.text)}</div>
|
||||||
|
<div class="prompt-options">
|
||||||
|
${optionsHtml}
|
||||||
|
</div>
|
||||||
|
<div class="prompt-actions">
|
||||||
|
<button class="btn btn-submit" onclick="submitRichPrompt('${p.id}')">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.selectOption = (_promptId, _optionIdx) => {
|
||||||
|
// Radio buttons and checkboxes handle their own state
|
||||||
|
// This function is just for accessibility/mobile tap handling
|
||||||
|
};
|
||||||
|
|
||||||
|
window.enableOtherInput = (promptId) => {
|
||||||
|
const input = document.getElementById(`other-${promptId}`);
|
||||||
|
if (input) {
|
||||||
|
input.disabled = false;
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.submitRichPrompt = async (promptId) => {
|
||||||
|
promptId = Number(promptId);
|
||||||
|
const prompt = state.prompts.get(promptId);
|
||||||
|
if (!prompt || !prompt.json) {
|
||||||
|
console.error('Invalid prompt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = prompt.json;
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (json.prompt_type === 'permission') {
|
||||||
|
// Get selected radio button
|
||||||
|
const selected = document.querySelector(`input[name="prompt-${promptId}"]:checked`);
|
||||||
|
if (!selected) {
|
||||||
|
console.error('No option selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response = {
|
||||||
|
type: 'option',
|
||||||
|
value: selected.value,
|
||||||
|
};
|
||||||
|
} else if (json.prompt_type === 'question') {
|
||||||
|
if (json.multi_select) {
|
||||||
|
// Get all checked checkboxes
|
||||||
|
const checked = Array.from(document.querySelectorAll(`input[name="prompt-${promptId}"]:checked`));
|
||||||
|
if (checked.length === 0) {
|
||||||
|
console.error('No options selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const values = checked.map(el => {
|
||||||
|
if (el.value === '__other__') {
|
||||||
|
const otherInput = document.getElementById(`other-${promptId}`);
|
||||||
|
return otherInput ? otherInput.value : '';
|
||||||
|
}
|
||||||
|
return el.value;
|
||||||
|
});
|
||||||
|
response = {
|
||||||
|
type: 'text',
|
||||||
|
value: values.join(', '),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Get selected radio button
|
||||||
|
const selected = document.querySelector(`input[name="prompt-${promptId}"]:checked`);
|
||||||
|
if (!selected) {
|
||||||
|
console.error('No option selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selected.value === '__other__') {
|
||||||
|
const otherInput = document.getElementById(`other-${promptId}`);
|
||||||
|
if (!otherInput || !otherInput.value) {
|
||||||
|
console.error('Please enter a value for "Other"');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response = {
|
||||||
|
type: 'text',
|
||||||
|
value: otherInput.value,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
response = {
|
||||||
|
type: 'text',
|
||||||
|
value: selected.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (json.prompt_type === 'exit_plan') {
|
||||||
|
// Get selected radio button
|
||||||
|
const selected = document.querySelector(`input[name="prompt-${promptId}"]:checked`);
|
||||||
|
if (!selected) {
|
||||||
|
console.error('No option selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response = {
|
||||||
|
type: 'text',
|
||||||
|
value: selected.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/prompts/${promptId}/answer`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(response),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Failed to submit prompt response:', await res.text());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting prompt response:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.respondToPrompt = async (promptId, action) => {
|
window.respondToPrompt = async (promptId, action) => {
|
||||||
promptId = Number(promptId);
|
promptId = Number(promptId);
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue