547 lines
24 KiB
HTML
547 lines
24 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}AI Summary — Website Checker{% endblock %}
|
||
{% block content %}
|
||
|
||
<div class="page-header">
|
||
<h1>🤖 AI Document Analysis</h1>
|
||
{% if is_admin %}
|
||
<a href="{{ url_for('admin_settings.settings') }}" class="btn btn-secondary btn-sm">⚙ API Settings</a>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if not groq_key_set %}
|
||
<div class="alert alert-warning mb-2">⚠ Groq API key not configured. An admin must add it under Settings.</div>
|
||
{% endif %}
|
||
|
||
<div class="ai-layout">
|
||
|
||
<!-- ── Left panel ──────────────────────────────────────────── -->
|
||
<div class="ai-left">
|
||
|
||
<!-- Upload card -->
|
||
<div class="card">
|
||
<div class="card-header"><span class="card-title">Upload Documents</span></div>
|
||
<div class="card-body-pad">
|
||
{% if is_admin %}
|
||
<div class="form-group">
|
||
<label>Model</label>
|
||
<select id="ai-model">
|
||
{% for m in groq_models %}
|
||
<option {{ 'selected' if m == groq_model }}>{{ m }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
{% endif %}
|
||
<div class="form-group">
|
||
<label>Documents <span class="text-muted">(PDF, DOCX, TXT, CSV, XLSX)</span></label>
|
||
<input type="file" id="ai-files" multiple
|
||
accept=".pdf,.docx,.doc,.txt,.csv,.xlsx,.xls,.md">
|
||
</div>
|
||
<button class="btn btn-primary w-full" id="btn-analyze" onclick="runAnalysis()">
|
||
🔍 Analyze with AI
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Criteria card -->
|
||
<div class="card mt-2">
|
||
<div class="card-header">
|
||
<span class="card-title">Evaluation Criteria</span>
|
||
{% if is_admin %}
|
||
<button class="btn btn-secondary btn-sm" onclick="openModal('modal-add-criterion')">+ Add</button>
|
||
{% endif %}
|
||
</div>
|
||
<div class="criteria-list">
|
||
{% for c in criteria %}
|
||
<div class="criterion-row {{ '' if c.is_active else 'criterion-inactive' }}"
|
||
data-title="{{ c.title }}"
|
||
data-desc="{{ c.description }}"
|
||
data-id="{{ c.id }}"
|
||
data-active="{{ '1' if c.is_active else '0' }}"
|
||
data-order="{{ c.sort_order }}">
|
||
<div class="criterion-header">
|
||
<strong class="criterion-title">{{ c.title }}</strong>
|
||
<div class="criterion-badges">
|
||
{% if not c.is_active %}<span class="badge badge-muted">Inactive</span>{% endif %}
|
||
{% if is_admin %}
|
||
<button class="btn btn-secondary btn-sm js-edit-criterion">✎ Edit</button>
|
||
<form method="post"
|
||
action="{{ url_for('ai_summary.delete_criterion_view', criterion_id=c.id) }}"
|
||
style="display:inline"
|
||
onsubmit="return confirm('Delete this criterion?')">
|
||
<button class="btn btn-danger btn-sm" type="submit">✕</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="criterion-desc">{{ c.description[:140] }}{% if c.description|length > 140 %}…{% endif %}</div>
|
||
<button class="btn-link-sm js-view-criterion">View full details ›</button>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted text-center" style="padding:1rem">No criteria defined yet.</p>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ── Right panel ─────────────────────────────────────────── -->
|
||
<div class="ai-right">
|
||
|
||
<!-- Output card -->
|
||
<div class="card ai-output-card">
|
||
<div class="card-header">
|
||
<span class="card-title">Extracted Information</span>
|
||
<div class="flex gap-1">
|
||
<button class="btn btn-secondary btn-sm" id="btn-copy" onclick="copyOutput()" style="display:none">📋 Copy</button>
|
||
<button class="btn btn-secondary btn-sm" id="btn-save" onclick="saveOutput()" style="display:none">💾 Save as TXT</button>
|
||
<button class="btn btn-secondary btn-sm" id="btn-clear" onclick="clearOutput()" style="display:none">🗑 Clear</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Verdict banner — hidden until result arrives -->
|
||
<div id="verdict-banner" style="display:none"></div>
|
||
|
||
<!-- Output body -->
|
||
<div id="ai-output" class="ai-output-body">
|
||
<div class="ai-placeholder">
|
||
<p>Upload a document and click <strong>Analyze with AI</strong> to begin.</p>
|
||
<p class="text-muted text-sm mt-1">The AI will extract: solicitation number & type, set-aside, scope of work, work site, pre-proposal conference, POC, square footage, driving distance & travel time, due date, and key deadlines.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- History card -->
|
||
<div class="card mt-2">
|
||
<div class="card-header"><span class="card-title">Analysis History</span></div>
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Date</th>
|
||
{% if is_admin %}<th>User</th>{% endif %}
|
||
<th>Files</th>
|
||
<th>Verdict</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for h in history %}
|
||
<tr class="history-row" data-id="{{ h.id }}" style="cursor:pointer" title="Click to view result">
|
||
<td class="nowrap text-muted">{{ h.analyzed_at.strftime('%Y-%m-%d %H:%M') if h.analyzed_at else '—' }}</td>
|
||
{% if is_admin %}<td class="text-muted">{{ h.username or '—' }}</td>{% endif %}
|
||
<td class="text-muted">{{ h.file_names[:50] }}{% if h.file_names|length > 50 %}…{% endif %}</td>
|
||
<td>
|
||
{% if h.verdict %}
|
||
<span class="badge badge-{{ 'success' if h.verdict=='PURSUE' else 'danger' if h.verdict=='PASS' else 'warning' }}">
|
||
{{ h.verdict }}
|
||
</span>
|
||
{% else %}—{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="{{ 5 if is_admin else 4 }}" style="padding:2.5rem 1rem;text-align:center">
|
||
<div style="font-size:2rem;margin-bottom:.5rem">🤖</div>
|
||
<p class="text-muted" style="margin:0 0 .75rem">No analyses yet. Upload a document above to get started.</p>
|
||
<button class="btn btn-primary btn-sm" onclick="document.getElementById('ai-files').scrollIntoView({behavior:'smooth'});document.getElementById('ai-files').focus()">Start Analyzing</button>
|
||
</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── View Criterion Modal ─────────────────────────────────── -->
|
||
<div class="modal-overlay" id="modal-view-criterion">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<span class="modal-title" id="vc-title">Criterion Details</span>
|
||
<button class="modal-close" onclick="closeModal('modal-view-criterion')">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p id="vc-desc" style="white-space:pre-wrap;line-height:1.7;color:var(--text-secondary)"></p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeModal('modal-view-criterion')">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if is_admin %}
|
||
<!-- ── Add Criterion Modal ──────────────────────────────────── -->
|
||
<div class="modal-overlay" id="modal-add-criterion">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<span class="modal-title">Add Criterion</span>
|
||
<button class="modal-close" onclick="closeModal('modal-add-criterion')">✕</button>
|
||
</div>
|
||
<form method="post" action="{{ url_for('ai_summary.create_criterion_view') }}">
|
||
<div class="modal-body">
|
||
<div class="form-group"><label>Title *</label><input name="title" required></div>
|
||
<div class="form-group"><label>Description *</label><textarea name="description" rows="5" required></textarea></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label>Sort Order</label><input type="number" name="sort_order" value="0"></div>
|
||
<div class="form-group"><label>Active</label>
|
||
<select name="is_active"><option value="1">Yes</option><option value="0">No</option></select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-add-criterion')">Cancel</button>
|
||
<button type="submit" class="btn btn-primary">Add Criterion</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Edit Criterion Modal ─────────────────────────────────── -->
|
||
<div class="modal-overlay" id="modal-edit-criterion">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<span class="modal-title">Edit Criterion</span>
|
||
<button class="modal-close" onclick="closeModal('modal-edit-criterion')">✕</button>
|
||
</div>
|
||
<form method="post" id="edit-criterion-form">
|
||
<div class="modal-body">
|
||
<div class="form-group"><label>Title *</label><input name="title" id="ec-title" required></div>
|
||
<div class="form-group"><label>Description *</label><textarea name="description" id="ec-desc" rows="5" required></textarea></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label>Sort Order</label><input type="number" name="sort_order" id="ec-order"></div>
|
||
<div class="form-group"><label>Active</label>
|
||
<select name="is_active" id="ec-active"><option value="1">Yes</option><option value="0">No</option></select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-edit-criterion')">Cancel</button>
|
||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- ── History Detail Modal ─────────────────────────────────── -->
|
||
<div class="modal-overlay" id="modal-history">
|
||
<div class="modal" style="max-width:780px">
|
||
<div class="modal-header">
|
||
<span class="modal-title" id="hist-title">Analysis Result</span>
|
||
<button class="modal-close" onclick="closeModal('modal-history')">✕</button>
|
||
</div>
|
||
<div id="hist-verdict-banner" style="display:none"></div>
|
||
<div class="modal-body" style="padding:0">
|
||
<div class="ai-meta-bar" id="hist-meta"></div>
|
||
<div id="hist-body" class="ai-rendered-output" style="max-height:65vh;overflow-y:auto;padding:1.25rem 1.5rem"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeModal('modal-history')">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- marked.js for markdown rendering -->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
|
||
|
||
<script>
|
||
/* ─── Markdown renderer config ──────────────────────────────── */
|
||
marked.setOptions({ breaks: true, gfm: true });
|
||
|
||
/* ─── Render AI text: markdown + escape safety ──────────────── */
|
||
function renderAI(text) {
|
||
// marked.parse handles markdown; it escapes HTML internally for safety
|
||
return marked.parse(String(text || ''));
|
||
}
|
||
|
||
/* ─── Verdict banner HTML ───────────────────────────────────── */
|
||
function verdictBannerHTML(verdict) {
|
||
if (!verdict) return '';
|
||
var cfg = {
|
||
'PURSUE': { bg:'#16a34a', icon:'✅', label:'PURSUE THIS OPPORTUNITY', sub:'Strong alignment with your evaluation criteria.' },
|
||
'PASS': { bg:'#dc2626', icon:'🚫', label:'PASS ON THIS OPPORTUNITY', sub:'Does not meet one or more key criteria.' },
|
||
'UNCLEAR': { bg:'#d97706', icon:'⚠️', label:'UNCLEAR — REVIEW MANUALLY', sub:'Insufficient information for a confident determination.' },
|
||
};
|
||
var c = cfg[verdict.toUpperCase()] || { bg:'#4b5563', icon:'•', label: verdict, sub:'' };
|
||
return '<div class="verdict-banner" style="background:' + c.bg + '">'
|
||
+ '<span class="verdict-icon">' + c.icon + '</span>'
|
||
+ '<div><div class="verdict-label">' + c.label + '</div>'
|
||
+ (c.sub ? '<div class="verdict-sub">' + c.sub + '</div>' : '')
|
||
+ '</div></div>';
|
||
}
|
||
|
||
/* ─── Show/clear output ─────────────────────────────────────── */
|
||
var _rawText = ''; // kept for copy/save
|
||
|
||
function showResult(data) {
|
||
_rawText = data.summary || '';
|
||
var fileList = (data.file_names || '').split(', ').map(function(f) {
|
||
return ' - ' + f;
|
||
}).join('\n');
|
||
var criteriaNote = data.criteria_count > 0
|
||
? 'Criteria evaluated: ' + data.criteria_count
|
||
: 'No evaluation criteria configured.';
|
||
var header = 'AI Extraction | Model: ' + (data.model || '')
|
||
+ '\nFiles analyzed (' + (data.file_count || 1) + '):\n' + fileList
|
||
+ '\n' + criteriaNote
|
||
+ '\n' + '='.repeat(60);
|
||
|
||
// Verdict banner
|
||
var banner = document.getElementById('verdict-banner');
|
||
if (data.verdict) {
|
||
banner.innerHTML = verdictBannerHTML(data.verdict);
|
||
banner.style.display = '';
|
||
} else {
|
||
banner.style.display = 'none';
|
||
banner.innerHTML = '';
|
||
}
|
||
|
||
// Render output: header as plain preformatted, then markdown body
|
||
document.getElementById('ai-output').innerHTML =
|
||
'<pre class="ai-header-block">' + escHtml(header) + '</pre>'
|
||
+ '<div class="ai-rendered-output">' + renderAI(_rawText) + '</div>';
|
||
|
||
// Show action buttons
|
||
document.getElementById('btn-copy').style.display = '';
|
||
document.getElementById('btn-save').style.display = '';
|
||
document.getElementById('btn-clear').style.display = '';
|
||
}
|
||
|
||
function clearOutput() {
|
||
_rawText = '';
|
||
document.getElementById('ai-output').innerHTML =
|
||
'<div class="ai-placeholder"><p>Upload a document and click <strong>Analyze with AI</strong> to begin.</p></div>';
|
||
document.getElementById('verdict-banner').style.display = 'none';
|
||
document.getElementById('verdict-banner').innerHTML = '';
|
||
document.getElementById('btn-copy').style.display = 'none';
|
||
document.getElementById('btn-save').style.display = 'none';
|
||
document.getElementById('btn-clear').style.display = 'none';
|
||
}
|
||
|
||
function copyOutput() {
|
||
if (!_rawText) return;
|
||
navigator.clipboard.writeText(_rawText).then(function() {
|
||
var btn = document.getElementById('btn-copy');
|
||
var orig = btn.textContent;
|
||
btn.textContent = '✓ Copied';
|
||
setTimeout(function() { btn.textContent = orig; }, 2000);
|
||
});
|
||
}
|
||
|
||
function saveOutput() {
|
||
if (!_rawText) return;
|
||
var blob = new Blob([_rawText], { type: 'text/plain' });
|
||
var a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = 'ai_extraction.txt';
|
||
a.click();
|
||
}
|
||
|
||
/* ─── Analyze ───────────────────────────────────────────────── */
|
||
async function runAnalysis() {
|
||
var files = document.getElementById('ai-files').files;
|
||
if (!files.length) { alert('Please select at least one file.'); return; }
|
||
|
||
var modelEl = document.getElementById('ai-model');
|
||
var model = modelEl ? modelEl.value : '';
|
||
|
||
var btn = document.getElementById('btn-analyze');
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳ Analysing…';
|
||
|
||
document.getElementById('ai-output').innerHTML =
|
||
'<div class="ai-placeholder"><p class="text-muted">Running AI analysis… this may take a moment.</p></div>';
|
||
document.getElementById('verdict-banner').style.display = 'none';
|
||
document.getElementById('btn-copy').style.display = 'none';
|
||
document.getElementById('btn-save').style.display = 'none';
|
||
document.getElementById('btn-clear').style.display = 'none';
|
||
|
||
var fd = new FormData();
|
||
for (var i = 0; i < files.length; i++) fd.append('documents[]', files[i]);
|
||
if (model) fd.append('model', model);
|
||
|
||
try {
|
||
var resp = await fetch('/ai-summary/analyze', { method: 'POST', body: fd, headers: {'X-CSRFToken': getCsrfToken()} });
|
||
var data = await resp.json();
|
||
if (data.error) {
|
||
document.getElementById('ai-output').innerHTML =
|
||
'<div class="alert alert-danger" style="margin:1rem">' + escHtml(data.error) + '</div>';
|
||
} else {
|
||
// Attach metadata for header rendering
|
||
data.file_names = Array.from(files).map(function(f){ return f.name; }).join(', ');
|
||
data.file_count = files.length;
|
||
data.model = model || '{{ groq_model }}';
|
||
data.criteria_count = {{ criteria | selectattr('is_active') | list | length }};
|
||
showResult(data);
|
||
}
|
||
} catch(e) {
|
||
document.getElementById('ai-output').innerHTML =
|
||
'<div class="alert alert-danger" style="margin:1rem">Request failed: ' + escHtml(String(e)) + '</div>';
|
||
} finally {
|
||
btn.disabled = false;
|
||
btn.textContent = '🔍 Analyze with AI';
|
||
}
|
||
}
|
||
|
||
/* ─── Criteria modals ───────────────────────────────────────── */
|
||
document.querySelectorAll('.js-view-criterion').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var row = btn.closest('.criterion-row');
|
||
document.getElementById('vc-title').textContent = row.dataset.title;
|
||
document.getElementById('vc-desc').textContent = row.dataset.desc;
|
||
openModal('modal-view-criterion');
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.js-edit-criterion').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var row = btn.closest('.criterion-row');
|
||
document.getElementById('ec-title').value = row.dataset.title;
|
||
document.getElementById('ec-desc').value = row.dataset.desc;
|
||
document.getElementById('ec-order').value = row.dataset.order;
|
||
document.getElementById('ec-active').value = row.dataset.active;
|
||
document.getElementById('edit-criterion-form').action =
|
||
'/ai-summary/criteria/' + row.dataset.id + '/edit';
|
||
openModal('modal-edit-criterion');
|
||
});
|
||
});
|
||
|
||
/* ─── History rows ──────────────────────────────────────────── */
|
||
document.querySelectorAll('.history-row').forEach(function(row) {
|
||
row.addEventListener('click', function() {
|
||
var id = row.dataset.id;
|
||
document.getElementById('hist-title').textContent = 'Loading…';
|
||
document.getElementById('hist-meta').textContent = '';
|
||
document.getElementById('hist-body').innerHTML = '';
|
||
document.getElementById('hist-verdict-banner').style.display = 'none';
|
||
openModal('modal-history');
|
||
|
||
fetch('/ai-summary/history/' + id)
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.error) {
|
||
document.getElementById('hist-body').textContent = d.error;
|
||
return;
|
||
}
|
||
document.getElementById('hist-title').textContent =
|
||
'Analysis — ' + (d.file_names || '');
|
||
document.getElementById('hist-meta').textContent =
|
||
(d.analyzed_at || '').slice(0, 16)
|
||
+ (d.username ? ' · ' + d.username : '')
|
||
+ ' · ' + (d.model || '');
|
||
|
||
if (d.verdict) {
|
||
var vb = document.getElementById('hist-verdict-banner');
|
||
vb.innerHTML = verdictBannerHTML(d.verdict);
|
||
vb.style.display = '';
|
||
}
|
||
document.getElementById('hist-body').innerHTML =
|
||
renderAI(d.summary_text || '');
|
||
})
|
||
.catch(function() {
|
||
document.getElementById('hist-body').textContent = 'Failed to load analysis.';
|
||
});
|
||
});
|
||
});
|
||
|
||
/* ─── Utility ───────────────────────────────────────────────── */
|
||
function escHtml(str) {
|
||
return String(str || '').replace(/[&<>"']/g, function(c) {
|
||
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* ── Layout ────────────────────────────────────────────────── */
|
||
.ai-layout{display:grid;grid-template-columns:320px 1fr;gap:1.25rem;align-items:start}
|
||
.card-body-pad{padding:1.1rem 1.4rem 1.4rem}
|
||
.ai-output-card{display:flex;flex-direction:column;overflow:hidden}
|
||
|
||
/* ── Verdict banner ────────────────────────────────────────── */
|
||
.verdict-banner{
|
||
display:flex;align-items:center;gap:1rem;
|
||
padding:.9rem 1.4rem;color:#fff;
|
||
}
|
||
.verdict-icon{font-size:1.3rem;flex-shrink:0}
|
||
.verdict-label{font-size:1rem;font-weight:700;letter-spacing:.01em}
|
||
.verdict-sub{font-size:.8rem;opacity:.9;margin-top:2px}
|
||
|
||
/* ── Output area ───────────────────────────────────────────── */
|
||
.ai-output-body{
|
||
flex:1;overflow-y:auto;
|
||
min-height:300px;max-height:calc(100vh - 280px);
|
||
}
|
||
.ai-placeholder{
|
||
padding:2rem 1.5rem;color:var(--text-muted);
|
||
}
|
||
.ai-header-block{
|
||
background:var(--bg-subtle);border-bottom:1px solid var(--border);
|
||
padding:.85rem 1.4rem;font-family:'DM Mono',monospace;font-size:.78rem;
|
||
color:var(--text-secondary);margin:0;white-space:pre-wrap;line-height:1.6;
|
||
}
|
||
.ai-meta-bar{
|
||
padding:.6rem 1.5rem;font-size:.78rem;font-family:'DM Mono',monospace;
|
||
color:var(--text-muted);background:var(--bg-subtle);
|
||
border-bottom:1px solid var(--border);
|
||
}
|
||
|
||
/* ── Rendered markdown output ──────────────────────────────── */
|
||
.ai-rendered-output{
|
||
padding:1.1rem 1.4rem 1.5rem;
|
||
font-size:.9rem;line-height:1.8;color:var(--text-secondary);
|
||
}
|
||
.ai-rendered-output h1,.ai-rendered-output h2{
|
||
font-size:1rem;font-weight:700;color:var(--text);
|
||
margin:1.4rem 0 .5rem;padding-bottom:.3rem;
|
||
border-bottom:1px solid var(--border);
|
||
}
|
||
.ai-rendered-output h3,.ai-rendered-output h4{
|
||
font-size:.9rem;font-weight:700;color:var(--text);margin:1rem 0 .35rem;
|
||
}
|
||
.ai-rendered-output p{margin:.35rem 0}
|
||
.ai-rendered-output strong{color:var(--text);font-weight:700}
|
||
.ai-rendered-output ul,.ai-rendered-output ol{
|
||
padding-left:1.4rem;margin:.35rem 0;
|
||
}
|
||
.ai-rendered-output li{margin:.2rem 0}
|
||
.ai-rendered-output hr{
|
||
border:none;border-top:2px solid var(--border);margin:1.25rem 0;
|
||
}
|
||
.ai-rendered-output pre,.ai-rendered-output code{
|
||
font-family:'DM Mono',monospace;font-size:.82rem;
|
||
background:var(--bg-subtle);border-radius:var(--r-sm);
|
||
}
|
||
.ai-rendered-output pre{padding:.75rem 1rem;overflow-x:auto}
|
||
.ai-rendered-output code{padding:.1rem .3rem}
|
||
|
||
/* ── Criteria list ─────────────────────────────────────────── */
|
||
.criteria-list{display:flex;flex-direction:column}
|
||
.criterion-row{
|
||
padding:.8rem 1.2rem;border-bottom:1px solid var(--border);
|
||
transition:background var(--t);
|
||
}
|
||
.criterion-row:last-child{border-bottom:none}
|
||
.criterion-row:hover{background:var(--bg-subtle)}
|
||
.criterion-inactive{opacity:.55}
|
||
.criterion-header{display:flex;align-items:center;justify-content:space-between;
|
||
gap:.5rem;margin-bottom:.2rem}
|
||
.criterion-title{font-size:.875rem;font-weight:600;color:var(--text)}
|
||
.criterion-badges{display:flex;align-items:center;gap:.35rem;flex-shrink:0}
|
||
.criterion-desc{font-size:.78rem;color:var(--text-muted);line-height:1.5;margin-bottom:.3rem}
|
||
.btn-link-sm{
|
||
background:none;border:none;padding:0;font-size:.73rem;
|
||
color:var(--accent);cursor:pointer;font-family:inherit;
|
||
}
|
||
.btn-link-sm:hover{text-decoration:underline}
|
||
|
||
/* ── History ───────────────────────────────────────────────── */
|
||
.history-row:hover td{background:var(--bg-subtle)}
|
||
|
||
@media(max-width:960px){
|
||
.ai-layout{grid-template-columns:1fr}
|
||
.ai-output-body{max-height:60vh}
|
||
}
|
||
</style>
|
||
{% endblock %}
|