feat(audit-ai): 9 UX improvements for AI audit actions
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
- Cache: exclude volatile citations fields from hash for better hit rate - Thinking level: reduce from 'high' to 'low' for faster responses (13-24s → ~5-10s) - UTF-8: html.unescape() on meta_title, meta_description, h1_text - SEO score: add "(Google Lighthouse)" label and explanatory note - Spinner: update text to "15-30 seconds" with live timer counter - Auto-scroll: smooth scroll to results after AI analysis and content generation - Cache info: show generation date with "Wygeneruj ponownie" link - Markdown: render non-code AI content with basic markdown formatting - Error feedback: inline error display with retry button instead of modal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ad6299381
commit
0c6cd09d18
@ -20,6 +20,7 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from html import unescape
|
||||
|
||||
from database import (
|
||||
SessionLocal, Company, CompanyWebsiteAnalysis, CompanySocialMedia,
|
||||
@ -75,10 +76,10 @@ def _collect_seo_data(db, company) -> dict:
|
||||
'accessibility_score': analysis.pagespeed_accessibility_score,
|
||||
'best_practices_score': analysis.pagespeed_best_practices_score,
|
||||
# On-page
|
||||
'meta_title': analysis.meta_title,
|
||||
'meta_description': analysis.meta_description,
|
||||
'meta_title': unescape(analysis.meta_title or ''),
|
||||
'meta_description': unescape(analysis.meta_description or ''),
|
||||
'h1_count': analysis.h1_count,
|
||||
'h1_text': analysis.h1_text,
|
||||
'h1_text': unescape(analysis.h1_text or ''),
|
||||
'h2_count': analysis.h2_count,
|
||||
'h3_count': analysis.h3_count,
|
||||
'total_images': analysis.total_images,
|
||||
@ -583,7 +584,9 @@ def generate_analysis(company_id: int, audit_type: str, user_id: int = None, for
|
||||
if not data:
|
||||
return {'error': f'Brak danych audytu {audit_type} dla tej firmy'}
|
||||
|
||||
data_hash = _hash_data(data)
|
||||
# Exclude volatile fields from hash to improve cache hit rate
|
||||
hash_data = {k: v for k, v in data.items() if k not in ('citations_count', 'citations_found')}
|
||||
data_hash = _hash_data(hash_data)
|
||||
|
||||
# Check cache
|
||||
if not force:
|
||||
@ -614,6 +617,7 @@ def generate_analysis(company_id: int, audit_type: str, user_id: int = None, for
|
||||
response_text = gemini.generate_text(
|
||||
prompt=prompt,
|
||||
temperature=0.3,
|
||||
thinking_level='low',
|
||||
feature='audit_analysis',
|
||||
user_id=user_id,
|
||||
company_id=company_id,
|
||||
@ -800,6 +804,7 @@ Max 500 słów."""
|
||||
content = gemini.generate_text(
|
||||
prompt=prompt,
|
||||
temperature=0.5,
|
||||
thinking_level='low',
|
||||
feature='audit_content_generation',
|
||||
user_id=user_id,
|
||||
company_id=company_id,
|
||||
|
||||
@ -1911,6 +1911,18 @@ async function runAudit() {
|
||||
const companyId = {{ company.id }};
|
||||
const auditType = 'gbp';
|
||||
|
||||
function simpleMarkdown(text) {
|
||||
return text
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
|
||||
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
async function runAIAnalysis(force) {
|
||||
const prompt = document.getElementById('aiAnalyzePrompt');
|
||||
const loading = document.getElementById('aiLoading');
|
||||
@ -1922,6 +1934,13 @@ async function runAIAnalysis(force) {
|
||||
if (results) results.style.display = 'none';
|
||||
if (loading) loading.style.display = 'block';
|
||||
|
||||
let seconds = 0;
|
||||
const timerEl = document.getElementById('aiTimer');
|
||||
const timerInterval = setInterval(() => {
|
||||
seconds++;
|
||||
if (timerEl) timerEl.textContent = seconds + 's';
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/audit/analyze', {
|
||||
method: 'POST',
|
||||
@ -1937,20 +1956,36 @@ async function runAIAnalysis(force) {
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
clearInterval(timerInterval);
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
if (data.success) {
|
||||
renderAIResults(data);
|
||||
} else {
|
||||
if (prompt) prompt.style.display = 'block';
|
||||
if (prompt) prompt.style.display = 'none';
|
||||
if (btn) btn.disabled = false;
|
||||
showInfoModal('Blad analizy AI', data.error || 'Wystapil blad', false);
|
||||
const results = document.getElementById('aiResults');
|
||||
results.innerHTML = `
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
|
||||
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Blad analizy AI</p>
|
||||
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(data.error || 'Nieznany blad')}</p>
|
||||
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Sprobuj ponownie</button>
|
||||
</div>`;
|
||||
results.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(timerInterval);
|
||||
if (loading) loading.style.display = 'none';
|
||||
if (prompt) prompt.style.display = 'block';
|
||||
if (prompt) prompt.style.display = 'none';
|
||||
if (btn) btn.disabled = false;
|
||||
showInfoModal('Blad polaczenia', error.message, false);
|
||||
const results = document.getElementById('aiResults');
|
||||
results.innerHTML = `
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
|
||||
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Blad polaczenia</p>
|
||||
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(error.message)}</p>
|
||||
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Sprobuj ponownie</button>
|
||||
</div>`;
|
||||
results.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1961,7 +1996,14 @@ function renderAIResults(data) {
|
||||
const actionsList = document.getElementById('aiActionsList');
|
||||
|
||||
summaryEl.textContent = data.summary || '';
|
||||
cacheInfo.style.display = data.cached ? 'block' : 'none';
|
||||
if (data.cached && data.generated_at) {
|
||||
const d = new Date(data.generated_at);
|
||||
document.getElementById('aiCacheDate').textContent =
|
||||
d.toLocaleDateString('pl-PL') + ' ' + d.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'});
|
||||
cacheInfo.style.display = 'block';
|
||||
} else {
|
||||
cacheInfo.style.display = 'none';
|
||||
}
|
||||
|
||||
actionsList.innerHTML = '';
|
||||
const actions = data.actions || [];
|
||||
@ -2008,6 +2050,7 @@ function renderAIResults(data) {
|
||||
});
|
||||
|
||||
results.style.display = 'block';
|
||||
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||
window._aiActions = actions;
|
||||
}
|
||||
|
||||
@ -2028,18 +2071,36 @@ async function generateContent(actionType, idx) {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success && data.content) {
|
||||
container.innerHTML = `<div class="ai-content-output"><button class="ai-copy-btn" onclick="copyContent(this)">Kopiuj</button><code>${escapeHtml(data.content)}</code></div>`;
|
||||
const isCode = data.content.includes('{') && (data.content.includes('<script') || data.content.includes('"@type"') || data.content.trim().startsWith('{') || data.content.trim().startsWith('<'));
|
||||
if (isCode) {
|
||||
container.innerHTML = `<div class="ai-content-output"><button class="ai-copy-btn" onclick="copyContent(this)">Kopiuj</button><code>${escapeHtml(data.content)}</code></div>`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div style="background: var(--surface); border: 1px solid var(--border); padding: var(--spacing-md); border-radius: var(--radius); margin-top: var(--spacing-md); position: relative; line-height: 1.6; font-size: var(--font-size-sm); color: var(--text-primary);">
|
||||
<button class="ai-copy-btn" style="position: absolute; top: 8px; right: 8px; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text-secondary); padding: 4px 10px; border-radius: var(--radius-sm); font-size: var(--font-size-xs); cursor: pointer;" onclick="copyContent(this)">Kopiuj</button>
|
||||
<div class="ai-markdown-content">${simpleMarkdown(data.content)}</div>
|
||||
</div>`;
|
||||
}
|
||||
container.dataset.loaded = 'true';
|
||||
container.scrollIntoView({behavior: 'smooth', block: 'nearest'});
|
||||
} else {
|
||||
container.innerHTML = `<div style="padding: var(--spacing-sm); color: #ef4444; font-size: var(--font-size-sm);">${escapeHtml(data.error || 'Blad generowania')}</div>`;
|
||||
container.innerHTML = `
|
||||
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
|
||||
<span style="color: #dc2626;">${escapeHtml(data.error || 'Blad generowania')}</span>
|
||||
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
|
||||
</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
container.innerHTML = `<div style="padding: var(--spacing-sm); color: #ef4444; font-size: var(--font-size-sm);">Blad: ${escapeHtml(error.message)}</div>`;
|
||||
container.innerHTML = `
|
||||
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
|
||||
<span style="color: #dc2626;">${escapeHtml(error.message)}</span>
|
||||
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function copyContent(btn) {
|
||||
const code = btn.parentElement.querySelector('code');
|
||||
const code = btn.parentElement.querySelector('code') || btn.parentElement.querySelector('.ai-markdown-content');
|
||||
if (!code) return;
|
||||
navigator.clipboard.writeText(code.textContent).then(() => {
|
||||
const orig = btn.textContent;
|
||||
|
||||
@ -34,7 +34,10 @@
|
||||
<!-- AI Loading Spinner -->
|
||||
<div id="aiLoading" style="display: none; background: var(--surface); padding: var(--spacing-xl); border-radius: var(--radius-lg); box-shadow: var(--shadow); text-align: center;">
|
||||
<div style="width: 40px; height: 40px; border: 3px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto var(--spacing-md);"></div>
|
||||
<p style="color: var(--text-secondary);">Analiza AI w toku... (moze potrwac 5-10 sekund)</p>
|
||||
<p style="color: var(--text-secondary);">
|
||||
Analiza AI w toku... (moze potrwac 15-30 sekund)
|
||||
<span id="aiTimer" style="font-weight: 600;"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- AI Results Container -->
|
||||
@ -48,7 +51,7 @@
|
||||
<p id="aiSummaryText" style="color: var(--text-primary); line-height: 1.6; margin: 0;"></p>
|
||||
</div>
|
||||
<div id="aiCacheInfo" style="display: none; margin-top: var(--spacing-sm); font-size: var(--font-size-xs); color: var(--text-tertiary);">
|
||||
Analiza z cache — <a href="#" onclick="runAIAnalysis(true); return false;" style="color: var(--primary);">Wygeneruj ponownie</a>
|
||||
Analiza z <span id="aiCacheDate"></span> — <a href="#" onclick="runAIAnalysis(true); return false;" style="color: var(--primary);">Wygeneruj ponownie</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -454,15 +454,15 @@
|
||||
<div class="score-details">
|
||||
<div class="score-category" style="color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">
|
||||
{% if score >= 90 %}
|
||||
Doskonaly wynik SEO
|
||||
Doskonaly wynik SEO (Google Lighthouse)
|
||||
{% elif score >= 70 %}
|
||||
Dobry wynik SEO
|
||||
Dobry wynik SEO (Google Lighthouse)
|
||||
{% elif score >= 50 %}
|
||||
Przecietny wynik SEO
|
||||
Przecietny wynik SEO (Google Lighthouse)
|
||||
{% elif score >= 30 %}
|
||||
Wynik SEO wymaga poprawy
|
||||
Wynik SEO wymaga poprawy (Google Lighthouse)
|
||||
{% else %}
|
||||
Slaby wynik SEO
|
||||
Slaby wynik SEO (Google Lighthouse)
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="score-description">
|
||||
@ -476,6 +476,10 @@
|
||||
Strona ma powazne problemy z SEO. Priorytetowo popraw wydajnosc i optymalizacje.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-top: var(--spacing-xs);">
|
||||
Wynik pochodzi z Google PageSpeed Insights i ocenia techniczne aspekty SEO (meta tagi, robots.txt, indeksowalnosc).
|
||||
Pelna ocena SEO, wlaczajac lokalne SEO i widocznosc, jest dostepna w analizie AI ponizej.
|
||||
</p>
|
||||
<div class="audit-meta">
|
||||
<div class="audit-meta-item">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@ -952,6 +956,18 @@ async function runAudit() {
|
||||
const companyId = {{ company.id }};
|
||||
const auditType = 'seo';
|
||||
|
||||
function simpleMarkdown(text) {
|
||||
return text
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
|
||||
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
async function runAIAnalysis(force) {
|
||||
const prompt = document.getElementById('aiAnalyzePrompt');
|
||||
const loading = document.getElementById('aiLoading');
|
||||
@ -963,6 +979,14 @@ async function runAIAnalysis(force) {
|
||||
if (results) results.style.display = 'none';
|
||||
if (loading) loading.style.display = 'block';
|
||||
|
||||
// Start timer
|
||||
let seconds = 0;
|
||||
const timerEl = document.getElementById('aiTimer');
|
||||
const timerInterval = setInterval(() => {
|
||||
seconds++;
|
||||
if (timerEl) timerEl.textContent = seconds + 's';
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/audit/analyze', {
|
||||
method: 'POST',
|
||||
@ -978,20 +1002,36 @@ async function runAIAnalysis(force) {
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
clearInterval(timerInterval);
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
if (data.success) {
|
||||
renderAIResults(data);
|
||||
} else {
|
||||
if (prompt) prompt.style.display = 'block';
|
||||
if (prompt) prompt.style.display = 'none';
|
||||
if (btn) btn.disabled = false;
|
||||
showInfoModal('Blad analizy AI', data.error || 'Wystapil blad', false);
|
||||
const results = document.getElementById('aiResults');
|
||||
results.innerHTML = `
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
|
||||
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Blad analizy AI</p>
|
||||
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(data.error || 'Nieznany blad')}</p>
|
||||
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Sprobuj ponownie</button>
|
||||
</div>`;
|
||||
results.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(timerInterval);
|
||||
if (loading) loading.style.display = 'none';
|
||||
if (prompt) prompt.style.display = 'block';
|
||||
if (prompt) prompt.style.display = 'none';
|
||||
if (btn) btn.disabled = false;
|
||||
showInfoModal('Blad polaczenia', error.message, false);
|
||||
const results = document.getElementById('aiResults');
|
||||
results.innerHTML = `
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
|
||||
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Blad polaczenia</p>
|
||||
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(error.message)}</p>
|
||||
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Sprobuj ponownie</button>
|
||||
</div>`;
|
||||
results.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1002,7 +1042,10 @@ function renderAIResults(data) {
|
||||
const actionsList = document.getElementById('aiActionsList');
|
||||
|
||||
summaryEl.textContent = data.summary || '';
|
||||
if (data.cached) {
|
||||
if (data.cached && data.generated_at) {
|
||||
const d = new Date(data.generated_at);
|
||||
document.getElementById('aiCacheDate').textContent =
|
||||
d.toLocaleDateString('pl-PL') + ' ' + d.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'});
|
||||
cacheInfo.style.display = 'block';
|
||||
} else {
|
||||
cacheInfo.style.display = 'none';
|
||||
@ -1057,6 +1100,7 @@ function renderAIResults(data) {
|
||||
});
|
||||
|
||||
results.style.display = 'block';
|
||||
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||
|
||||
// Store actions data for content generation
|
||||
window._aiActions = actions;
|
||||
@ -1092,23 +1136,42 @@ async function generateContent(actionType, idx) {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.content) {
|
||||
container.innerHTML = `
|
||||
<div class="ai-content-output">
|
||||
<button class="ai-copy-btn" onclick="copyContent(this)">Kopiuj</button>
|
||||
<code>${escapeHtml(data.content)}</code>
|
||||
</div>
|
||||
`;
|
||||
const isCode = data.content.includes('{') && (data.content.includes('<script') || data.content.includes('"@type"') || data.content.trim().startsWith('{') || data.content.trim().startsWith('<'));
|
||||
if (isCode) {
|
||||
container.innerHTML = `
|
||||
<div class="ai-content-output">
|
||||
<button class="ai-copy-btn" onclick="copyContent(this)">Kopiuj</button>
|
||||
<code>${escapeHtml(data.content)}</code>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div style="background: var(--surface); border: 1px solid var(--border); padding: var(--spacing-md); border-radius: var(--radius); margin-top: var(--spacing-md); position: relative; line-height: 1.6; font-size: var(--font-size-sm); color: var(--text-primary);">
|
||||
<button class="ai-copy-btn" style="position: absolute; top: 8px; right: 8px; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text-secondary); padding: 4px 10px; border-radius: var(--radius-sm); font-size: var(--font-size-xs); cursor: pointer;" onclick="copyContent(this)">Kopiuj</button>
|
||||
<div class="ai-markdown-content">${simpleMarkdown(data.content)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
container.dataset.loaded = 'true';
|
||||
container.scrollIntoView({behavior: 'smooth', block: 'nearest'});
|
||||
} else {
|
||||
container.innerHTML = `<div style="padding: var(--spacing-sm); color: #ef4444; font-size: var(--font-size-sm);">${escapeHtml(data.error || 'Blad generowania')}</div>`;
|
||||
container.innerHTML = `
|
||||
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
|
||||
<span style="color: #dc2626;">${escapeHtml(data.error || 'Blad generowania')}</span>
|
||||
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
|
||||
</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
container.innerHTML = `<div style="padding: var(--spacing-sm); color: #ef4444; font-size: var(--font-size-sm);">Blad: ${escapeHtml(error.message)}</div>`;
|
||||
container.innerHTML = `
|
||||
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
|
||||
<span style="color: #dc2626;">${escapeHtml(error.message)}</span>
|
||||
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function copyContent(btn) {
|
||||
const code = btn.parentElement.querySelector('code');
|
||||
const code = btn.parentElement.querySelector('code') || btn.parentElement.querySelector('.ai-markdown-content');
|
||||
if (!code) return;
|
||||
|
||||
navigator.clipboard.writeText(code.textContent).then(() => {
|
||||
|
||||
@ -1361,6 +1361,18 @@ document.getElementById('modalOverlay').addEventListener('click', function(e) {
|
||||
const companyId = {{ company.id }};
|
||||
const auditType = 'social';
|
||||
|
||||
function simpleMarkdown(text) {
|
||||
return text
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
|
||||
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
async function runAIAnalysis(force) {
|
||||
const prompt = document.getElementById('aiAnalyzePrompt');
|
||||
const loading = document.getElementById('aiLoading');
|
||||
@ -1372,6 +1384,13 @@ async function runAIAnalysis(force) {
|
||||
if (results) results.style.display = 'none';
|
||||
if (loading) loading.style.display = 'block';
|
||||
|
||||
let seconds = 0;
|
||||
const timerEl = document.getElementById('aiTimer');
|
||||
const timerInterval = setInterval(() => {
|
||||
seconds++;
|
||||
if (timerEl) timerEl.textContent = seconds + 's';
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/audit/analyze', {
|
||||
method: 'POST',
|
||||
@ -1387,20 +1406,36 @@ async function runAIAnalysis(force) {
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
clearInterval(timerInterval);
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
if (data.success) {
|
||||
renderAIResults(data);
|
||||
} else {
|
||||
if (prompt) prompt.style.display = 'block';
|
||||
if (prompt) prompt.style.display = 'none';
|
||||
if (btn) btn.disabled = false;
|
||||
showModal('Blad analizy AI', data.error || 'Wystapil blad', false);
|
||||
const results = document.getElementById('aiResults');
|
||||
results.innerHTML = `
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
|
||||
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Blad analizy AI</p>
|
||||
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(data.error || 'Nieznany blad')}</p>
|
||||
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Sprobuj ponownie</button>
|
||||
</div>`;
|
||||
results.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(timerInterval);
|
||||
if (loading) loading.style.display = 'none';
|
||||
if (prompt) prompt.style.display = 'block';
|
||||
if (prompt) prompt.style.display = 'none';
|
||||
if (btn) btn.disabled = false;
|
||||
showModal('Blad polaczenia', error.message, false);
|
||||
const results = document.getElementById('aiResults');
|
||||
results.innerHTML = `
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
|
||||
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Blad polaczenia</p>
|
||||
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(error.message)}</p>
|
||||
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Sprobuj ponownie</button>
|
||||
</div>`;
|
||||
results.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1411,7 +1446,14 @@ function renderAIResults(data) {
|
||||
const actionsList = document.getElementById('aiActionsList');
|
||||
|
||||
summaryEl.textContent = data.summary || '';
|
||||
cacheInfo.style.display = data.cached ? 'block' : 'none';
|
||||
if (data.cached && data.generated_at) {
|
||||
const d = new Date(data.generated_at);
|
||||
document.getElementById('aiCacheDate').textContent =
|
||||
d.toLocaleDateString('pl-PL') + ' ' + d.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'});
|
||||
cacheInfo.style.display = 'block';
|
||||
} else {
|
||||
cacheInfo.style.display = 'none';
|
||||
}
|
||||
|
||||
actionsList.innerHTML = '';
|
||||
const actions = data.actions || [];
|
||||
@ -1458,6 +1500,7 @@ function renderAIResults(data) {
|
||||
});
|
||||
|
||||
results.style.display = 'block';
|
||||
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||
window._aiActions = actions;
|
||||
}
|
||||
|
||||
@ -1478,18 +1521,36 @@ async function generateContent(actionType, idx) {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success && data.content) {
|
||||
container.innerHTML = `<div class="ai-content-output"><button class="ai-copy-btn" onclick="copyContent(this)">Kopiuj</button><code>${escapeHtml(data.content)}</code></div>`;
|
||||
const isCode = data.content.includes('{') && (data.content.includes('<script') || data.content.includes('"@type"') || data.content.trim().startsWith('{') || data.content.trim().startsWith('<'));
|
||||
if (isCode) {
|
||||
container.innerHTML = `<div class="ai-content-output"><button class="ai-copy-btn" onclick="copyContent(this)">Kopiuj</button><code>${escapeHtml(data.content)}</code></div>`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div style="background: var(--surface); border: 1px solid var(--border); padding: var(--spacing-md); border-radius: var(--radius); margin-top: var(--spacing-md); position: relative; line-height: 1.6; font-size: var(--font-size-sm); color: var(--text-primary);">
|
||||
<button class="ai-copy-btn" style="position: absolute; top: 8px; right: 8px; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text-secondary); padding: 4px 10px; border-radius: var(--radius-sm); font-size: var(--font-size-xs); cursor: pointer;" onclick="copyContent(this)">Kopiuj</button>
|
||||
<div class="ai-markdown-content">${simpleMarkdown(data.content)}</div>
|
||||
</div>`;
|
||||
}
|
||||
container.dataset.loaded = 'true';
|
||||
container.scrollIntoView({behavior: 'smooth', block: 'nearest'});
|
||||
} else {
|
||||
container.innerHTML = `<div style="padding: var(--spacing-sm); color: #ef4444; font-size: var(--font-size-sm);">${escapeHtml(data.error || 'Blad generowania')}</div>`;
|
||||
container.innerHTML = `
|
||||
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
|
||||
<span style="color: #dc2626;">${escapeHtml(data.error || 'Blad generowania')}</span>
|
||||
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
|
||||
</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
container.innerHTML = `<div style="padding: var(--spacing-sm); color: #ef4444; font-size: var(--font-size-sm);">Blad: ${escapeHtml(error.message)}</div>`;
|
||||
container.innerHTML = `
|
||||
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
|
||||
<span style="color: #dc2626;">${escapeHtml(error.message)}</span>
|
||||
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function copyContent(btn) {
|
||||
const code = btn.parentElement.querySelector('code');
|
||||
const code = btn.parentElement.querySelector('code') || btn.parentElement.querySelector('.ai-markdown-content');
|
||||
if (!code) return;
|
||||
navigator.clipboard.writeText(code.textContent).then(() => {
|
||||
const orig = btn.textContent;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user