feat(bi): add interactive sliders to cost configuration modal

Enhanced cost configuration UX with dual-control interface:
- Range sliders for quick visual adjustments
- Number inputs for precise values
- Real-time sync between slider and input
- Live value display with formatting ($X,XXX)
- Custom purple styling matching Tractatus theme

Slider ranges by severity:
- CRITICAL: $1k-$250k (step: $1k)
- HIGH: $500-$50k (step: $500)
- MEDIUM: $100-$10k (step: $100)
- LOW: $50-$5k (step: $50)

Users can drag sliders OR type exact amounts for maximum flexibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TheFlow 2025-10-27 11:16:21 +13:00
parent a421c93c51
commit 70f02ec932

View file

@ -950,6 +950,14 @@ async function showCostConfigModal() {
return;
}
// Slider ranges by severity
const sliderConfig = {
CRITICAL: { min: 1000, max: 250000, step: 1000 },
HIGH: { min: 500, max: 50000, step: 500 },
MEDIUM: { min: 100, max: 10000, step: 100 },
LOW: { min: 50, max: 5000, step: 50 }
};
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
modal.innerHTML = `
@ -960,14 +968,34 @@ async function showCostConfigModal() {
</div>
<div class="p-6 space-y-4">
${['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map(severity => `
${['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map(severity => {
const config = sliderConfig[severity];
const amount = Math.min(Math.max(costFactors[severity].amount, config.min), config.max);
return `
<div class="border border-gray-200 rounded-lg p-4">
<label class="block font-semibold text-gray-900 mb-2">${severity}</label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<label class="block font-semibold text-gray-900 mb-3">${severity}</label>
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<label class="block text-sm text-gray-600">Amount</label>
<span class="text-sm font-medium text-gray-900">$<span id="cost-${severity}-display">${amount.toLocaleString()}</span></span>
</div>
<input type="range" id="cost-${severity}-slider"
min="${config.min}" max="${config.max}" step="${config.step}"
value="${amount}"
class="w-full h-2 bg-purple-200 rounded-lg appearance-none cursor-pointer slider">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>$${(config.min / 1000).toFixed(0)}k</span>
<span>$${(config.max / 1000).toFixed(0)}k</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
<div>
<label class="block text-sm text-gray-600 mb-1">Amount ($)</label>
<label class="block text-sm text-gray-600 mb-1">Exact Amount ($)</label>
<input type="number" id="cost-${severity}-amount"
value="${costFactors[severity].amount}"
min="0" step="${config.step}"
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-purple-500 focus:border-transparent">
</div>
<div>
@ -977,13 +1005,14 @@ async function showCostConfigModal() {
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-purple-500 focus:border-transparent">
</div>
</div>
<div class="mt-2">
<div>
<label class="block text-sm text-gray-600 mb-1">Rationale</label>
<textarea id="cost-${severity}-rationale" rows="2"
class="w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-purple-500 focus:border-transparent">${costFactors[severity].rationale}</textarea>
</div>
</div>
`).join('')}
`}).join('')}
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm text-amber-800">
<strong>Note:</strong> These values are illustrative placeholders for research purposes.
@ -1000,10 +1029,63 @@ async function showCostConfigModal() {
</button>
</div>
</div>
<style>
/* Custom slider styling */
.slider::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #9333ea;
cursor: pointer;
transition: all 0.15s ease;
}
.slider::-webkit-slider-thumb:hover {
background: #7e22ce;
transform: scale(1.1);
}
.slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #9333ea;
cursor: pointer;
border: none;
transition: all 0.15s ease;
}
.slider::-moz-range-thumb:hover {
background: #7e22ce;
transform: scale(1.1);
}
</style>
`;
document.body.appendChild(modal);
// Sync sliders with number inputs
['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].forEach(severity => {
const slider = modal.querySelector(`#cost-${severity}-slider`);
const input = modal.querySelector(`#cost-${severity}-amount`);
const display = modal.querySelector(`#cost-${severity}-display`);
// Slider changes update input and display
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
input.value = value;
display.textContent = value.toLocaleString();
});
// Input changes update slider and display
input.addEventListener('input', (e) => {
const value = parseFloat(e.target.value) || 0;
const config = sliderConfig[severity];
const clampedValue = Math.min(Math.max(value, config.min), config.max);
slider.value = clampedValue;
display.textContent = value.toLocaleString();
});
});
// Close on cancel
modal.querySelector('#cancel-config-btn').addEventListener('click', () => {
document.body.removeChild(modal);