feat(audit): Add previous vs current AI analysis comparison
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Store previous analysis before regeneration and show comparison table with priority breakdown, new/removed actions diff. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3307d99729
commit
7197af3933
@ -600,12 +600,19 @@ def generate_analysis(company_id: int, audit_type: str, user_id: int = None, for
|
||||
|
||||
if cache and cache.audit_data_hash == data_hash and cache.expires_at and cache.expires_at > datetime.now():
|
||||
logger.info(f"AI analysis cache hit for company {company_id} audit_type={audit_type}")
|
||||
return {
|
||||
result = {
|
||||
'summary': cache.analysis_summary,
|
||||
'actions': cache.actions_json or [],
|
||||
'cached': True,
|
||||
'generated_at': cache.generated_at.isoformat() if cache.generated_at else None,
|
||||
}
|
||||
if cache.previous_summary or cache.previous_actions_json:
|
||||
result['previous'] = {
|
||||
'summary': cache.previous_summary,
|
||||
'actions': cache.previous_actions_json or [],
|
||||
'generated_at': cache.previous_generated_at.isoformat() if cache.previous_generated_at else None,
|
||||
}
|
||||
return result
|
||||
|
||||
# Build prompt
|
||||
prompt_builders = {
|
||||
@ -656,6 +663,10 @@ def generate_analysis(company_id: int, audit_type: str, user_id: int = None, for
|
||||
).first()
|
||||
|
||||
if cache:
|
||||
# Preserve previous analysis for comparison
|
||||
cache.previous_summary = cache.analysis_summary
|
||||
cache.previous_actions_json = cache.actions_json
|
||||
cache.previous_generated_at = cache.generated_at
|
||||
cache.analysis_summary = summary
|
||||
cache.actions_json = actions
|
||||
cache.audit_data_hash = data_hash
|
||||
@ -693,12 +704,19 @@ def generate_analysis(company_id: int, audit_type: str, user_id: int = None, for
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
result = {
|
||||
'summary': summary,
|
||||
'actions': actions,
|
||||
'cached': False,
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
}
|
||||
if cache and (cache.previous_summary or cache.previous_actions_json):
|
||||
result['previous'] = {
|
||||
'summary': cache.previous_summary,
|
||||
'actions': cache.previous_actions_json or [],
|
||||
'generated_at': cache.previous_generated_at.isoformat() if cache.previous_generated_at else None,
|
||||
}
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
|
||||
@ -5135,6 +5135,9 @@ class AuditAICache(Base):
|
||||
audit_data_hash = Column(String(64))
|
||||
generated_at = Column(DateTime, default=datetime.now)
|
||||
expires_at = Column(DateTime)
|
||||
previous_summary = Column(Text)
|
||||
previous_actions_json = Column(JSONB)
|
||||
previous_generated_at = Column(DateTime)
|
||||
|
||||
# Relationships
|
||||
company = relationship('Company', backref='audit_ai_caches')
|
||||
|
||||
6
database/migrations/057_audit_cache_previous.sql
Normal file
6
database/migrations/057_audit_cache_previous.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- Migration 057: Add previous analysis columns to audit_ai_cache
|
||||
-- Stores the previous AI analysis before regeneration for comparison
|
||||
|
||||
ALTER TABLE audit_ai_cache ADD COLUMN IF NOT EXISTS previous_summary TEXT;
|
||||
ALTER TABLE audit_ai_cache ADD COLUMN IF NOT EXISTS previous_actions_json JSONB;
|
||||
ALTER TABLE audit_ai_cache ADD COLUMN IF NOT EXISTS previous_generated_at TIMESTAMP;
|
||||
@ -2049,6 +2049,9 @@ function renderAIResults(data) {
|
||||
actionsList.appendChild(card);
|
||||
});
|
||||
|
||||
// Render comparison with previous analysis if available
|
||||
if (typeof renderAIComparison === 'function') renderAIComparison(data);
|
||||
|
||||
results.style.display = 'block';
|
||||
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||
window._aiActions = actions;
|
||||
|
||||
@ -55,6 +55,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comparison with previous analysis -->
|
||||
<div id="aiComparison" style="display: none; margin-bottom: var(--spacing-lg);">
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); cursor: pointer;" onclick="toggleComparison()">
|
||||
<svg id="aiComparisonArrow" width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="transition: transform 0.2s;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<span style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-secondary);">Porownanie z poprzednia analiza</span>
|
||||
<span id="aiComparisonDate" style="font-size: var(--font-size-xs); color: var(--text-tertiary);"></span>
|
||||
</div>
|
||||
<div id="aiComparisonBody" style="display: none;">
|
||||
<table class="ai-comparison-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Aspekt</th>
|
||||
<th>Poprzednia</th>
|
||||
<th>Obecna</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="aiComparisonRows"></tbody>
|
||||
</table>
|
||||
<div id="aiComparisonDiff" style="margin-top: var(--spacing-sm);"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions List -->
|
||||
<div style="font-size: var(--font-size-lg); font-weight: 600; color: var(--text-primary); margin-bottom: var(--spacing-md);">
|
||||
Priorytetowe akcje
|
||||
@ -160,6 +184,48 @@
|
||||
.ai-action-card.dismissed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ai-comparison-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
.ai-comparison-table th {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 2px solid var(--border);
|
||||
}
|
||||
.ai-comparison-table td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
vertical-align: top;
|
||||
}
|
||||
.ai-comparison-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.ai-diff-added {
|
||||
display: inline-block;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
margin: 2px;
|
||||
}
|
||||
.ai-diff-removed {
|
||||
display: inline-block;
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
margin: 2px;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@ -188,4 +254,92 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
})
|
||||
.catch(function() {});
|
||||
});
|
||||
|
||||
function toggleComparison() {
|
||||
var body = document.getElementById('aiComparisonBody');
|
||||
var arrow = document.getElementById('aiComparisonArrow');
|
||||
if (body.style.display === 'none') {
|
||||
body.style.display = 'block';
|
||||
arrow.style.transform = 'rotate(90deg)';
|
||||
} else {
|
||||
body.style.display = 'none';
|
||||
arrow.style.transform = '';
|
||||
}
|
||||
}
|
||||
|
||||
function renderAIComparison(data) {
|
||||
var section = document.getElementById('aiComparison');
|
||||
if (!data.previous) {
|
||||
section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
var prev = data.previous;
|
||||
var prevActions = prev.actions || [];
|
||||
var currActions = data.actions || [];
|
||||
var priorityOrder = ['critical', 'high', 'medium', 'low'];
|
||||
|
||||
function countPriority(actions, p) {
|
||||
return actions.filter(function(a) { return a.priority === p; }).length;
|
||||
}
|
||||
|
||||
var rows = document.getElementById('aiComparisonRows');
|
||||
var html = '';
|
||||
|
||||
// Summary row
|
||||
var prevSummaryShort = (prev.summary || '').substring(0, 120) + ((prev.summary || '').length > 120 ? '...' : '');
|
||||
var currSummaryShort = (data.summary || '').substring(0, 120) + ((data.summary || '').length > 120 ? '...' : '');
|
||||
html += '<tr><td><strong>Podsumowanie</strong></td><td style="color: var(--text-secondary);">' + escapeHtml(prevSummaryShort) + '</td><td>' + escapeHtml(currSummaryShort) + '</td></tr>';
|
||||
|
||||
// Count row
|
||||
html += '<tr><td><strong>Liczba akcji</strong></td><td>' + prevActions.length + '</td><td>' + currActions.length + '</td></tr>';
|
||||
|
||||
// Priority breakdown
|
||||
priorityOrder.forEach(function(p) {
|
||||
var labels = {critical: 'Krytyczne', high: 'Wysokie', medium: 'Srednie', low: 'Niskie'};
|
||||
var pc = countPriority(prevActions, p);
|
||||
var cc = countPriority(currActions, p);
|
||||
if (pc > 0 || cc > 0) {
|
||||
var diff = cc - pc;
|
||||
var diffStr = diff > 0 ? ' <span style="color:#16a34a;">(+' + diff + ')</span>' : diff < 0 ? ' <span style="color:#dc2626;">(' + diff + ')</span>' : '';
|
||||
html += '<tr><td>' + labels[p] + '</td><td>' + pc + '</td><td>' + cc + diffStr + '</td></tr>';
|
||||
}
|
||||
});
|
||||
|
||||
// Date row
|
||||
if (prev.generated_at) {
|
||||
var pd = new Date(prev.generated_at);
|
||||
html += '<tr><td><strong>Data analizy</strong></td><td>' + pd.toLocaleDateString('pl-PL') + ' ' + pd.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'}) + '</td><td>teraz</td></tr>';
|
||||
}
|
||||
|
||||
rows.innerHTML = html;
|
||||
|
||||
// Diff: new/removed actions
|
||||
var prevTitles = prevActions.map(function(a) { return a.title; });
|
||||
var currTitles = currActions.map(function(a) { return a.title; });
|
||||
var added = currTitles.filter(function(t) { return prevTitles.indexOf(t) === -1; });
|
||||
var removed = prevTitles.filter(function(t) { return currTitles.indexOf(t) === -1; });
|
||||
|
||||
var diffEl = document.getElementById('aiComparisonDiff');
|
||||
var diffHtml = '';
|
||||
if (added.length > 0) {
|
||||
diffHtml += '<div style="margin-bottom: var(--spacing-xs);"><strong style="font-size: var(--font-size-xs); color: var(--text-secondary);">Nowe akcje:</strong> ';
|
||||
added.forEach(function(t) { diffHtml += '<span class="ai-diff-added">' + escapeHtml(t) + '</span>'; });
|
||||
diffHtml += '</div>';
|
||||
}
|
||||
if (removed.length > 0) {
|
||||
diffHtml += '<div><strong style="font-size: var(--font-size-xs); color: var(--text-secondary);">Usuniete akcje:</strong> ';
|
||||
removed.forEach(function(t) { diffHtml += '<span class="ai-diff-removed">' + escapeHtml(t) + '</span>'; });
|
||||
diffHtml += '</div>';
|
||||
}
|
||||
diffEl.innerHTML = diffHtml;
|
||||
|
||||
// Show date in header
|
||||
if (prev.generated_at) {
|
||||
var pd2 = new Date(prev.generated_at);
|
||||
document.getElementById('aiComparisonDate').textContent = '(z ' + pd2.toLocaleDateString('pl-PL') + ')';
|
||||
}
|
||||
|
||||
section.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1099,6 +1099,9 @@ function renderAIResults(data) {
|
||||
actionsList.appendChild(card);
|
||||
});
|
||||
|
||||
// Render comparison with previous analysis if available
|
||||
if (typeof renderAIComparison === 'function') renderAIComparison(data);
|
||||
|
||||
results.style.display = 'block';
|
||||
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||
|
||||
|
||||
@ -1499,6 +1499,9 @@ function renderAIResults(data) {
|
||||
actionsList.appendChild(card);
|
||||
});
|
||||
|
||||
// Render comparison with previous analysis if available
|
||||
if (typeof renderAIComparison === 'function') renderAIComparison(data);
|
||||
|
||||
results.style.display = 'block';
|
||||
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||
window._aiActions = actions;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user