mirror of
https://github.com/fsecada01/Pygentic-AI.git
synced 2026-05-11 19:54:59 +00:00
feat: comprehensive accessibility improvements for visually impaired users
Implemented WCAG 2.1 AA compliant accessibility enhancements: ARIA & Semantic HTML: - Added semantic landmarks (main, section, article, role attributes) - Comprehensive ARIA labels and descriptions throughout - ARIA live regions for dynamic content updates (polite/assertive) - Screen reader-only text for context (.sr-only class) - Proper heading hierarchy with IDs for navigation Keyboard Navigation: - Full keyboard support for SWOT cards (Arrow keys, Home, End) - Skip to main content link for keyboard users - Focus management: auto-focus results heading after analysis - Focus visible styles already in place from SCSS Screen Reader Enhancements: - announceToScreenReader() function for status updates - ARIA live regions on spinner, status timeline, results - Descriptive labels for icons (aria-hidden for decorative) - Category descriptions (e.g., "Strengths - positive internal factors") Interactive Elements: - Enhanced button and link labels - Progress indicators with aria-busy and role="progressbar" - Modal dialog attributes for spinner overlay - Feed role for status timeline updates Files Modified: - result.html: SWOT cards, summary section with full ARIA support - app.js: Screen reader announcements, keyboard nav, focus management - base.html: Skip link and semantic main element - EmptyState.jinja: role="status" with aria-live - ErrorState.jinja: role="alert" for error announcements - Spinner.jinja: Modal dialog with progress indicator - status.html: Feed role for timeline updates - _components.scss: sr-only and skip-link utility classes These improvements ensure the SWOT analyzer is fully accessible to visually impaired users using screen readers and keyboard navigation. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -3,6 +3,38 @@
|
||||
// Reusable UI components
|
||||
// ============================================
|
||||
|
||||
// Accessibility
|
||||
// ===================================
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: $brand-primary;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
z-index: 10000;
|
||||
border-radius: 0 0 $radius-md 0;
|
||||
transition: top $transition-fast;
|
||||
|
||||
&:focus {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading Spinner
|
||||
// ===================================
|
||||
.spinner-wrapper {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["../../scss/styles.scss","../../scss/_variables.scss","../../scss/_typography.scss","../../scss/_animations.scss","../../scss/_components.scss","../../scss/_layout.scss","../../scss/_states.scss","../../scss/_responsive.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA,GCMQ,gICFR,KACE,YDIU,8FCHV,kDACA,mCACA,kCAGF,mCAGE,YDJa,mCCKb,uBACA,gBAIF,YACE,iCACA,gBACA,WDmBkB,kDClBlB,6BACA,sCACA,qBACA,mBACA,gBCxBF,gBACE,KACE,uBAEF,GACE,0BAIJ,iBACE,QACE,0BAEF,IACE,6BAIJ,iBACE,QACE,uCAEF,IACE,0CAIJ,mBACE,KACE,UACA,2BAEF,GACE,UACA,yBAIJ,kBACE,KACE,UAEF,GACE,WAIJ,iBACE,gCACA,gCACA,gCAGF,qBACE,QACE,mBAEF,IACE,sBAIJ,2BACE,KACE,UACA,2BAEF,GACE,UACA,yBAIJ,wBACE,KACE,UACA,4BAEF,GACE,UACA,yBAIJ,uBACE,QACE,WF3CQ,6DE6CV,IACE,2CCtFJ,iBACE,eACA,MACA,OACA,WACA,YACA,qCACA,0BACA,aACA,uBACA,mBACA,aAGF,iBACE,kBAGF,QACE,qCACA,6BACA,kBACA,WACA,YACA,mCACA,cAGF,cACE,kBAEA,iBACE,kBACA,gBACA,MHHU,QGIV,oBAGF,gBACE,kBACA,MHZU,QGkBd,kBACE,gBACA,cACA,kBAGF,aACE,WAGF,oBACE,aACA,mBACA,gBACA,cHSY,OGRZ,cACA,WHpBU,iEGqBV,kDAEA,iCACE,2CACA,2BAIJ,aACE,eACA,MH/CY,QGgDZ,kBAGF,cACE,OACA,YACA,aACA,qBACA,eACA,yBACA,MHrDY,QGuDZ,2BACE,MH7DU,QGiEd,eACE,WHtFc,QGuFd,WACA,YACA,cH1BY,OG2BZ,qBACA,gBACA,eACA,eACA,aACA,mBACA,UACA,kDACA,mBAEA,qBACE,WHnGiB,QGoGjB,0BACA,WHpEQ,+DGuEV,sBACE,sBAGF,qBACE,0BACA,mBAGF,0BACE,kBACA,oBACA,oBAEA,iCACE,WACA,kBACA,WACA,YACA,QACA,SACA,iBACA,gBACA,+BACA,sBACA,kBACA,mCAKN,aACE,gBACA,kBACA,MHtHY,QGuHZ,kBAEA,eACE,oBACA,MHjJY,QGsJhB,2CAKE,kDAIF,eACE,0BA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","file":"pygentic_ai.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["../../scss/styles.scss","../../scss/_variables.scss","../../scss/_typography.scss","../../scss/_animations.scss","../../scss/_components.scss","../../scss/_layout.scss","../../scss/_states.scss","../../scss/_responsive.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA,GCMQ,gICFR,KACE,YDIU,8FCHV,kDACA,mCACA,kCAGF,mCAGE,YDJa,mCCKb,uBACA,gBAIF,YACE,iCACA,gBACA,WDmBkB,kDClBlB,6BACA,sCACA,qBACA,mBACA,gBCxBF,gBACE,KACE,uBAEF,GACE,0BAIJ,iBACE,QACE,0BAEF,IACE,6BAIJ,iBACE,QACE,uCAEF,IACE,0CAIJ,mBACE,KACE,UACA,2BAEF,GACE,UACA,yBAIJ,kBACE,KACE,UAEF,GACE,WAIJ,iBACE,gCACA,gCACA,gCAGF,qBACE,QACE,mBAEF,IACE,sBAIJ,2BACE,KACE,UACA,2BAEF,GACE,UACA,yBAIJ,wBACE,KACE,UACA,4BAEF,GACE,UACA,yBAIJ,uBACE,QACE,WF3CQ,6DE6CV,IACE,2CCtFJ,SACE,kBACA,UACA,WACA,UACA,YACA,gBACA,sBACA,mBACA,eAGF,WACE,kBACA,UACA,OACA,WHVc,QGWd,WACA,sBACA,qBACA,gBACA,cACA,yBACA,kDAEA,iBACE,MAMJ,iBACE,eACA,MACA,OACA,WACA,YACA,qCACA,0BACA,aACA,uBACA,mBACA,aAGF,iBACE,kBAGF,QACE,qCACA,6BACA,kBACA,WACA,YACA,mCACA,cAGF,cACE,kBAEA,iBACE,kBACA,gBACA,MHnCU,QGoCV,oBAGF,gBACE,kBACA,MH5CU,QGkDd,kBACE,gBACA,cACA,kBAGF,aACE,WAGF,oBACE,aACA,mBACA,gBACA,cHvBY,OGwBZ,cACA,WHpDU,iEGqDV,kDAEA,iCACE,2CACA,2BAIJ,aACE,eACA,MH/EY,QGgFZ,kBAGF,cACE,OACA,YACA,aACA,qBACA,eACA,yBACA,MHrFY,QGuFZ,2BACE,MH7FU,QGiGd,eACE,WHtHc,QGuHd,WACA,YACA,cH1DY,OG2DZ,qBACA,gBACA,eACA,eACA,aACA,mBACA,UACA,kDACA,mBAEA,qBACE,WHnIiB,QGoIjB,0BACA,WHpGQ,+DGuGV,sBACE,sBAGF,qBACE,0BACA,mBAGF,0BACE,kBACA,oBACA,oBAEA,iCACE,WACA,kBACA,WACA,YACA,QACA,SACA,iBACA,gBACA,+BACA,sBACA,kBACA,mCAKN,aACE,gBACA,kBACA,MHtJY,QGuJZ,kBAEA,eACE,oBACA,MHjLY,QGsLhB,2CAKE,kDAIF,eACE,0BACA,mBCvMF,eACE,wEACA,kBACA,gBAIA,eACE,+BACA,wCAEA,qBACE,wCACA,8EAKN,kBACE,4BAKF,WACE,aACA,2DACA,IJiCW,OIhCX,WJiCW,KI9Bb,WACE,gBACA,cJkCU,KIjCV,QJ0BW,OIzBX,WJMU,+DILV,kDACA,uCACA,yCAEA,iBACE,2BACA,WJCS,kCICT,kCACE,iCAKJ,qBACE,sBAGF,qBACE,sBAGF,wBACE,sBAGF,mBACE,sBAIJ,mBACE,aACA,mBACA,WACA,mBACA,oBACA,gCAGF,iBACE,WACA,YACA,cJbU,KIcV,6BACA,aACA,mBACA,uBACA,WACA,iBACA,cAGF,kBACE,gBACA,mBACA,MJ7DY,QI8DZ,YACA,SAGF,kBACE,WJ3EY,QI4EZ,sBACA,cJhCU,KIiCV,kBACA,gBACA,MJ1EY,QI2EZ,cAGF,iBACE,gBAGF,WACE,gBACA,UACA,SAGF,iBACE,aACA,uBACA,WACA,iBACA,gCACA,wCAEA,4BACE,mBAKA,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,+BACE,mBAKN,mBACE,UACA,WACA,kBACA,6BACA,iBACA,cAGF,iBACE,OACA,MJvHY,QIwHZ,gBAKA,wBACE,qBADF,wBACE,qBADF,wBACE,qBADF,wBACE,qBAKJ,6BACE,uCAKF,iBACE,kBACA,kBAEA,yBACE,WACA,kBACA,UACA,SACA,YACA,UACA,WJzJU,QI6Jd,aACE,kBACA,yBACA,oCAIE,mFAEE,MJ9Ka,QImLf,yFAEE,MJvLU,QI0LZ,4CACE,8CAKF,yFAEE,MJpMU,QIyMZ,qFAEE,MJrMQ,QI0Md,wBACE,kBACA,OACA,YACA,WACA,YACA,kBACA,aACA,mBACA,uBACA,eACA,gBACA,0BACA,UAGF,sBACE,gBACA,cJ5KU,KI6KV,qBACA,WJxMU,6DIyMV,kDAGF,yCACE,WJ5MU,+DI6MV,0BAGF,qBACE,gBACA,kBACA,yBACA,qBACA,oBAGF,sBACE,MJrOY,QIsOZ,gBACA,mBCpQF,aACE,aACA,mBACA,uBACA,iBACA,oBAGF,sBACE,kBACA,gBAGF,mBACE,YACA,aACA,mBACA,kBACA,6DACA,aACA,mBACA,uBACA,eACA,MLGY,QKFZ,wCAGF,oBACE,kBACA,gBACA,cACA,qBAGF,0BACE,mBACA,MLRY,QKSZ,gBACA,qBAGF,kBACE,oBACA,mBACA,UACA,qBACA,WLxCc,QKyCd,WACA,cLqBY,OKpBZ,qBACA,gBACA,kDACA,WLZU,6DKcV,wBACE,WL/CiB,QKgDjB,2BACA,WLhBQ,+DKiBR,WAMJ,aACE,aACA,mBACA,uBACA,iBACA,oBAGF,sBACE,kBACA,gBAGF,mBACE,YACA,aACA,mBACA,kBACA,6DACA,aACA,mBACA,uBACA,eACA,MLrEY,QKsEZ,gCAGF,oBACE,kBACA,gBACA,MLhEY,QKiEZ,qBAGF,0BACE,mBACA,MLxEY,QKyEZ,gBACA,qBAGF,sBACE,WLnFY,QKoFZ,cLzCU,KK0CV,aACA,gBACA,oCACA,kBACA,MLnFY,QKoFZ,gBACA,gBAGF,sBACE,aACA,SACA,uBACA,eAGF,qBACE,oBACA,mBACA,UACA,qBACA,cL5DY,OK6DZ,gBACA,kDACA,WL5FU,6DK6FV,eACA,YACA,qBAEA,8BACE,WLpIY,QKqIZ,WAEA,oCACE,WLtIe,QKuIf,2BACA,WLvGM,+DKwGN,WAIJ,gCACE,gBACA,ML1HU,QK2HV,yBAEA,sCACE,WLpIQ,QKqIR,aLlIQ,QKmIR,2BACA,MLhIQ,QM/Bd,yBACE,oBACE,mBAGF,WACE,0BAGF,YACE,eAGF,oBACE,sBACA,UACA,cNqDQ,KMpDR,aAGF,cACE,WACA,kBAGF,eACE,WACA,uBAGF,mBACE,eAGF,kBACE,SACA,kBAKJ,yBACE,WACE,aAGF,iBACE,WACA,YACA,kBAGF,kBACE,mBAGF,YACE,kBAGF,iBACE,oBAGF,aACE,qBP3CJ,MAEE,yBACA,+BACA,8BAGA,yBACA,yBACA,4BACA,uBAGA,sBACA,uBACA,uBACA,uBACA,uBACA,uBACA,uBACA,uBACA,uBACA,uBAGA,6CACA,mFACA,qFACA,uFACA,oDAGA,sDACA,sDACA","file":"pygentic_ai.css"}
|
||||
@ -71,15 +71,47 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth scroll to results
|
||||
* Announce to screen readers
|
||||
*/
|
||||
function announceToScreenReader(message, priority = 'polite') {
|
||||
const announcement = document.createElement('div');
|
||||
announcement.setAttribute('role', 'status');
|
||||
announcement.setAttribute('aria-live', priority);
|
||||
announcement.setAttribute('aria-atomic', 'true');
|
||||
announcement.className = 'sr-only';
|
||||
announcement.textContent = message;
|
||||
|
||||
document.body.appendChild(announcement);
|
||||
|
||||
// Remove after announcement is made
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(announcement);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth scroll to results and manage focus
|
||||
*/
|
||||
function scrollToResults() {
|
||||
const resultsSection = document.getElementById('result-container');
|
||||
const resultsHeading = document.getElementById('results-heading');
|
||||
|
||||
if (resultsSection) {
|
||||
// Announce completion to screen readers
|
||||
announceToScreenReader('Analysis complete. Results are now available.', 'assertive');
|
||||
|
||||
// Scroll to results
|
||||
resultsSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
|
||||
// Move focus to results heading for keyboard users
|
||||
if (resultsHeading) {
|
||||
setTimeout(() => {
|
||||
resultsHeading.focus();
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,6 +128,44 @@
|
||||
return pollInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize keyboard navigation for SWOT cards
|
||||
*/
|
||||
function initializeKeyboardNavigation() {
|
||||
const cards = document.querySelectorAll('.swot-card');
|
||||
|
||||
cards.forEach((card, index) => {
|
||||
card.addEventListener('keydown', function(e) {
|
||||
let targetCard = null;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
targetCard = cards[index + 1] || cards[0];
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
targetCard = cards[index - 1] || cards[cards.length - 1];
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
targetCard = cards[0];
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
targetCard = cards[cards.length - 1];
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetCard) {
|
||||
targetCard.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize form submission handler
|
||||
*/
|
||||
@ -104,6 +174,9 @@
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Announce to screen readers
|
||||
announceToScreenReader('Analysis started. Please wait while we process your request.', 'assertive');
|
||||
|
||||
// Start loading messages when form is submitted
|
||||
startLoadingMessages();
|
||||
|
||||
@ -135,6 +208,9 @@
|
||||
spinner.classList.add('is-hidden');
|
||||
}
|
||||
|
||||
// Initialize keyboard navigation for SWOT cards
|
||||
initializeKeyboardNavigation();
|
||||
|
||||
// Scroll to results after a brief delay
|
||||
setTimeout(scrollToResults, 500);
|
||||
|
||||
@ -219,6 +295,7 @@
|
||||
window.PygenticAI = {
|
||||
startLoadingMessages,
|
||||
stopLoadingMessages,
|
||||
scrollToResults
|
||||
scrollToResults,
|
||||
announceToScreenReader
|
||||
};
|
||||
})();
|
||||
|
||||
@ -8,9 +8,13 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a href="#main-content"
|
||||
class="skip-link">Skip to main content</a>
|
||||
{% include "components/main/nav.html" %}
|
||||
{% block content%}
|
||||
{% endblock %}
|
||||
<main id="main-content">
|
||||
{% block content%}
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% block js_content %}
|
||||
{% endblock js_content %}
|
||||
{% include "components/snippets/js.html" %}
|
||||
|
||||
@ -4,9 +4,12 @@
|
||||
icon: str = "fa-chart-simple"
|
||||
#}
|
||||
|
||||
<div class="empty-state">
|
||||
<div class="empty-state"
|
||||
role="status"
|
||||
aria-live="polite">
|
||||
<div class="empty-state__content">
|
||||
<div class="empty-state__icon">
|
||||
<div class="empty-state__icon"
|
||||
aria-hidden="true">
|
||||
<i class="fas {{ icon }}"></i>
|
||||
</div>
|
||||
<h3 class="empty-state__title">{{ title }}</h3>
|
||||
|
||||
@ -5,16 +5,21 @@
|
||||
show_retry: bool = True
|
||||
#}
|
||||
|
||||
<div class="error-state">
|
||||
<div class="error-state"
|
||||
role="alert"
|
||||
aria-live="assertive">
|
||||
<div class="error-state__content">
|
||||
<div class="error-state__icon">
|
||||
<div class="error-state__icon"
|
||||
aria-hidden="true">
|
||||
<i class="fas fa-triangle-exclamation"></i>
|
||||
</div>
|
||||
<h3 class="error-state__title">{{ title }}</h3>
|
||||
<p class="error-state__description">{{ description }}</p>
|
||||
|
||||
{% if error_details %}
|
||||
<div class="error-state__details">
|
||||
<div class="error-state__details"
|
||||
role="region"
|
||||
aria-label="Error details">
|
||||
{{ error_details }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -22,14 +27,18 @@
|
||||
<div class="error-state__actions">
|
||||
{% if show_retry %}
|
||||
<button class="error-state__button error-state__button--primary"
|
||||
onclick="location.reload()">
|
||||
<i class="fas fa-rotate-right"></i>
|
||||
onclick="location.reload()"
|
||||
aria-label="Try analyzing again">
|
||||
<i class="fas fa-rotate-right"
|
||||
aria-hidden="true"></i>
|
||||
<span>Try Again</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="/"
|
||||
class="error-state__button error-state__button--secondary">
|
||||
<i class="fas fa-home"></i>
|
||||
class="error-state__button error-state__button--secondary"
|
||||
aria-label="Return to home page">
|
||||
<i class="fas fa-home"
|
||||
aria-hidden="true"></i>
|
||||
<span>Go Home</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
<div class="spinner-wrapper is-overlay is-hidden"
|
||||
id="spinner">
|
||||
id="spinner"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="spinner-title"
|
||||
aria-describedby="loading-status">
|
||||
<div class="loading-content">
|
||||
<div class="loader"></div>
|
||||
<div class="loader"
|
||||
role="progressbar"
|
||||
aria-label="Loading in progress"
|
||||
aria-busy="true"></div>
|
||||
<div class="loading-text">
|
||||
<h3>Analyzing...</h3>
|
||||
<p id="loading-status">Fetching URL content</p>
|
||||
<h3 id="spinner-title">Analyzing...</h3>
|
||||
<p id="loading-status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true">Fetching URL content</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,52 +1,78 @@
|
||||
{% if result %}
|
||||
<section class="section"
|
||||
id="result-container">
|
||||
id="result-container"
|
||||
aria-live="polite"
|
||||
aria-atomic="false"
|
||||
role="region"
|
||||
aria-label="SWOT Analysis Results">
|
||||
<div class="container">
|
||||
<h2 class="title is-2 has-text-centered mb-6">Analysis Complete ✨</h2>
|
||||
<div class="swot-grid">
|
||||
<h2 class="title is-2 has-text-centered mb-6"
|
||||
id="results-heading"
|
||||
tabindex="-1">
|
||||
<span aria-label="Analysis Complete">Analysis Complete</span> ✨
|
||||
</h2>
|
||||
<div class="swot-grid"
|
||||
role="list"
|
||||
aria-label="SWOT Analysis Categories">
|
||||
{% for cat, val in result.dict().items() %}
|
||||
{% if cat != 'summary' %}
|
||||
{# Determine card class and icon based on category #}
|
||||
{% set card_class = 'strength' if cat == 'strengths' else ('weakness' if cat == 'weaknesses' else ('opportunity' if cat == 'opportunities' else 'threat')) %}
|
||||
{% set icon = 'fa-arrow-trend-up' if cat == 'strengths' else ('fa-arrow-trend-down' if cat == 'weaknesses' else ('fa-lightbulb' if cat == 'opportunities' else 'fa-triangle-exclamation')) %}
|
||||
{% set category_label = 'Strengths - positive internal factors' if cat == 'strengths' else ('Weaknesses - negative internal factors' if cat == 'weaknesses' else ('Opportunities - positive external factors' if cat == 'opportunities' else 'Threats - negative external factors')) %}
|
||||
|
||||
<div class="swot-card swot-card--{{ card_class }}">
|
||||
<article class="swot-card swot-card--{{ card_class }}"
|
||||
role="listitem"
|
||||
aria-labelledby="card-title-{{ cat }}"
|
||||
tabindex="0">
|
||||
<div class="swot-card__header">
|
||||
<div class="swot-card__icon">
|
||||
<div class="swot-card__icon"
|
||||
aria-hidden="true">
|
||||
<i class="fas {{ icon }}"></i>
|
||||
</div>
|
||||
<h3 class="swot-card__title">{{ cat.title() }}</h3>
|
||||
<span class="swot-card__count">{{ val|length }}</span>
|
||||
<h3 class="swot-card__title"
|
||||
id="card-title-{{ cat }}">
|
||||
{{ cat.title() }}
|
||||
<span class="sr-only">: {{ category_label }}</span>
|
||||
</h3>
|
||||
<span class="swot-card__count"
|
||||
aria-label="{{ val|length }} items">{{ val|length }}</span>
|
||||
</div>
|
||||
<div class="swot-card__body">
|
||||
<ul class="swot-list">
|
||||
<ul class="swot-list"
|
||||
aria-label="{{ cat.title() }} list">
|
||||
{% for value in val %}
|
||||
<li class="swot-list__item">
|
||||
<span class="swot-list__bullet"></span>
|
||||
<span class="swot-list__bullet"
|
||||
aria-hidden="true"></span>
|
||||
<span class="swot-list__text">{{ value }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Summary Section #}
|
||||
{% if result.summary %}
|
||||
<div class="box mt-6"
|
||||
style="border-radius: 16px; border-left: 4px solid var(--brand-primary); box-shadow: var(--shadow-lg);">
|
||||
<section class="box mt-6"
|
||||
role="region"
|
||||
aria-labelledby="summary-heading"
|
||||
style="border-radius: 16px; border-left: 4px solid var(--brand-primary); box-shadow: var(--shadow-lg);">
|
||||
<h3 class="title is-4"
|
||||
id="summary-heading"
|
||||
style="color: var(--brand-primary);">
|
||||
<i class="fas fa-clipboard-list mr-2"></i>
|
||||
<i class="fas fa-clipboard-list mr-2"
|
||||
aria-hidden="true"></i>
|
||||
Executive Summary
|
||||
</h3>
|
||||
<div class="content"
|
||||
style="color: var(--neutral-700); line-height: 1.8;">
|
||||
{{ result.summary }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1,15 +1,26 @@
|
||||
{% if messages %}
|
||||
<section class="section" id="status-container">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<div class="status-timeline">
|
||||
<section class="section"
|
||||
id="status-container"
|
||||
role="region"
|
||||
aria-label="Analysis Progress"
|
||||
aria-live="polite">
|
||||
<div class="container"
|
||||
style="max-width: 800px;">
|
||||
<div class="status-timeline"
|
||||
role="feed"
|
||||
aria-label="Status updates">
|
||||
{% for message in messages %}
|
||||
{% set is_error = message.startswith('Error:') %}
|
||||
{% set is_loading = loop.last and not result %}
|
||||
{% set is_tool_message = message.startswith('Using tool') %}
|
||||
{% set is_complete = "complete" in message.lower() or "done" in message.lower() %}
|
||||
{% set status_label = 'Error' if is_error else ('Complete' if is_complete else ('In Progress' if is_loading else ('Processing' if is_tool_message else 'Status'))) %}
|
||||
|
||||
<div class="status-item {% if is_error %}status-item--error{% elif is_complete %}status-item--success{% elif is_loading %}status-item--loading{% else %}status-item--info{% endif %}">
|
||||
<div class="status-item__indicator">
|
||||
<article class="status-item {% if is_error %}status-item--error{% elif is_complete %}status-item--success{% elif is_loading %}status-item--loading{% else %}status-item--info{% endif %}"
|
||||
role="article"
|
||||
aria-label="{{ status_label }}: {{ message }}">
|
||||
<div class="status-item__indicator"
|
||||
aria-hidden="true">
|
||||
{% if is_error %}
|
||||
<i class="fas fa-circle-xmark"></i>
|
||||
{% elif is_complete %}
|
||||
@ -22,25 +33,15 @@
|
||||
</div>
|
||||
<div class="status-item__content">
|
||||
<div class="status-item__header">
|
||||
{% if is_error %}
|
||||
Error
|
||||
{% elif is_complete %}
|
||||
Complete
|
||||
{% elif is_loading %}
|
||||
In Progress
|
||||
{% elif is_tool_message %}
|
||||
Processing
|
||||
{% else %}
|
||||
Status
|
||||
{% endif %}
|
||||
{{ status_label }}
|
||||
</div>
|
||||
<div class="status-item__message">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user