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

- 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:
Maciej Pienczyn 2026-02-07 13:42:24 +01:00
parent 8ad6299381
commit 0c6cd09d18
5 changed files with 236 additions and 43 deletions

View File

@ -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,

View File

@ -1911,6 +1911,18 @@ async function runAudit() {
const companyId = {{ company.id }};
const auditType = 'gbp';
function simpleMarkdown(text) {
return text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.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;

View File

@ -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 &mdash; <a href="#" onclick="runAIAnalysis(true); return false;" style="color: var(--primary);">Wygeneruj ponownie</a>
Analiza z <span id="aiCacheDate"></span> &mdash; <a href="#" onclick="runAIAnalysis(true); return false;" style="color: var(--primary);">Wygeneruj ponownie</a>
</div>
</div>

View File

@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.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(() => {

View File

@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.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;