mirror of
https://github.com/fsecada01/Pygentic-AI.git
synced 2026-05-13 12:44:59 +00:00
feat: migrate frontend styling to SCSS with modular architecture
- Implement SCSS with 7 modular partials for better maintainability: - _variables.scss: Complete design token system - _typography.scss: Font hierarchy and styles - _animations.scss: Reusable keyframe animations - _components.scss: UI components (spinner, search, buttons) - _layout.scss: Page structure (hero, SWOT cards, status timeline) - _states.scss: Empty and error state components - _responsive.scss: Mobile-first breakpoints - Add build scripts for SCSS compilation and watch mode - Add EmptyState and ErrorState components for better UX - Enhance templates with progressive loading and accessibility - Configure npm with local registry override (.npmrc) - Add app.js for progressive loading and smooth interactions Benefits: - Better code organization with partial separation - CSS custom properties for runtime theming - BEM naming convention for maintainability - Compressed output for production (13KB) - Easy development with watch mode Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2448
src/frontend/package-lock.json
generated
Normal file
2448
src/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
src/frontend/package.json
Normal file
23
src/frontend/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@creativebulma/bulma-collapsible": "^1.0.4",
|
||||
"@vizuaalog/bulmajs": "^0.12.2",
|
||||
"bulma": "^1.0.4",
|
||||
"bulma-tagsinput": "^2.0.0",
|
||||
"htmx.org": "^2.0.8",
|
||||
"install": "^0.13.0",
|
||||
"jquery": "^4.0.0",
|
||||
"npm": "^11.8.0"
|
||||
},
|
||||
"description": "Pygentic AI Frontend - SWOT Analysis Interface",
|
||||
"devDependencies": {
|
||||
"sass": "^1.83.0"
|
||||
},
|
||||
"name": "pygentic-ai-frontend",
|
||||
"scripts": {
|
||||
"build:css": "sass scss/styles.scss static/css/pygentic_ai.css --style compressed",
|
||||
"dev": "npm run watch:css",
|
||||
"watch:css": "sass scss/styles.scss static/css/pygentic_ai.css --watch"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
96
src/frontend/scss/_animations.scss
Normal file
96
src/frontend/scss/_animations.scss
Normal file
@ -0,0 +1,96 @@
|
||||
// ============================================
|
||||
// ANIMATIONS & KEYFRAMES
|
||||
// ============================================
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 0 rgba($brand-primary, 0.7);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 10px rgba($brand-primary, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-10px); }
|
||||
75% { transform: translateX(10px); }
|
||||
}
|
||||
|
||||
@keyframes iconPulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes containerFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes statusFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes statusPulse {
|
||||
0%, 100% {
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 4px 20px rgba($swot-weakness, 0.3);
|
||||
}
|
||||
}
|
||||
176
src/frontend/scss/_components.scss
Normal file
176
src/frontend/scss/_components.scss
Normal file
@ -0,0 +1,176 @@
|
||||
// ============================================
|
||||
// COMPONENTS
|
||||
// Reusable UI components
|
||||
// ============================================
|
||||
|
||||
// Loading Spinner
|
||||
// ===================================
|
||||
.spinner-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 4px solid rgba($brand-primary, 0.1);
|
||||
border-top: 4px solid $brand-primary;
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 1.5rem;
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: $neutral-900;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
color: $neutral-600;
|
||||
}
|
||||
}
|
||||
|
||||
// Search Form
|
||||
// ===================================
|
||||
.search-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: $radius-full;
|
||||
padding: 0.5rem;
|
||||
box-shadow: $shadow-xl;
|
||||
transition: all $transition-base;
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0 20px 40px rgba($brand-primary, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
padding: 0 1rem;
|
||||
color: $neutral-400;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0.875rem 1rem;
|
||||
font-size: 1rem;
|
||||
background: transparent;
|
||||
color: $neutral-900;
|
||||
|
||||
&::placeholder {
|
||||
color: $neutral-400;
|
||||
}
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background: $brand-primary;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: $radius-full;
|
||||
padding: 0.875rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all $transition-base;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: $brand-primary-dark;
|
||||
transform: translateX(2px);
|
||||
box-shadow: $shadow-lg;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $brand-primary-light;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-help {
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
color: $neutral-600;
|
||||
font-size: 0.875rem;
|
||||
|
||||
i {
|
||||
margin-right: 0.25rem;
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Smooth transitions for all interactive elements
|
||||
button,
|
||||
a,
|
||||
input,
|
||||
.swot-card,
|
||||
.swot-card__icon {
|
||||
transition: all $transition-base;
|
||||
}
|
||||
|
||||
// Focus visible styles for accessibility
|
||||
:focus-visible {
|
||||
outline: 2px solid $brand-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
269
src/frontend/scss/_layout.scss
Normal file
269
src/frontend/scss/_layout.scss
Normal file
@ -0,0 +1,269 @@
|
||||
// ============================================
|
||||
// LAYOUT
|
||||
// Page structure and major sections
|
||||
// ============================================
|
||||
|
||||
// Hero Section
|
||||
// ===================================
|
||||
.gradient-hero {
|
||||
background: $gradient-hero !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
img {
|
||||
filter: brightness(0) invert(1);
|
||||
animation: float 3s ease-in-out infinite;
|
||||
|
||||
&:hover {
|
||||
animation: float 1s ease-in-out infinite;
|
||||
filter: brightness(0) invert(1) drop-shadow(0 0 20px rgba(255, 255, 255, 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero-cta .button {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
// SWOT Cards
|
||||
// ===================================
|
||||
.swot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $spacing-lg;
|
||||
margin-top: $spacing-xl;
|
||||
}
|
||||
|
||||
.swot-card {
|
||||
background: white;
|
||||
border-radius: $radius-lg;
|
||||
padding: $spacing-lg;
|
||||
box-shadow: $shadow-lg;
|
||||
transition: all $transition-base;
|
||||
border-top: 4px solid var(--card-color);
|
||||
animation: slideUp 0.5s ease-out backwards;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: $shadow-2xl;
|
||||
|
||||
.swot-card__icon {
|
||||
animation: iconPulse 0.5s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
// Card variants
|
||||
&--strength {
|
||||
--card-color: #{$swot-strength};
|
||||
}
|
||||
|
||||
&--weakness {
|
||||
--card-color: #{$swot-weakness};
|
||||
}
|
||||
|
||||
&--opportunity {
|
||||
--card-color: #{$swot-opportunity};
|
||||
}
|
||||
|
||||
&--threat {
|
||||
--card-color: #{$swot-threat};
|
||||
}
|
||||
}
|
||||
|
||||
.swot-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid $neutral-200;
|
||||
}
|
||||
|
||||
.swot-card__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $radius-md;
|
||||
background: var(--card-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.swot-card__title {
|
||||
font-weight: 600;
|
||||
font-size: 1.375rem;
|
||||
color: $neutral-900;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.swot-card__count {
|
||||
background: $neutral-100;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: $radius-xl;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: $neutral-700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.swot-card__body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.swot-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.swot-list__item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid $neutral-100;
|
||||
animation: fadeIn 0.3s ease-out backwards;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// Stagger animation for list items
|
||||
@for $i from 1 through 10 {
|
||||
&:nth-child(#{$i}) {
|
||||
animation-delay: #{$i * 0.1}s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swot-list__bullet {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--card-color);
|
||||
margin-top: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.swot-list__text {
|
||||
flex: 1;
|
||||
color: $neutral-700;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
// Stagger card animations
|
||||
@for $i from 1 through 4 {
|
||||
.swot-card:nth-child(#{$i}) {
|
||||
animation-delay: #{$i * 0.1}s;
|
||||
}
|
||||
}
|
||||
|
||||
// Result container entrance
|
||||
#result-container.animate-in {
|
||||
animation: containerFadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
// Status Timeline
|
||||
// ===================================
|
||||
.status-timeline {
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 24px;
|
||||
bottom: 24px;
|
||||
width: 2px;
|
||||
background: $neutral-200;
|
||||
}
|
||||
}
|
||||
|
||||
.status-item {
|
||||
position: relative;
|
||||
padding: 1rem 0 1rem 2rem;
|
||||
animation: statusFadeIn 0.3s ease-out;
|
||||
|
||||
// Status variants
|
||||
&--info {
|
||||
.status-item__indicator,
|
||||
.status-item__header {
|
||||
color: $swot-opportunity;
|
||||
}
|
||||
}
|
||||
|
||||
&--loading {
|
||||
.status-item__indicator,
|
||||
.status-item__header {
|
||||
color: $swot-weakness;
|
||||
}
|
||||
|
||||
.status-item__content {
|
||||
animation: statusPulse 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&--success {
|
||||
.status-item__indicator,
|
||||
.status-item__header {
|
||||
color: $swot-strength;
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
.status-item__indicator,
|
||||
.status-item__header {
|
||||
color: $swot-threat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-item__indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 1.25rem;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 4px white;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.status-item__content {
|
||||
background: white;
|
||||
border-radius: $radius-md;
|
||||
padding: 1rem 1.25rem;
|
||||
box-shadow: $shadow-md;
|
||||
transition: all $transition-base;
|
||||
}
|
||||
|
||||
.status-item:hover .status-item__content {
|
||||
box-shadow: $shadow-lg;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.status-item__header {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.status-item__message {
|
||||
color: $neutral-700;
|
||||
line-height: 1.6;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
74
src/frontend/scss/_responsive.scss
Normal file
74
src/frontend/scss/_responsive.scss
Normal file
@ -0,0 +1,74 @@
|
||||
// ============================================
|
||||
// RESPONSIVE DESIGN
|
||||
// Mobile-first breakpoints
|
||||
// ============================================
|
||||
|
||||
// Tablet and below (768px)
|
||||
@media (max-width: $breakpoint-tablet) {
|
||||
.cell.is-col-span-2 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.swot-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
border-radius: $radius-xl;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.swot-card__header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.swot-card__count {
|
||||
order: -1;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile (480px)
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
.swot-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.swot-card__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.swot-card__title {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.status-timeline {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
169
src/frontend/scss/_states.scss
Normal file
169
src/frontend/scss/_states.scss
Normal file
@ -0,0 +1,169 @@
|
||||
// ============================================
|
||||
// STATES
|
||||
// Empty and error states
|
||||
// ============================================
|
||||
|
||||
// Empty State
|
||||
// ===================================
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
|
||||
.empty-state__content {
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.empty-state__icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 2rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, $neutral-100 0%, $neutral-200 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
color: $neutral-400;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.empty-state__title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: $neutral-800;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.empty-state__description {
|
||||
font-size: 1.125rem;
|
||||
color: $neutral-600;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.empty-state__cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.875rem 2rem;
|
||||
background: $brand-primary;
|
||||
color: white;
|
||||
border-radius: $radius-full;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all $transition-base;
|
||||
box-shadow: $shadow-md;
|
||||
|
||||
&:hover {
|
||||
background: $brand-primary-dark;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $shadow-lg;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// Error State
|
||||
// ===================================
|
||||
.error-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
|
||||
.error-state__content {
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.error-state__icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 2rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #FEE2E2 0%, #FEF2F2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
color: $swot-threat;
|
||||
animation: shake 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.error-state__title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: $neutral-800;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.error-state__description {
|
||||
font-size: 1.125rem;
|
||||
color: $neutral-600;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.error-state__details {
|
||||
background: $neutral-100;
|
||||
border-radius: $radius-md;
|
||||
padding: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: $neutral-700;
|
||||
text-align: left;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.error-state__actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.error-state__button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.875rem 2rem;
|
||||
border-radius: $radius-full;
|
||||
font-weight: 600;
|
||||
transition: all $transition-base;
|
||||
box-shadow: $shadow-md;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
|
||||
&--primary {
|
||||
background: $brand-primary;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: $brand-primary-dark;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $shadow-lg;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: white;
|
||||
color: $neutral-700;
|
||||
border: 2px solid $neutral-300;
|
||||
|
||||
&:hover {
|
||||
background: $neutral-100;
|
||||
border-color: $neutral-400;
|
||||
transform: translateY(-2px);
|
||||
color: $neutral-800;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/frontend/scss/_typography.scss
Normal file
30
src/frontend/scss/_typography.scss
Normal file
@ -0,0 +1,30 @@
|
||||
// ============================================
|
||||
// TYPOGRAPHY
|
||||
// ============================================
|
||||
|
||||
body {
|
||||
font-family: $font-body;
|
||||
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle,
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: $font-heading;
|
||||
letter-spacing: -0.02em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// Hero Title with Gradient
|
||||
.hero-title {
|
||||
font-size: clamp(2.5rem, 5vw, 4rem);
|
||||
font-weight: 700;
|
||||
background: $gradient-hero-alt;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
77
src/frontend/scss/_variables.scss
Normal file
77
src/frontend/scss/_variables.scss
Normal file
@ -0,0 +1,77 @@
|
||||
// ============================================
|
||||
// PYGENTIC AI - SCSS VARIABLES
|
||||
// Design tokens and configuration
|
||||
// ============================================
|
||||
|
||||
// Font Imports
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap');
|
||||
|
||||
// Font Families
|
||||
$font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
$font-heading: 'Space Grotesk', 'Inter', sans-serif;
|
||||
|
||||
// Brand Colors (from purple.svg logo)
|
||||
$brand-primary: #8B5CF6;
|
||||
$brand-primary-light: #A78BFA;
|
||||
$brand-primary-dark: #7C3AED;
|
||||
$brand-primary-darker: #6D28D9;
|
||||
|
||||
// Semantic SWOT Colors
|
||||
$swot-strength: #10B981;
|
||||
$swot-strength-light: #34D399;
|
||||
$swot-weakness: #F59E0B;
|
||||
$swot-weakness-light: #FBBF24;
|
||||
$swot-opportunity: #3B82F6;
|
||||
$swot-opportunity-light: #60A5FA;
|
||||
$swot-threat: #EF4444;
|
||||
$swot-threat-light: #F87171;
|
||||
|
||||
// Neutral Palette
|
||||
$neutral-50: #F9FAFB;
|
||||
$neutral-100: #F3F4F6;
|
||||
$neutral-200: #E5E7EB;
|
||||
$neutral-300: #D1D5DB;
|
||||
$neutral-400: #9CA3AF;
|
||||
$neutral-500: #6B7280;
|
||||
$neutral-600: #4B5563;
|
||||
$neutral-700: #374151;
|
||||
$neutral-800: #1F2937;
|
||||
$neutral-900: #111827;
|
||||
|
||||
// Gradients
|
||||
$gradient-hero: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
$gradient-hero-alt: linear-gradient(135deg, $brand-primary 0%, $brand-primary-darker 100%);
|
||||
$gradient-card: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
|
||||
// Shadows
|
||||
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
$shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
|
||||
// Transitions
|
||||
$transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
$transition-base: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
$transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// Breakpoints
|
||||
$breakpoint-mobile: 480px;
|
||||
$breakpoint-tablet: 768px;
|
||||
$breakpoint-desktop: 1024px;
|
||||
$breakpoint-widescreen: 1216px;
|
||||
|
||||
// Spacing
|
||||
$spacing-xs: 0.25rem;
|
||||
$spacing-sm: 0.5rem;
|
||||
$spacing-md: 1rem;
|
||||
$spacing-lg: 1.5rem;
|
||||
$spacing-xl: 2rem;
|
||||
$spacing-2xl: 3rem;
|
||||
|
||||
// Border Radius
|
||||
$radius-sm: 8px;
|
||||
$radius-md: 12px;
|
||||
$radius-lg: 16px;
|
||||
$radius-xl: 20px;
|
||||
$radius-full: 9999px;
|
||||
64
src/frontend/scss/styles.scss
Normal file
64
src/frontend/scss/styles.scss
Normal file
@ -0,0 +1,64 @@
|
||||
/*!
|
||||
* Pygentic AI - Main Stylesheet
|
||||
* Compiled from SCSS partials
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
// Import order matters!
|
||||
// 1. Variables first (used by all other partials)
|
||||
// 2. Typography (base styles)
|
||||
// 3. Animations (keyframes)
|
||||
// 4. Components (reusable UI elements)
|
||||
// 5. Layout (page structure)
|
||||
// 6. States (empty/error states)
|
||||
// 7. Responsive (media queries last)
|
||||
|
||||
@import 'variables';
|
||||
@import 'typography';
|
||||
@import 'animations';
|
||||
@import 'components';
|
||||
@import 'layout';
|
||||
@import 'states';
|
||||
@import 'responsive';
|
||||
|
||||
// ============================================
|
||||
// CSS CUSTOM PROPERTIES
|
||||
// For runtime theming support
|
||||
// ============================================
|
||||
|
||||
:root {
|
||||
// Brand Colors
|
||||
--brand-primary: #{$brand-primary};
|
||||
--brand-primary-light: #{$brand-primary-light};
|
||||
--brand-primary-dark: #{$brand-primary-dark};
|
||||
|
||||
// SWOT Colors
|
||||
--swot-strength: #{$swot-strength};
|
||||
--swot-weakness: #{$swot-weakness};
|
||||
--swot-opportunity: #{$swot-opportunity};
|
||||
--swot-threat: #{$swot-threat};
|
||||
|
||||
// Neutrals
|
||||
--neutral-50: #{$neutral-50};
|
||||
--neutral-100: #{$neutral-100};
|
||||
--neutral-200: #{$neutral-200};
|
||||
--neutral-300: #{$neutral-300};
|
||||
--neutral-400: #{$neutral-400};
|
||||
--neutral-500: #{$neutral-500};
|
||||
--neutral-600: #{$neutral-600};
|
||||
--neutral-700: #{$neutral-700};
|
||||
--neutral-800: #{$neutral-800};
|
||||
--neutral-900: #{$neutral-900};
|
||||
|
||||
// Shadows
|
||||
--shadow-sm: #{$shadow-sm};
|
||||
--shadow-md: #{$shadow-md};
|
||||
--shadow-lg: #{$shadow-lg};
|
||||
--shadow-xl: #{$shadow-xl};
|
||||
--shadow-2xl: #{$shadow-2xl};
|
||||
|
||||
// Transitions
|
||||
--transition-fast: #{$transition-fast};
|
||||
--transition-base: #{$transition-base};
|
||||
--transition-slow: #{$transition-slow};
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
1
src/frontend/static/css/pygentic_ai.css.map
Normal file
1
src/frontend/static/css/pygentic_ai.css.map
Normal file
@ -0,0 +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"}
|
||||
224
src/frontend/static/js/app.js
Normal file
224
src/frontend/static/js/app.js
Normal file
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Pygentic AI - Frontend Application
|
||||
* Progressive loading and enhanced UX interactions
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Progressive loading messages
|
||||
const LOADING_MESSAGES = [
|
||||
'Fetching URL content...',
|
||||
'Analyzing page structure...',
|
||||
'Extracting key information...',
|
||||
'Identifying patterns...',
|
||||
'Generating SWOT analysis...',
|
||||
'Finalizing insights...',
|
||||
'Almost there...'
|
||||
];
|
||||
|
||||
let loadingMessageIndex = 0;
|
||||
let loadingInterval = null;
|
||||
let pollCount = 0;
|
||||
let pollInterval = 1000; // Start at 1s
|
||||
const MAX_POLL_INTERVAL = 5000; // Max 5s
|
||||
|
||||
/**
|
||||
* Update loading message progressively
|
||||
*/
|
||||
function updateLoadingMessage() {
|
||||
const statusElement = document.getElementById('loading-status');
|
||||
if (!statusElement) return;
|
||||
|
||||
if (loadingMessageIndex < LOADING_MESSAGES.length - 1) {
|
||||
loadingMessageIndex++;
|
||||
}
|
||||
|
||||
statusElement.textContent = LOADING_MESSAGES[loadingMessageIndex];
|
||||
statusElement.style.animation = 'fadeIn 0.3s ease-in';
|
||||
}
|
||||
|
||||
/**
|
||||
* Start progressive loading messages
|
||||
*/
|
||||
function startLoadingMessages() {
|
||||
loadingMessageIndex = 0;
|
||||
pollCount = 0;
|
||||
pollInterval = 1000;
|
||||
|
||||
const statusElement = document.getElementById('loading-status');
|
||||
if (statusElement) {
|
||||
statusElement.textContent = LOADING_MESSAGES[0];
|
||||
}
|
||||
|
||||
// Update message every 3 seconds
|
||||
if (loadingInterval) {
|
||||
clearInterval(loadingInterval);
|
||||
}
|
||||
|
||||
loadingInterval = setInterval(updateLoadingMessage, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop loading messages
|
||||
*/
|
||||
function stopLoadingMessages() {
|
||||
if (loadingInterval) {
|
||||
clearInterval(loadingInterval);
|
||||
loadingInterval = null;
|
||||
}
|
||||
loadingMessageIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth scroll to results
|
||||
*/
|
||||
function scrollToResults() {
|
||||
const resultsSection = document.getElementById('result-container');
|
||||
if (resultsSection) {
|
||||
resultsSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exponential backoff for polling
|
||||
*/
|
||||
function calculateNextPollInterval() {
|
||||
pollCount++;
|
||||
|
||||
if (pollCount > 3) {
|
||||
pollInterval = Math.min(pollInterval * 1.5, MAX_POLL_INTERVAL);
|
||||
}
|
||||
|
||||
return pollInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize form submission handler
|
||||
*/
|
||||
function initializeForm() {
|
||||
const form = document.getElementById('swotSearch');
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Start loading messages when form is submitted
|
||||
startLoadingMessages();
|
||||
|
||||
// Show spinner
|
||||
const spinner = document.getElementById('spinner');
|
||||
if (spinner) {
|
||||
spinner.classList.remove('is-hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor for analysis completion
|
||||
*/
|
||||
function monitorAnalysisCompletion() {
|
||||
const resultBox = document.getElementById('result');
|
||||
if (!resultBox) return;
|
||||
|
||||
// Use MutationObserver to watch for content changes
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList' && resultBox.innerHTML.trim().length > 0) {
|
||||
// Analysis complete
|
||||
stopLoadingMessages();
|
||||
|
||||
// Hide spinner
|
||||
const spinner = document.getElementById('spinner');
|
||||
if (spinner) {
|
||||
spinner.classList.add('is-hidden');
|
||||
}
|
||||
|
||||
// Scroll to results after a brief delay
|
||||
setTimeout(scrollToResults, 500);
|
||||
|
||||
// Add success class for animation
|
||||
const resultContainer = document.getElementById('result-container');
|
||||
if (resultContainer) {
|
||||
resultContainer.classList.add('animate-in');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(resultBox, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add button press effects
|
||||
*/
|
||||
function initializeButtonEffects() {
|
||||
const buttons = document.querySelectorAll('.search-button, .error-state__button');
|
||||
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('mousedown', function() {
|
||||
this.style.transform = 'scale(0.95)';
|
||||
});
|
||||
|
||||
button.addEventListener('mouseup', function() {
|
||||
this.style.transform = '';
|
||||
});
|
||||
|
||||
button.addEventListener('mouseleave', function() {
|
||||
this.style.transform = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize smooth anchor scrolling
|
||||
*/
|
||||
function initializeSmoothScrolling() {
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
const href = this.getAttribute('href');
|
||||
if (href === '#' || !href) return;
|
||||
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(href);
|
||||
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all features on DOM ready
|
||||
*/
|
||||
function initialize() {
|
||||
initializeForm();
|
||||
monitorAnalysisCompletion();
|
||||
initializeButtonEffects();
|
||||
initializeSmoothScrolling();
|
||||
|
||||
console.log('✨ Pygentic AI initialized');
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initialize);
|
||||
} else {
|
||||
initialize();
|
||||
}
|
||||
|
||||
// Export functions for external use if needed
|
||||
window.PygenticAI = {
|
||||
startLoadingMessages,
|
||||
stopLoadingMessages,
|
||||
scrollToResults
|
||||
};
|
||||
})();
|
||||
@ -1,3 +1,4 @@
|
||||
<Js url="{{ url_for('static', path="/js/htmx.js") }}">type="modules"</Js>
|
||||
<Js url="{{ url_for('static', path="/js/bulma.js") }}"></Js>
|
||||
<Js url="{{ url_for('static', path="/js/bulma-collapsible.min.js") }}"></Js>
|
||||
<Js url="{{ url_for('static', path="/js/bulma-collapsible.min.js") }}"></Js>
|
||||
<Js url="{{ url_for('static', path="/js/app.js") }}"></Js>
|
||||
16
src/frontend/templates/components/snippets/EmptyState.jinja
Normal file
16
src/frontend/templates/components/snippets/EmptyState.jinja
Normal file
@ -0,0 +1,16 @@
|
||||
{# def
|
||||
title: str = "No Analysis Yet",
|
||||
description: str = "Enter a URL above to get started with your first SWOT analysis",
|
||||
icon: str = "fa-chart-simple"
|
||||
#}
|
||||
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__content">
|
||||
<div class="empty-state__icon">
|
||||
<i class="fas {{ icon }}"></i>
|
||||
</div>
|
||||
<h3 class="empty-state__title">{{ title }}</h3>
|
||||
<p class="empty-state__description">{{ description }}</p>
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
39
src/frontend/templates/components/snippets/ErrorState.jinja
Normal file
39
src/frontend/templates/components/snippets/ErrorState.jinja
Normal file
@ -0,0 +1,39 @@
|
||||
{# def
|
||||
title: str = "Analysis Failed",
|
||||
description: str = "We couldn't analyze this URL. Please check the URL and try again.",
|
||||
error_details: str = None,
|
||||
show_retry: bool = True
|
||||
#}
|
||||
|
||||
<div class="error-state">
|
||||
<div class="error-state__content">
|
||||
<div class="error-state__icon">
|
||||
<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">
|
||||
{{ error_details }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
<span>Try Again</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="/"
|
||||
class="error-state__button error-state__button--secondary">
|
||||
<i class="fas fa-home"></i>
|
||||
<span>Go Home</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
@ -29,8 +29,10 @@
|
||||
action={{ url_for('analyze_url') }}
|
||||
target="status"
|
||||
method="post">
|
||||
<div class="search-input-group">
|
||||
<span class="search-icon">
|
||||
<div class="search-input-group"
|
||||
role="search">
|
||||
<span class="search-icon"
|
||||
aria-hidden="true">
|
||||
<i class="fas fa-link"></i>
|
||||
</span>
|
||||
<input type="url"
|
||||
@ -38,11 +40,14 @@
|
||||
id="url"
|
||||
name="url"
|
||||
placeholder="Enter a URL to analyze (e.g., https://example.com)"
|
||||
aria-label="Enter URL to analyze"
|
||||
aria-describedby="search-help"
|
||||
required
|
||||
pattern="https?://.+"
|
||||
autocomplete="url" />
|
||||
<button type="submit"
|
||||
class="search-button"
|
||||
aria-label="Analyze URL"
|
||||
hx-indicator='#spinner'
|
||||
hx-on:click="
|
||||
const [status, result, spinner] = ['#status', '#result', '#spinner'].map(id => document.querySelector(id));
|
||||
@ -51,7 +56,8 @@
|
||||
result.style.display = 'none';
|
||||
">
|
||||
<span class="button-text">Analyze</span>
|
||||
<span class="button-icon">
|
||||
<span class="button-icon"
|
||||
aria-hidden="true">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@ -1,39 +1,46 @@
|
||||
{% if messages %}
|
||||
<section class="section"
|
||||
id="status-container">
|
||||
<div class="container">
|
||||
{% 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') %}
|
||||
<div class="box">
|
||||
{% if is_error %}
|
||||
{% set bg_color = 'danger' %}
|
||||
{% set header_content = 'Error' %}
|
||||
{% set content = message %}
|
||||
{% elif is_loading and "analysis complete" not in message.lower() %}
|
||||
{% set bg_color = 'warning' %}
|
||||
{% set content = message %}
|
||||
{% set header_content = 'In Progress' %}
|
||||
<section class="section" id="status-container">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<div class="status-timeline">
|
||||
{% 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() %}
|
||||
|
||||
{% elif is_tool_message and "analysis complete" not in message.lower() %}
|
||||
{% set bg_color = 'info '%}
|
||||
{% set header_content, content = message.split(' ', 2)[2].split('...', 1) %}
|
||||
|
||||
{% elif "analyzing..." in message.lower() %}
|
||||
{% set bg_color = 'link' %}
|
||||
{% set content = 'message' %}
|
||||
{% set header_content = "Starting"%}
|
||||
{% else %}
|
||||
{% set bg_color = 'success' %}
|
||||
{% set content = message %}
|
||||
{% set header_content = 'Done!' %}
|
||||
{% endif %}
|
||||
<StatusResult div_class={{ bg_color }}
|
||||
header_content={{ header_content }}>{{ message }}
|
||||
</StatusResult>
|
||||
<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">
|
||||
{% if is_error %}
|
||||
<i class="fas fa-circle-xmark"></i>
|
||||
{% elif is_complete %}
|
||||
<i class="fas fa-circle-check"></i>
|
||||
{% elif is_loading %}
|
||||
<i class="fas fa-circle-notch fa-spin"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-circle"></i>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
</div>
|
||||
<div class="status-item__message">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user