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():
|
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}")
|
logger.info(f"AI analysis cache hit for company {company_id} audit_type={audit_type}")
|
||||||
return {
|
result = {
|
||||||
'summary': cache.analysis_summary,
|
'summary': cache.analysis_summary,
|
||||||
'actions': cache.actions_json or [],
|
'actions': cache.actions_json or [],
|
||||||
'cached': True,
|
'cached': True,
|
||||||
'generated_at': cache.generated_at.isoformat() if cache.generated_at else None,
|
'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
|
# Build prompt
|
||||||
prompt_builders = {
|
prompt_builders = {
|
||||||
@ -656,6 +663,10 @@ def generate_analysis(company_id: int, audit_type: str, user_id: int = None, for
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
if cache:
|
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.analysis_summary = summary
|
||||||
cache.actions_json = actions
|
cache.actions_json = actions
|
||||||
cache.audit_data_hash = data_hash
|
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()
|
db.commit()
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
'summary': summary,
|
'summary': summary,
|
||||||
'actions': actions,
|
'actions': actions,
|
||||||
'cached': False,
|
'cached': False,
|
||||||
'generated_at': datetime.now().isoformat(),
|
'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:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
|
|||||||
@ -5135,6 +5135,9 @@ class AuditAICache(Base):
|
|||||||
audit_data_hash = Column(String(64))
|
audit_data_hash = Column(String(64))
|
||||||
generated_at = Column(DateTime, default=datetime.now)
|
generated_at = Column(DateTime, default=datetime.now)
|
||||||
expires_at = Column(DateTime)
|
expires_at = Column(DateTime)
|
||||||
|
previous_summary = Column(Text)
|
||||||
|
previous_actions_json = Column(JSONB)
|
||||||
|
previous_generated_at = Column(DateTime)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
company = relationship('Company', backref='audit_ai_caches')
|
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);
|
actionsList.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Render comparison with previous analysis if available
|
||||||
|
if (typeof renderAIComparison === 'function') renderAIComparison(data);
|
||||||
|
|
||||||
results.style.display = 'block';
|
results.style.display = 'block';
|
||||||
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||||
window._aiActions = actions;
|
window._aiActions = actions;
|
||||||
|
|||||||
@ -55,6 +55,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Actions List -->
|
||||||
<div style="font-size: var(--font-size-lg); font-weight: 600; color: var(--text-primary); margin-bottom: var(--spacing-md);">
|
<div style="font-size: var(--font-size-lg); font-weight: 600; color: var(--text-primary); margin-bottom: var(--spacing-md);">
|
||||||
Priorytetowe akcje
|
Priorytetowe akcje
|
||||||
@ -160,6 +184,48 @@
|
|||||||
.ai-action-card.dismissed {
|
.ai-action-card.dismissed {
|
||||||
display: none;
|
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>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -188,4 +254,92 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
})
|
})
|
||||||
.catch(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>
|
</script>
|
||||||
|
|||||||
@ -1099,6 +1099,9 @@ function renderAIResults(data) {
|
|||||||
actionsList.appendChild(card);
|
actionsList.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Render comparison with previous analysis if available
|
||||||
|
if (typeof renderAIComparison === 'function') renderAIComparison(data);
|
||||||
|
|
||||||
results.style.display = 'block';
|
results.style.display = 'block';
|
||||||
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||||
|
|
||||||
|
|||||||
@ -1499,6 +1499,9 @@ function renderAIResults(data) {
|
|||||||
actionsList.appendChild(card);
|
actionsList.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Render comparison with previous analysis if available
|
||||||
|
if (typeof renderAIComparison === 'function') renderAIComparison(data);
|
||||||
|
|
||||||
results.style.display = 'block';
|
results.style.display = 'block';
|
||||||
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||||
window._aiActions = actions;
|
window._aiActions = actions;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user