diff --git a/frontend/src/index.css b/frontend/src/index.css index cff81bb..53764e5 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -42,3 +42,24 @@ textarea:focus { box-shadow: 0 0 0 3px var(--focus-ring); outline: none; } + +@keyframes stats-spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +@keyframes stats-pulse { + 0%, + 100% { + opacity: 0.5; + } + + 50% { + opacity: 1; + } +} diff --git a/frontend/src/pages/Stats.tsx b/frontend/src/pages/Stats.tsx index 49a62a6..586d6c2 100644 --- a/frontend/src/pages/Stats.tsx +++ b/frontend/src/pages/Stats.tsx @@ -136,7 +136,27 @@ const StatPage = () => { getStats(); }, [datasetId]) - if (loading) return

Loading insights…

; + if (loading) { + return ( +
+
+
+
+
+

Loading analytics

+

Fetching summary, timeline, user, and content insights.

+
+
+ +
+
+
+
+
+
+
+ ); + } if (error) return

{error}

; return ( diff --git a/frontend/src/styles/stats/feedback.ts b/frontend/src/styles/stats/feedback.ts index 33cbe2d..31f9fa6 100644 --- a/frontend/src/styles/stats/feedback.ts +++ b/frontend/src/styles/stats/feedback.ts @@ -2,6 +2,78 @@ import { palette } from "./palette"; import type { StyleMap } from "./types"; export const feedbackStyles: StyleMap = { + loadingPage: { + width: "100%", + minHeight: "100vh", + padding: 20, + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + + loadingCard: { + width: "min(560px, 92vw)", + background: palette.surface, + border: `1px solid ${palette.borderDefault}`, + borderRadius: 8, + boxShadow: `0 1px 0 ${palette.shadowSubtle}`, + padding: 20, + }, + + loadingHeader: { + display: "flex", + alignItems: "center", + gap: 12, + }, + + loadingSpinner: { + width: 18, + height: 18, + borderRadius: "50%", + border: `2px solid ${palette.borderDefault}`, + borderTopColor: palette.brandGreen, + animation: "stats-spin 0.9s linear infinite", + flexShrink: 0, + }, + + loadingTitle: { + margin: 0, + fontSize: 16, + fontWeight: 600, + color: palette.textPrimary, + }, + + loadingSubtitle: { + margin: "6px 0 0", + fontSize: 13, + color: palette.textSecondary, + }, + + loadingSkeleton: { + marginTop: 16, + display: "grid", + gap: 8, + }, + + loadingSkeletonLine: { + height: 9, + borderRadius: 999, + background: palette.canvas, + animation: "stats-pulse 1.25s ease-in-out infinite", + }, + + loadingSkeletonLineLong: { + width: "100%", + }, + + loadingSkeletonLineMed: { + width: "78%", + }, + + loadingSkeletonLineShort: { + width: "62%", + }, + alertCardError: { borderColor: palette.alertErrorBorder, background: palette.alertErrorBg, diff --git a/frontend/src/types/ApiTypes.ts b/frontend/src/types/ApiTypes.ts index 6bf9c2d..5feaddf 100644 --- a/frontend/src/types/ApiTypes.ts +++ b/frontend/src/types/ApiTypes.ts @@ -55,16 +55,24 @@ type TimeAnalysisResponse = { } // Content Analysis +type Emotion = { + emotion_anger: number; + emotion_disgust: number; + emotion_fear: number; + emotion_joy: number; + emotion_sadness: number; +}; + type NGram = { count: number; ngram: string; } -type AverageEmotionByTopic = { - topic: string; - n: number; - [emotion: string]: string | number; -} +type AverageEmotionByTopic = Emotion & { + n: number; + topic: string; +}; + type ContentAnalysisResponse = { word_frequencies: FrequencyWord[];