Syed Arfan Claude commited on
Commit
2857363
·
1 Parent(s): 66da1e3

Add React frontend for Sentiment Analysis API

Browse files

Features:
- Real-time sentiment analysis with visual feedback
- Confidence score progress bars
- Analysis history with filtering
- Cache performance dashboard
- Responsive design (mobile-first)
- GitHub Pages deployment workflow

Tech stack:
- React 19 + TypeScript
- Vite build tool
- Tailwind CSS 4
- Axios for API calls
- Lucide React icons

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

.github/workflows/deploy-frontend.yml ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GitHub Actions Workflow for Frontend Deployment to GitHub Pages
2
+ # Automatically deploys the React frontend when changes are pushed to main
3
+
4
+ name: Deploy Frontend to GitHub Pages
5
+
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ paths:
10
+ - 'frontend/**'
11
+ - '.github/workflows/deploy-frontend.yml'
12
+ workflow_dispatch: # Allow manual deployment
13
+
14
+ # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
15
+ permissions:
16
+ contents: read
17
+ pages: write
18
+ id-token: write
19
+
20
+ # Allow only one concurrent deployment
21
+ concurrency:
22
+ group: 'pages'
23
+ cancel-in-progress: true
24
+
25
+ jobs:
26
+ build:
27
+ name: Build Frontend
28
+ runs-on: ubuntu-latest
29
+
30
+ steps:
31
+ - name: Checkout code
32
+ uses: actions/checkout@v4
33
+
34
+ - name: Setup Node.js
35
+ uses: actions/setup-node@v4
36
+ with:
37
+ node-version: '20'
38
+ cache: 'npm'
39
+ cache-dependency-path: frontend/package-lock.json
40
+
41
+ - name: Install dependencies
42
+ working-directory: frontend
43
+ run: npm ci
44
+
45
+ - name: Build project
46
+ working-directory: frontend
47
+ run: npm run build
48
+ env:
49
+ VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL || 'http://localhost:8000' }}
50
+
51
+ - name: Upload artifact
52
+ uses: actions/upload-pages-artifact@v3
53
+ with:
54
+ path: frontend/dist
55
+
56
+ deploy:
57
+ name: Deploy to GitHub Pages
58
+ needs: build
59
+ runs-on: ubuntu-latest
60
+ environment:
61
+ name: github-pages
62
+ url: ${{ steps.deployment.outputs.page_url }}
63
+
64
+ steps:
65
+ - name: Deploy to GitHub Pages
66
+ id: deployment
67
+ uses: actions/deploy-pages@v4
docs/Sentiment-Analysis-Frontend-PRD.md ADDED
@@ -0,0 +1,883 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Product Requirements Document (PRD)
2
+ # Sentiment Analysis Frontend Application
3
+
4
+ **Version:** 1.0
5
+ **Date:** December 11, 2025
6
+ **Author:** Syed Arfan Hussain
7
+ **Status:** Draft → Approved
8
+
9
+ ---
10
+
11
+ ## 1. Executive Summary
12
+
13
+ ### 1.1 Product Overview
14
+ A modern, responsive web frontend that provides an intuitive interface for the Sentiment Analysis API. The application will feature real-time sentiment analysis with visual feedback, analysis history, and cache statistics monitoring.
15
+
16
+ ### 1.2 Goals
17
+ - **Primary Goal**: Create a production-ready, user-friendly interface that showcases the Sentiment API's capabilities
18
+ - **Secondary Goals**:
19
+ - Demonstrate frontend development skills (React, TypeScript, modern CSS)
20
+ - Provide real-time visual feedback for sentiment analysis
21
+ - Display cache performance metrics in an engaging way
22
+ - Serve as a portfolio piece with professional UX/UI design
23
+
24
+ ### 1.3 Success Metrics
25
+ - Page load time < 2 seconds
26
+ - Sentiment analysis response time < 100ms (cached) / < 2s (uncached)
27
+ - Mobile-responsive on all devices (320px+)
28
+ - Accessibility score (Lighthouse) > 90
29
+ - Zero console errors in production
30
+
31
+ ---
32
+
33
+ ## 2. Target Audience
34
+
35
+ ### 2.1 Primary Users
36
+ - **Technical Recruiters**: Evaluating full-stack capabilities
37
+ - **Potential Employers**: Assessing frontend skills and design sensibility
38
+ - **Fellow Developers**: Reviewing code quality and architecture
39
+ - **End Users**: Testing sentiment analysis for personal/business text
40
+
41
+ ### 2.2 User Personas
42
+
43
+ **Persona 1: Sarah - Technical Recruiter**
44
+ - Needs: Quick understanding of capabilities, professional appearance
45
+ - Goals: Assess technical skills within 2 minutes
46
+ - Pain Points: Poorly designed demos that don't work on mobile
47
+
48
+ **Persona 2: Mike - Product Manager**
49
+ - Needs: Test sentiment analysis with real business text
50
+ - Goals: Evaluate accuracy and speed for potential use case
51
+ - Pain Points: Unclear confidence scores, no historical data
52
+
53
+ **Persona 3: Alex - Fellow Developer**
54
+ - Needs: Inspect code quality, architecture decisions
55
+ - Goals: Learn from implementation patterns
56
+ - Pain Points: Overly complex or poorly documented code
57
+
58
+ ---
59
+
60
+ ## 3. Product Features
61
+
62
+ ### 3.1 Feature Priority Matrix
63
+
64
+ | Feature | Priority | Complexity | Value | Version |
65
+ |---------|----------|------------|-------|---------|
66
+ | Sentiment Analysis Input | P0 | Low | High | MVP |
67
+ | Real-time Results Display | P0 | Medium | High | MVP |
68
+ | Visual Sentiment Indicator | P0 | Medium | High | MVP |
69
+ | Analysis History View | P0 | Medium | High | MVP |
70
+ | Cache Statistics Dashboard | P1 | Medium | Medium | MVP |
71
+ | Dark/Light Mode Toggle | P1 | Low | Medium | V1.1 |
72
+ | Export Analysis Data | P2 | Medium | Low | V1.2 |
73
+ | Batch Analysis Upload | P2 | High | Medium | V2.0 |
74
+
75
+ ---
76
+
77
+ ## 4. Feature Specifications
78
+
79
+ ### 4.1 Sentiment Analysis Input (P0)
80
+
81
+ **Description**: Primary interface for users to input text for sentiment analysis.
82
+
83
+ **Requirements**:
84
+ - **Text Input Field**:
85
+ - Multi-line textarea (min height: 120px, auto-expand to 400px)
86
+ - Character counter (0/5000 characters)
87
+ - Real-time validation with visual feedback
88
+ - Placeholder text: "Type or paste your text here to analyze sentiment..."
89
+
90
+ - **Validation Rules**:
91
+ - Minimum: 3 characters
92
+ - Maximum: 5000 characters
93
+ - Error messages: Clear, inline, non-blocking
94
+
95
+ - **Sample Text Buttons**:
96
+ - "Try Positive Example" → Pre-fills positive text
97
+ - "Try Negative Example" → Pre-fills negative text
98
+ - "Clear" → Resets input field
99
+
100
+ - **Analyze Button**:
101
+ - Primary action button (prominent, colorful)
102
+ - Disabled state when input is invalid
103
+ - Loading state with spinner during API call
104
+ - Keyboard shortcut: Ctrl/Cmd + Enter
105
+
106
+ **User Flow**:
107
+ ```
108
+ User lands → Sees input field → Types/pastes text →
109
+ Character count updates → Button becomes active →
110
+ Clicks "Analyze" → Loading spinner → Results appear
111
+ ```
112
+
113
+ **Edge Cases**:
114
+ - Empty input: Button disabled, gentle prompt shown
115
+ - Network error: Error message with retry button
116
+ - API timeout: "Taking longer than usual" message after 3s
117
+ - Very long text: Auto-scroll to results after submission
118
+
119
+ ---
120
+
121
+ ### 4.2 Real-time Results Display (P0)
122
+
123
+ **Description**: Displays sentiment analysis results with confidence scores and metadata.
124
+
125
+ **Requirements**:
126
+ - **Result Card Layout**:
127
+ - Large sentiment badge (POSITIVE/NEGATIVE)
128
+ - Confidence score (percentage with progress bar)
129
+ - Processing time (with cache hit/miss indicator)
130
+ - Timestamp of analysis
131
+ - Original text snippet (first 100 chars)
132
+
133
+ - **Visual Design**:
134
+ - POSITIVE: Green color scheme (#81c784)
135
+ - NEGATIVE: Red color scheme (#e57373)
136
+ - Card shadow and subtle animation on appearance
137
+ - Responsive: stacks on mobile, side-by-side on desktop
138
+
139
+ - **Confidence Visualization**:
140
+ - Progress bar (0-100%)
141
+ - Color gradient based on confidence level
142
+ - Numerical percentage displayed
143
+ - "High confidence" / "Medium confidence" label
144
+
145
+ - **Performance Metrics**:
146
+ - Processing time in milliseconds
147
+ - Cache status: "⚡ Cached" (green) or "🔄 Processed" (blue)
148
+ - Cache hit: Show time savings vs. uncached
149
+
150
+ **API Response Handling**:
151
+ ```typescript
152
+ interface SentimentResult {
153
+ text: string;
154
+ sentiment: 'POSITIVE' | 'NEGATIVE';
155
+ confidence: number; // 0.0 - 1.0
156
+ processing_time_ms: number;
157
+ cached: boolean;
158
+ created_at: string;
159
+ }
160
+ ```
161
+
162
+ **Animation**:
163
+ - Fade-in animation (300ms)
164
+ - Confidence bar fills from 0 to actual value
165
+ - Smooth scroll to results after analysis
166
+
167
+ ---
168
+
169
+ ### 4.3 Visual Sentiment Indicator (P0)
170
+
171
+ **Description**: Large, eye-catching visual representation of sentiment result.
172
+
173
+ **Requirements**:
174
+ - **Design Options** (choose one):
175
+ - **Option A**: Emoji + Badge
176
+ - 😊 for POSITIVE (green background)
177
+ - 😞 for NEGATIVE (red background)
178
+ - Large size (80px) with subtle bounce animation
179
+
180
+ - **Option B**: Gradient Card
181
+ - Full-width card with gradient background
182
+ - Green gradient for POSITIVE
183
+ - Red gradient for NEGATIVE
184
+ - White text overlaid
185
+
186
+ - **Option C**: Meter/Gauge
187
+ - Semi-circular gauge showing confidence
188
+ - Needle points to POSITIVE or NEGATIVE
189
+ - More sophisticated, dashboard-style
190
+
191
+ **Recommendation**: Start with Option A (emoji + badge) for MVP, add Option C in V1.1
192
+
193
+ **Accessibility**:
194
+ - Color is not the only indicator (text + icon)
195
+ - ARIA labels for screen readers
196
+ - High contrast ratios (WCAG AA compliant)
197
+
198
+ ---
199
+
200
+ ### 4.4 Analysis History View (P0)
201
+
202
+ **Description**: Display recent sentiment analyses with search and filter capabilities.
203
+
204
+ **Requirements**:
205
+ - **History List**:
206
+ - Most recent 10 analyses displayed by default
207
+ - Each item shows:
208
+ - Text snippet (first 50 characters + "...")
209
+ - Sentiment badge
210
+ - Confidence percentage
211
+ - Timestamp (relative: "2 mins ago")
212
+ - Click to expand full details
213
+
214
+ - **Features**:
215
+ - "Load More" button (pagination, 10 per page)
216
+ - Filter by sentiment (All / Positive / Negative)
217
+ - Sort by: Newest / Oldest / Highest Confidence
218
+ - Search by text content (client-side)
219
+ - "Clear History" button (with confirmation)
220
+
221
+ - **Empty State**:
222
+ - Message: "No analysis history yet"
223
+ - Call-to-action: "Analyze your first text above!"
224
+ - Illustration or icon
225
+
226
+ - **Data Management**:
227
+ - Fetch from `/history?limit=10` endpoint
228
+ - Auto-refresh after new analysis
229
+ - Cache in browser localStorage (optional, for performance)
230
+
231
+ **User Flow**:
232
+ ```
233
+ User scrolls down → Sees history section →
234
+ Can filter/search → Clicks item → Expands details →
235
+ Can re-analyze same text
236
+ ```
237
+
238
+ ---
239
+
240
+ ### 4.5 Cache Statistics Dashboard (P1)
241
+
242
+ **Description**: Real-time visualization of Redis cache performance metrics.
243
+
244
+ **Requirements**:
245
+ - **Metrics Display**:
246
+ - **Total Analyses**: Count with icon
247
+ - **Cache Hit Rate**: Percentage with progress ring
248
+ - **Avg Response Time**: Milliseconds comparison (cached vs uncached)
249
+ - **Memory Used**: MB with gauge
250
+
251
+ - **Visual Components**:
252
+ - Card-based layout (4 cards in grid)
253
+ - Icons for each metric (using icon library)
254
+ - Color coding:
255
+ - Green: Good performance (hit rate > 80%)
256
+ - Yellow: Medium (hit rate 50-80%)
257
+ - Red: Poor (hit rate < 50%)
258
+
259
+ - **Data Source**:
260
+ - Fetch from `/cache/stats` endpoint
261
+ - Auto-refresh every 30 seconds
262
+ - Manual refresh button
263
+
264
+ - **Charts** (V1.1 enhancement):
265
+ - Line chart: Hit rate over time
266
+ - Bar chart: Response time comparison
267
+ - Pie chart: Cache hits vs misses
268
+
269
+ **Layout**:
270
+ ```
271
+ ┌─────────────┬─────────────┐
272
+ │ Total │ Cache Hit │
273
+ │ Analyses │ Rate │
274
+ │ │ │
275
+ │ 1,234 │ 87% │
276
+ └─────────────┴─────────────┘
277
+ ┌─────────────┬─────────────┐
278
+ │ Avg Response│ Memory │
279
+ │ Time │ Used │
280
+ │ │ │
281
+ │ 2ms (cached)│ 12.5 MB │
282
+ └─────────────┴─────────────┘
283
+ ```
284
+
285
+ ---
286
+
287
+ ## 5. Technical Specifications
288
+
289
+ ### 5.1 Technology Stack
290
+
291
+ **Frontend Framework**: React 18 with TypeScript
292
+ - Modern, component-based architecture
293
+ - Strong typing for better code quality
294
+ - Large ecosystem and community support
295
+
296
+ **Styling**:
297
+ - **Primary**: Tailwind CSS (utility-first, rapid development)
298
+ - **Alternative**: Styled Components (if component-scoped styles preferred)
299
+ - **Icons**: Lucide React or Heroicons
300
+
301
+ **State Management**:
302
+ - React Context API (for simple global state)
303
+ - React Query (for API data fetching and caching)
304
+
305
+ **Build Tool**: Vite
306
+ - Fast dev server and HMR
307
+ - Optimized production builds
308
+ - Better DX than Create React App
309
+
310
+ **API Client**: Axios
311
+ - Interceptors for error handling
312
+ - Request/response transformers
313
+ - Better error handling than fetch
314
+
315
+ ### 5.2 Project Structure
316
+
317
+ ```
318
+ sentiment-frontend/
319
+ ├── public/
320
+ │ ���── favicon.ico
321
+ ├── src/
322
+ │ ├── components/
323
+ │ │ ├── SentimentAnalyzer.tsx # Main analysis input/results
324
+ │ │ ├── ResultCard.tsx # Individual result display
325
+ │ │ ├── HistoryList.tsx # Analysis history
326
+ │ │ ├── CacheStats.tsx # Cache dashboard
327
+ │ │ ├── Header.tsx # App header/nav
328
+ │ │ └── Footer.tsx # App footer
329
+ │ ├── hooks/
330
+ │ │ ├── useSentimentAnalysis.ts # API call hook
331
+ │ │ ├── useHistory.ts # History fetching
332
+ │ │ └── useCacheStats.ts # Cache stats
333
+ │ ├── services/
334
+ │ │ └── api.ts # Axios instance & endpoints
335
+ │ ├── types/
336
+ │ │ └── index.ts # TypeScript interfaces
337
+ │ ├── utils/
338
+ │ │ ├── formatters.ts # Date/number formatting
339
+ │ │ └── validators.ts # Input validation
340
+ │ ├── App.tsx # Root component
341
+ │ ├── main.tsx # Entry point
342
+ │ └── index.css # Global styles
343
+ ├── .env.example
344
+ ├── .gitignore
345
+ ├── package.json
346
+ ├── tsconfig.json
347
+ ├── vite.config.ts
348
+ └── README.md
349
+ ```
350
+
351
+ ### 5.3 API Integration
352
+
353
+ **Base URL**:
354
+ - Development: `http://localhost:8000`
355
+ - Production: `http://localhost` (nginx proxy)
356
+
357
+ **Endpoints**:
358
+
359
+ ```typescript
360
+ // POST /analyze
361
+ interface AnalyzeRequest {
362
+ text: string;
363
+ }
364
+
365
+ interface AnalyzeResponse {
366
+ text: string;
367
+ sentiment: 'POSITIVE' | 'NEGATIVE';
368
+ confidence: number;
369
+ processing_time_ms: number;
370
+ cached: boolean;
371
+ }
372
+
373
+ // GET /history?limit=10
374
+ interface HistoryResponse {
375
+ total: number;
376
+ analyses: Analysis[];
377
+ }
378
+
379
+ // GET /cache/stats
380
+ interface CacheStatsResponse {
381
+ status: string;
382
+ total_keys: number;
383
+ memory_used_mb: number;
384
+ hits: number;
385
+ misses: number;
386
+ hit_rate: number;
387
+ }
388
+ ```
389
+
390
+ **Error Handling**:
391
+ - Network errors: Retry with exponential backoff
392
+ - 4xx errors: Display user-friendly message
393
+ - 5xx errors: "Service temporarily unavailable"
394
+ - Timeout: After 10 seconds
395
+
396
+ ---
397
+
398
+ ## 6. Design Specifications
399
+
400
+ ### 6.1 Color Palette
401
+
402
+ **Primary Colors**:
403
+ - Blue: `#4fc3f7` (Client, primary actions)
404
+ - Green: `#81c784` (Positive sentiment, success)
405
+ - Red: `#e57373` (Negative sentiment, errors)
406
+ - Orange: `#ffb74d` (Warnings, info)
407
+ - Purple: `#ba68c8` (Accents, secondary)
408
+
409
+ **Neutral Colors**:
410
+ - Background: `#f5f5f5` (light mode), `#1a1a1a` (dark mode)
411
+ - Card Background: `#ffffff` (light), `#2d2d2d` (dark)
412
+ - Text Primary: `#212121` (light), `#e0e0e0` (dark)
413
+ - Text Secondary: `#757575` (light), `#9e9e9e` (dark)
414
+ - Border: `#e0e0e0` (light), `#424242` (dark)
415
+
416
+ ### 6.2 Typography
417
+
418
+ **Font Family**:
419
+ - Primary: `Inter, system-ui, sans-serif`
420
+ - Monospace: `'Fira Code', 'Courier New', monospace` (for code/metrics)
421
+
422
+ **Font Sizes** (Tailwind scale):
423
+ - Heading 1: `text-4xl` (36px) - Page title
424
+ - Heading 2: `text-2xl` (24px) - Section headers
425
+ - Heading 3: `text-xl` (20px) - Card titles
426
+ - Body: `text-base` (16px) - Default text
427
+ - Small: `text-sm` (14px) - Metadata, labels
428
+ - Tiny: `text-xs` (12px) - Timestamps, footnotes
429
+
430
+ **Font Weights**:
431
+ - Regular: 400
432
+ - Medium: 500
433
+ - Semi-bold: 600
434
+ - Bold: 700
435
+
436
+ ### 6.3 Spacing & Layout
437
+
438
+ **Container**:
439
+ - Max width: `1280px` (xl breakpoint)
440
+ - Padding: `px-4` (mobile), `px-6` (tablet), `px-8` (desktop)
441
+
442
+ **Component Spacing**:
443
+ - Section gaps: `gap-8` (32px)
444
+ - Card padding: `p-6` (24px)
445
+ - Button padding: `px-6 py-3` (24px x 12px)
446
+ - Input padding: `px-4 py-3` (16px x 12px)
447
+
448
+ **Border Radius**:
449
+ - Small: `rounded-md` (6px) - Buttons, inputs
450
+ - Medium: `rounded-lg` (8px) - Cards
451
+ - Large: `rounded-xl` (12px) - Modal dialogs
452
+
453
+ ### 6.4 Responsive Breakpoints
454
+
455
+ | Breakpoint | Min Width | Description |
456
+ |------------|-----------|-------------|
457
+ | sm | 640px | Small tablets |
458
+ | md | 768px | Tablets |
459
+ | lg | 1024px | Small desktops |
460
+ | xl | 1280px | Large desktops |
461
+
462
+ **Mobile-First Approach**:
463
+ - Design for mobile (320px) first
464
+ - Progressively enhance for larger screens
465
+ - Test on: iPhone SE, iPad, Desktop 1920x1080
466
+
467
+ ---
468
+
469
+ ## 7. User Experience Flow
470
+
471
+ ### 7.1 Primary User Journey
472
+
473
+ ```mermaid
474
+ graph TD
475
+ A[User Lands on Page] --> B{Has Text to Analyze?}
476
+ B -->|No| C[Clicks Sample Text]
477
+ B -->|Yes| D[Pastes/Types Text]
478
+ C --> D
479
+ D --> E[Sees Character Count]
480
+ E --> F{Valid Input?}
481
+ F -->|No| G[See Validation Error]
482
+ G --> D
483
+ F -->|Yes| H[Click Analyze Button]
484
+ H --> I[Loading State]
485
+ I --> J[Results Appear]
486
+ J --> K{Satisfied?}
487
+ K -->|No| L[Try Different Text]
488
+ K -->|Yes| M[View History/Stats]
489
+ L --> D
490
+ M --> N[Share/Export]
491
+ ```
492
+
493
+ ### 7.2 Page Layout (Wireframe)
494
+
495
+ ```
496
+ ┌───────────────────────────────────────┐
497
+ │ 🎯 Sentiment Analysis │ <- Header
498
+ │ [About] [API Docs] [GitHub] │
499
+ └───────────────────────────────────────┘
500
+
501
+ ┌───────────────────────────────────────┐
502
+ │ Analyze Text Sentiment in Real-Time │ <- Hero Section
503
+ │ Powered by DistilBERT & Redis Cache │
504
+ └───────────────────────────────────────┘
505
+
506
+ ┌───────────────────────────────────────┐
507
+ │ Type or paste your text here... │ <- Input Section
508
+ │ ┌─────────────────────────────────┐ │
509
+ │ │ │ │
510
+ │ │ [Textarea - Multi-line] │ │
511
+ │ │ │ │
512
+ │ └─────────────────────────────────┘ │
513
+ │ Characters: 0 / 5000 │
514
+ │ [Try Positive] [Try Negative] [Clear]│
515
+ │ [🔍 Analyze Sentiment] ← Primary CTA│
516
+ └───────────────────────────────────────┘
517
+
518
+ ┌───────────────────────────────────────┐
519
+ │ 📊 Result │ <- Results Section
520
+ │ ┌─────────────────────────────────┐ │ (Appears after analysis)
521
+ │ │ 😊 POSITIVE │ │
522
+ │ │ Confidence: 99.8% │ │
523
+ │ │ ████████████████████░ 99.8% │ │
524
+ │ │ ⚡ Cached (2ms) │ │
525
+ │ └─────────────────────────────────┘ │
526
+ └───────────────────────────────────────┘
527
+
528
+ ┌───────────────────────────────────────┐
529
+ │ 📜 Recent Analyses │ <- History Section
530
+ │ ┌─────────────────────────────────┐ │
531
+ │ │ "I love this product..." POSITIVE│ │
532
+ │ │ 99.9% • 2 mins ago │ │
533
+ │ ├─────────────────────────────────┤ │
534
+ │ │ "This is terrible..." NEGATIVE │ │
535
+ │ │ 98.5% • 5 mins ago │ │
536
+ │ └─────────────────────────────────┘ │
537
+ │ [Load More] │
538
+ └───────────────────────────────────────┘
539
+
540
+ ┌───────────────────────────────────────┐
541
+ │ 📈 Cache Performance │ <- Stats Section
542
+ │ ┌─────────┬─────────┬──────────────┐│
543
+ │ │ Total │ Hit Rate│ Avg Response ││
544
+ │ │ 1,234 │ 87% │ 2ms ││
545
+ │ └─────────┴─────────┴──────────────┘│
546
+ └───────────────────────────────────────┘
547
+
548
+ ┌───────────────────────────────────────┐
549
+ │ Built with ❤️ by Syed Arfan │ <- Footer
550
+ │ [GitHub] [LinkedIn] [API Docs] │
551
+ └───────────────────────────────────────┘
552
+ ```
553
+
554
+ ---
555
+
556
+ ## 8. Development Phases
557
+
558
+ ### Phase 1: MVP (Week 1)
559
+ **Goal**: Core functionality working
560
+
561
+ **Tasks**:
562
+ - ✅ Set up Vite + React + TypeScript project
563
+ - ✅ Create basic layout structure
564
+ - ✅ Implement sentiment input component
565
+ - ✅ Connect to `/analyze` API endpoint
566
+ - ✅ Display results with basic styling
567
+ - ✅ Add history list (basic version)
568
+ - ✅ Responsive design (mobile + desktop)
569
+ - ✅ Basic error handling
570
+ - ✅ Deploy to GitHub Pages / Vercel
571
+
572
+ **Deliverable**: Working demo URL
573
+
574
+ ### Phase 2: Polish (Week 2)
575
+ **Goal**: Professional UX/UI
576
+
577
+ **Tasks**:
578
+ - ⬜ Add animations and transitions
579
+ - ⬜ Implement cache stats dashboard
580
+ - ⬜ Add loading skeletons
581
+ - ⬜ Improve error messages
582
+ - ⬜ Add sample text buttons
583
+ - ⬜ Implement search/filter in history
584
+ - ⬜ Add accessibility features (ARIA labels, keyboard nav)
585
+ - ⬜ Performance optimization (code splitting, lazy loading)
586
+
587
+ **Deliverable**: Production-ready application
588
+
589
+ ### Phase 3: Enhancements (Future)
590
+ **Goal**: Advanced features
591
+
592
+ **Tasks**:
593
+ - ⬜ Dark mode toggle
594
+ - ⬜ Export analysis data (CSV/JSON)
595
+ - ⬜ Charts for cache performance
596
+ - ⬜ Batch analysis (upload CSV)
597
+ - ⬜ User authentication (optional)
598
+ - ⬜ Personal dashboard with saved analyses
599
+
600
+ ---
601
+
602
+ ## 9. Non-Functional Requirements
603
+
604
+ ### 9.1 Performance
605
+ - **Page Load**: < 2 seconds (first contentful paint)
606
+ - **Time to Interactive**: < 3 seconds
607
+ - **API Response**: < 100ms (cached), < 2s (uncached)
608
+ - **Bundle Size**: < 500KB (gzipped)
609
+
610
+ ### 9.2 Accessibility
611
+ - WCAG 2.1 Level AA compliance
612
+ - Keyboard navigation support
613
+ - Screen reader compatible
614
+ - High contrast mode support
615
+ - Focus indicators visible
616
+
617
+ ### 9.3 Browser Support
618
+ - Chrome 90+ (primary)
619
+ - Firefox 88+
620
+ - Safari 14+
621
+ - Edge 90+
622
+ - Mobile Safari (iOS 14+)
623
+ - Chrome Mobile (Android 10+)
624
+
625
+ ### 9.4 Security
626
+ - Input sanitization (prevent XSS)
627
+ - HTTPS only in production
628
+ - CORS headers configured
629
+ - Rate limiting on API (handled by backend)
630
+ - No sensitive data in localStorage
631
+
632
+ ### 9.5 SEO
633
+ - Semantic HTML5 markup
634
+ - Meta tags (title, description, OG tags)
635
+ - Proper heading hierarchy (H1 → H6)
636
+ - Alt text on all images
637
+ - Sitemap.xml (if multi-page)
638
+
639
+ ---
640
+
641
+ ## 10. Testing Strategy
642
+
643
+ ### 10.1 Unit Tests
644
+ - Component rendering tests (React Testing Library)
645
+ - Hook logic tests (custom hooks)
646
+ - Utility function tests (formatters, validators)
647
+ - Target: 80%+ code coverage
648
+
649
+ ### 10.2 Integration Tests
650
+ - API integration tests (mock API responses)
651
+ - Form submission flows
652
+ - Error handling scenarios
653
+ - Cache stats refresh
654
+
655
+ ### 10.3 E2E Tests (Optional)
656
+ - Complete user flow (Cypress/Playwright)
657
+ - Cross-browser testing
658
+ - Mobile device testing
659
+
660
+ ### 10.4 Manual Testing
661
+ - Accessibility audit (Lighthouse, axe DevTools)
662
+ - Cross-browser testing (BrowserStack)
663
+ - Mobile device testing (real devices)
664
+ - Performance profiling (Chrome DevTools)
665
+
666
+ ---
667
+
668
+ ## 11. Deployment & DevOps
669
+
670
+ ### 11.1 Development Environment
671
+ ```bash
672
+ # Local development
673
+ npm run dev # Start Vite dev server (localhost:5173)
674
+ npm run build # Production build
675
+ npm run preview # Preview production build
676
+ npm run test # Run tests
677
+ npm run lint # ESLint
678
+ ```
679
+
680
+ ### 11.2 Deployment Options
681
+
682
+ **Option 1: Vercel (Recommended)**
683
+ - Zero-config deployment
684
+ - Automatic HTTPS
685
+ - CDN distribution
686
+ - Environment variables support
687
+ - Free tier available
688
+
689
+ **Option 2: GitHub Pages**
690
+ - Free hosting
691
+ - Automatic deployment from main branch
692
+ - Custom domain support
693
+ - Good for static sites
694
+
695
+ **Option 3: Netlify**
696
+ - Similar to Vercel
697
+ - Form handling built-in
698
+ - Function support
699
+ - Generous free tier
700
+
701
+ ### 11.3 CI/CD Pipeline
702
+
703
+ ```yaml
704
+ # .github/workflows/deploy.yml
705
+ name: Deploy Frontend
706
+ on:
707
+ push:
708
+ branches: [main]
709
+ jobs:
710
+ deploy:
711
+ runs-on: ubuntu-latest
712
+ steps:
713
+ - Checkout code
714
+ - Install dependencies
715
+ - Run tests
716
+ - Build production bundle
717
+ - Deploy to Vercel/Netlify
718
+ ```
719
+
720
+ ### 11.4 Environment Variables
721
+
722
+ ```bash
723
+ # .env.development
724
+ VITE_API_BASE_URL=http://localhost:8000
725
+
726
+ # .env.production
727
+ VITE_API_BASE_URL=http://localhost # nginx proxy
728
+ ```
729
+
730
+ ---
731
+
732
+ ## 12. Documentation Requirements
733
+
734
+ ### 12.1 README.md
735
+ - Project overview and features
736
+ - Live demo link
737
+ - Screenshots/GIF demos
738
+ - Installation instructions
739
+ - Development setup
740
+ - Tech stack details
741
+ - API integration details
742
+ - Contributing guidelines
743
+
744
+ ### 12.2 Code Comments
745
+ - JSDoc comments for complex functions
746
+ - Component props documentation
747
+ - API service documentation
748
+ - Type definitions documented
749
+
750
+ ### 12.3 User Guide (Optional)
751
+ - How to use the application
752
+ - Tips for best results
753
+ - Troubleshooting common issues
754
+
755
+ ---
756
+
757
+ ## 13. Success Criteria
758
+
759
+ ### 13.1 Definition of Done
760
+ - ✅ All P0 features implemented and tested
761
+ - ✅ Responsive on mobile, tablet, desktop
762
+ - ✅ Lighthouse score > 90 (all categories)
763
+ - ✅ Zero console errors/warnings
764
+ - ✅ API integration working (all endpoints)
765
+ - ✅ Deployed to production URL
766
+ - ✅ README documentation complete
767
+ - ✅ Code reviewed and refactored
768
+
769
+ ### 13.2 Launch Checklist
770
+ - [ ] All features working on production
771
+ - [ ] Performance optimized (bundle size, lazy loading)
772
+ - [ ] Accessibility tested and passing
773
+ - [ ] Cross-browser tested
774
+ - [ ] Mobile tested on real devices
775
+ - [ ] Error handling comprehensive
776
+ - [ ] Analytics integrated (optional)
777
+ - [ ] Social media preview cards working
778
+ - [ ] README and documentation complete
779
+ - [ ] GitHub repository organized
780
+
781
+ ---
782
+
783
+ ## 14. Risks & Mitigation
784
+
785
+ | Risk | Impact | Probability | Mitigation |
786
+ |------|--------|-------------|------------|
787
+ | API downtime | High | Low | Add offline mode with mock data |
788
+ | Slow API response | Medium | Medium | Loading states, timeout handling |
789
+ | Browser compatibility | Medium | Low | Polyfills, transpilation, testing |
790
+ | Over-engineering | Low | High | Stick to MVP scope, iterate later |
791
+ | Poor mobile UX | High | Medium | Mobile-first design, real device testing |
792
+
793
+ ---
794
+
795
+ ## 15. Future Enhancements (Post-MVP)
796
+
797
+ ### V1.1 Features
798
+ - Dark mode toggle with system preference detection
799
+ - Advanced charts for cache performance over time
800
+ - Keyboard shortcuts panel
801
+ - Confidence threshold slider (filter results)
802
+
803
+ ### V1.2 Features
804
+ - Export analysis history (CSV/JSON/PDF)
805
+ - Share result via URL/social media
806
+ - API key management (if backend adds auth)
807
+ - Custom themes/color schemes
808
+
809
+ ### V2.0 Features
810
+ - Batch analysis (upload CSV with multiple texts)
811
+ - User accounts and saved analyses
812
+ - Comparison view (compare two texts)
813
+ - Advanced analytics dashboard
814
+ - Sentiment trends over time
815
+ - API playground/Postman-like interface
816
+
817
+ ---
818
+
819
+ ## 16. Appendix
820
+
821
+ ### 16.1 Sample Texts for Testing
822
+
823
+ **Positive Examples**:
824
+ - "I absolutely love this product! It exceeded all my expectations and the customer service was fantastic."
825
+ - "This is the best experience I've ever had. Highly recommend to everyone!"
826
+ - "Amazing quality, fast delivery, and great value for money. Will definitely buy again!"
827
+
828
+ **Negative Examples**:
829
+ - "This is terrible. I'm extremely disappointed with the quality and service."
830
+ - "Worst purchase ever. Complete waste of money and time. Do not buy!"
831
+ - "Poor quality, slow delivery, and unhelpful customer support. Very frustrated."
832
+
833
+ **Edge Cases**:
834
+ - Empty string: ""
835
+ - Very short: "Bad"
836
+ - Maximum length: 5000 character text
837
+ - Special characters: "I ❤️ this! 🎉🎊"
838
+ - Mixed sentiment: "The product is great but the delivery was slow"
839
+
840
+ ### 16.2 API Response Examples
841
+
842
+ ```json
843
+ // Positive cached response
844
+ {
845
+ "text": "I love this!",
846
+ "sentiment": "POSITIVE",
847
+ "confidence": 0.9998,
848
+ "processing_time_ms": 2,
849
+ "cached": true
850
+ }
851
+
852
+ // Negative uncached response
853
+ {
854
+ "text": "This is terrible",
855
+ "sentiment": "NEGATIVE",
856
+ "confidence": 0.9875,
857
+ "processing_time_ms": 98,
858
+ "cached": false
859
+ }
860
+
861
+ // Error response
862
+ {
863
+ "detail": "Text must be between 1 and 5000 characters"
864
+ }
865
+ ```
866
+
867
+ ---
868
+
869
+ ## 17. Approval & Sign-off
870
+
871
+ **Prepared by**: Syed Arfan Hussain
872
+ **Date**: December 11, 2025
873
+ **Status**: Ready for Implementation
874
+
875
+ **Next Steps**:
876
+ 1. Review and approve PRD ✅
877
+ 2. Set up React + TypeScript project
878
+ 3. Begin Phase 1 (MVP) implementation
879
+ 4. Daily progress updates
880
+
881
+ ---
882
+
883
+ **End of PRD**
frontend/.env.example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # API Base URL
2
+ # For local development with Docker:
3
+ VITE_API_BASE_URL=http://localhost:8000
4
+
5
+ # For production with hosted backend:
6
+ # VITE_API_BASE_URL=https://your-api-domain.com
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Sentiment Analysis Frontend
2
+
3
+ A modern React frontend for the Sentiment Analysis API, featuring real-time sentiment analysis, history tracking, and cache performance monitoring.
4
+
5
+ ## Features
6
+
7
+ - **Real-time Sentiment Analysis**: Analyze text sentiment instantly
8
+ - **Visual Feedback**: Beautiful emoji-based sentiment indicators
9
+ - **Confidence Scores**: Progress bars showing prediction confidence
10
+ - **Analysis History**: Browse and filter previous analyses
11
+ - **Cache Statistics**: Monitor Redis cache performance
12
+ - **Responsive Design**: Works on all devices (320px+)
13
+ - **Accessibility**: WCAG AA compliant
14
+
15
+ ## Tech Stack
16
+
17
+ - **React 19** - UI library
18
+ - **TypeScript** - Type safety
19
+ - **Vite** - Build tool
20
+ - **Tailwind CSS 4** - Styling
21
+ - **Axios** - HTTP client
22
+ - **Lucide React** - Icons
23
+
24
+ ## Getting Started
25
+
26
+ ### Prerequisites
27
+
28
+ - Node.js 18+
29
+ - The Sentiment API backend running (see main README)
30
+
31
+ ### Installation
32
+
33
+ ```bash
34
+ # Install dependencies
35
+ npm install
36
+
37
+ # Start development server
38
+ npm run dev
39
+ ```
40
+
41
+ The app will be available at `http://localhost:5173`
42
+
43
+ ### Environment Variables
44
+
45
+ Create a `.env` file (copy from `.env.example`):
46
+
47
+ ```bash
48
+ # API Base URL
49
+ VITE_API_BASE_URL=http://localhost:8000
50
+ ```
51
+
52
+ ### Building for Production
53
+
54
+ ```bash
55
+ npm run build
56
+ ```
57
+
58
+ The built files will be in the `dist/` directory.
59
+
60
+ ## Project Structure
61
+
62
+ ```
63
+ src/
64
+ ├── components/ # React components
65
+ │ ├── Header.tsx # App header
66
+ │ ├── Footer.tsx # App footer
67
+ │ ├── SentimentAnalyzer.tsx # Main input/analysis
68
+ │ ├── ResultCard.tsx # Result display
69
+ │ ├── HistoryList.tsx # Analysis history
70
+ │ └── CacheStats.tsx # Cache dashboard
71
+ ├── hooks/ # Custom React hooks
72
+ │ ├── useSentimentAnalysis.ts
73
+ │ ├── useHistory.ts
74
+ │ └── useCacheStats.ts
75
+ ├── services/ # API services
76
+ │ └── api.ts # Axios instance & endpoints
77
+ ├── types/ # TypeScript types
78
+ │ └── index.ts
79
+ ├── utils/ # Utility functions
80
+ │ ├── formatters.ts # Date/number formatting
81
+ │ └── validators.ts # Input validation
82
+ ├── App.tsx # Root component
83
+ ├── main.tsx # Entry point
84
+ └── index.css # Global styles
85
+ ```
86
+
87
+ ## Deployment
88
+
89
+ ### GitHub Pages
90
+
91
+ The frontend is configured for GitHub Pages deployment. Push to `main` branch and the GitHub Actions workflow will automatically build and deploy.
92
+
93
+ Live URL: `https://simplyarfan.github.io/Sentiment-API/`
94
+
95
+ ### Other Platforms
96
+
97
+ - **Vercel**: Connect your GitHub repo
98
+ - **Netlify**: Connect your GitHub repo
99
+ - **Manual**: Run `npm run build` and upload `dist/` folder
100
+
101
+ ## API Endpoints Used
102
+
103
+ | Endpoint | Method | Description |
104
+ |----------|--------|-------------|
105
+ | `/analyze` | POST | Analyze text sentiment |
106
+ | `/history` | GET | Get analysis history |
107
+ | `/cache/stats` | GET | Get cache statistics |
108
+
109
+ ## License
110
+
111
+ MIT
frontend/eslint.config.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
frontend/index.html ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🧠</text></svg>" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
8
+ <!-- SEO Meta Tags -->
9
+ <title>Sentiment Analysis - AI-Powered Text Analysis</title>
10
+ <meta name="description" content="Analyze text sentiment in real-time using DistilBERT transformer model. Experience 99%+ accuracy with lightning-fast Redis caching." />
11
+ <meta name="keywords" content="sentiment analysis, NLP, machine learning, DistilBERT, text analysis, AI" />
12
+ <meta name="author" content="Syed Arfan Hussain" />
13
+
14
+ <!-- Open Graph / Social Media -->
15
+ <meta property="og:type" content="website" />
16
+ <meta property="og:title" content="Sentiment Analysis - AI-Powered Text Analysis" />
17
+ <meta property="og:description" content="Analyze text sentiment in real-time using DistilBERT transformer model with 99%+ accuracy." />
18
+ <meta property="og:url" content="https://simplyarfan.github.io/Sentiment-API/" />
19
+
20
+ <!-- Twitter Card -->
21
+ <meta name="twitter:card" content="summary_large_image" />
22
+ <meta name="twitter:title" content="Sentiment Analysis - AI-Powered Text Analysis" />
23
+ <meta name="twitter:description" content="Analyze text sentiment in real-time using DistilBERT transformer model with 99%+ accuracy." />
24
+
25
+ <!-- Theme Color -->
26
+ <meta name="theme-color" content="#4fc3f7" />
27
+
28
+ <!-- Fonts -->
29
+ <link rel="preconnect" href="https://fonts.googleapis.com">
30
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
31
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
32
+ </head>
33
+ <body>
34
+ <div id="root"></div>
35
+ <script type="module" src="/src/main.tsx"></script>
36
+ </body>
37
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sentiment-analysis-frontend",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "description": "React frontend for Sentiment Analysis API",
6
+ "author": "Syed Arfan Hussain",
7
+ "type": "module",
8
+ "scripts": {
9
+ "dev": "vite",
10
+ "build": "tsc -b && vite build",
11
+ "lint": "eslint .",
12
+ "preview": "vite preview"
13
+ },
14
+ "dependencies": {
15
+ "axios": "^1.13.2",
16
+ "lucide-react": "^0.559.0",
17
+ "react": "^19.2.0",
18
+ "react-dom": "^19.2.0"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/js": "^9.39.1",
22
+ "@tailwindcss/vite": "^4.1.17",
23
+ "@types/node": "^24.10.1",
24
+ "@types/react": "^19.2.5",
25
+ "@types/react-dom": "^19.2.3",
26
+ "@vitejs/plugin-react": "^5.1.1",
27
+ "eslint": "^9.39.1",
28
+ "eslint-plugin-react-hooks": "^7.0.1",
29
+ "eslint-plugin-react-refresh": "^0.4.24",
30
+ "globals": "^16.5.0",
31
+ "tailwindcss": "^4.1.17",
32
+ "typescript": "~5.9.3",
33
+ "typescript-eslint": "^8.46.4",
34
+ "vite": "^7.2.4"
35
+ }
36
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react';
2
+ import Header from './components/Header';
3
+ import Footer from './components/Footer';
4
+ import SentimentAnalyzer from './components/SentimentAnalyzer';
5
+ import HistoryList from './components/HistoryList';
6
+ import CacheStats from './components/CacheStats';
7
+
8
+ function App() {
9
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
10
+
11
+ const handleAnalysisComplete = useCallback(() => {
12
+ setRefreshTrigger(prev => prev + 1);
13
+ }, []);
14
+
15
+ return (
16
+ <div className="min-h-screen flex flex-col bg-gray-50">
17
+ <Header />
18
+
19
+ <main className="flex-1 py-8 px-4 sm:px-6 lg:px-8">
20
+ <div className="max-w-6xl mx-auto space-y-12">
21
+ {/* Sentiment Analyzer Section */}
22
+ <section>
23
+ <SentimentAnalyzer onAnalysisComplete={handleAnalysisComplete} />
24
+ </section>
25
+
26
+ {/* History and Stats Section */}
27
+ <section className="grid lg:grid-cols-2 gap-8">
28
+ <HistoryList refreshTrigger={refreshTrigger} />
29
+ <CacheStats />
30
+ </section>
31
+ </div>
32
+ </main>
33
+
34
+ <Footer />
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export default App;
frontend/src/components/CacheStats.tsx ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BarChart3, Zap, Database, HardDrive, RefreshCw, TrendingUp } from 'lucide-react';
2
+ import { useCacheStats } from '../hooks/useCacheStats';
3
+ import { formatMemory } from '../utils/formatters';
4
+
5
+ const CacheStats = () => {
6
+ const { stats, isLoading, error, refresh } = useCacheStats(30000);
7
+
8
+ if (error) {
9
+ return (
10
+ <div className="bg-white rounded-xl border border-gray-200 p-6">
11
+ <div className="text-center py-4">
12
+ <p className="text-red-500 text-sm mb-2">{error}</p>
13
+ <button
14
+ onClick={refresh}
15
+ className="text-sm text-blue-600 hover:underline"
16
+ >
17
+ Retry
18
+ </button>
19
+ </div>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ const hitRate = stats?.hit_rate ?? 0;
25
+ const hitRateColor = hitRate >= 80 ? 'text-green-600' : hitRate >= 50 ? 'text-yellow-600' : 'text-red-600';
26
+ const hitRateBg = hitRate >= 80 ? 'bg-green-500' : hitRate >= 50 ? 'bg-yellow-500' : 'bg-red-500';
27
+
28
+ return (
29
+ <div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
30
+ {/* Header */}
31
+ <div className="flex items-center justify-between p-4 border-b border-gray-100">
32
+ <div className="flex items-center gap-2">
33
+ <BarChart3 className="w-5 h-5 text-gray-500" />
34
+ <h3 className="font-semibold text-gray-900">Cache Performance</h3>
35
+ </div>
36
+ <button
37
+ onClick={refresh}
38
+ disabled={isLoading}
39
+ className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
40
+ aria-label="Refresh stats"
41
+ >
42
+ <RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
43
+ </button>
44
+ </div>
45
+
46
+ {/* Stats Grid */}
47
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 p-4">
48
+ {/* Total Analyses */}
49
+ <StatCard
50
+ icon={<Database className="w-5 h-5 text-blue-500" />}
51
+ label="Total Keys"
52
+ value={stats?.total_keys?.toLocaleString() ?? '-'}
53
+ isLoading={isLoading}
54
+ />
55
+
56
+ {/* Cache Hit Rate */}
57
+ <StatCard
58
+ icon={<Zap className="w-5 h-5 text-yellow-500" />}
59
+ label="Hit Rate"
60
+ value={stats ? `${hitRate.toFixed(1)}%` : '-'}
61
+ valueClass={hitRateColor}
62
+ isLoading={isLoading}
63
+ >
64
+ {stats && (
65
+ <div className="mt-2 h-1.5 bg-gray-200 rounded-full overflow-hidden">
66
+ <div
67
+ className={`h-full rounded-full transition-all duration-500 ${hitRateBg}`}
68
+ style={{ width: `${hitRate}%` }}
69
+ />
70
+ </div>
71
+ )}
72
+ </StatCard>
73
+
74
+ {/* Hits vs Misses */}
75
+ <StatCard
76
+ icon={<TrendingUp className="w-5 h-5 text-green-500" />}
77
+ label="Hits / Misses"
78
+ value={stats ? `${stats.hits} / ${stats.misses}` : '-'}
79
+ isLoading={isLoading}
80
+ />
81
+
82
+ {/* Memory Used */}
83
+ <StatCard
84
+ icon={<HardDrive className="w-5 h-5 text-purple-500" />}
85
+ label="Memory Used"
86
+ value={stats ? formatMemory(stats.memory_used_mb) : '-'}
87
+ isLoading={isLoading}
88
+ />
89
+ </div>
90
+
91
+ {/* Status Indicator */}
92
+ {stats && (
93
+ <div className="px-4 pb-4">
94
+ <div className="flex items-center gap-2 text-xs text-gray-500">
95
+ <span
96
+ className={`w-2 h-2 rounded-full ${
97
+ stats.status === 'connected' ? 'bg-green-500' : 'bg-red-500'
98
+ }`}
99
+ />
100
+ <span>
101
+ Redis {stats.status === 'connected' ? 'Connected' : 'Disconnected'}
102
+ </span>
103
+ <span className="text-gray-300">•</span>
104
+ <span>Auto-refreshes every 30s</span>
105
+ </div>
106
+ </div>
107
+ )}
108
+ </div>
109
+ );
110
+ };
111
+
112
+ interface StatCardProps {
113
+ icon: React.ReactNode;
114
+ label: string;
115
+ value: string;
116
+ valueClass?: string;
117
+ isLoading?: boolean;
118
+ children?: React.ReactNode;
119
+ }
120
+
121
+ const StatCard = ({ icon, label, value, valueClass = '', isLoading, children }: StatCardProps) => {
122
+ return (
123
+ <div className="p-4 bg-gray-50 rounded-lg">
124
+ <div className="flex items-center gap-2 mb-2">
125
+ {icon}
126
+ <span className="text-sm text-gray-600">{label}</span>
127
+ </div>
128
+ {isLoading ? (
129
+ <div className="h-8 bg-gray-200 rounded animate-pulse" />
130
+ ) : (
131
+ <>
132
+ <p className={`text-2xl font-bold font-mono ${valueClass || 'text-gray-900'}`}>
133
+ {value}
134
+ </p>
135
+ {children}
136
+ </>
137
+ )}
138
+ </div>
139
+ );
140
+ };
141
+
142
+ export default CacheStats;
frontend/src/components/Footer.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Github, Linkedin, Heart } from 'lucide-react';
2
+
3
+ const Footer = () => {
4
+ return (
5
+ <footer className="bg-white border-t border-gray-200 mt-auto">
6
+ <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
7
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-4">
8
+ {/* Credits */}
9
+ <div className="flex items-center gap-1 text-sm text-gray-600">
10
+ <span>Built with</span>
11
+ <Heart className="w-4 h-4 text-red-500 fill-red-500" />
12
+ <span>by</span>
13
+ <a
14
+ href="https://github.com/simplyarfan"
15
+ target="_blank"
16
+ rel="noopener noreferrer"
17
+ className="font-medium text-gray-900 hover:text-blue-600 transition-colors"
18
+ >
19
+ Syed Arfan Hussain
20
+ </a>
21
+ </div>
22
+
23
+ {/* Social Links */}
24
+ <div className="flex items-center gap-4">
25
+ <a
26
+ href="https://github.com/simplyarfan"
27
+ target="_blank"
28
+ rel="noopener noreferrer"
29
+ className="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
30
+ aria-label="GitHub"
31
+ >
32
+ <Github className="w-5 h-5" />
33
+ </a>
34
+ <a
35
+ href="https://www.linkedin.com/in/syed-arfan-hussain-7a3a95160/"
36
+ target="_blank"
37
+ rel="noopener noreferrer"
38
+ className="p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
39
+ aria-label="LinkedIn"
40
+ >
41
+ <Linkedin className="w-5 h-5" />
42
+ </a>
43
+ </div>
44
+
45
+ {/* Tech Stack */}
46
+ <div className="text-xs text-gray-500">
47
+ React + FastAPI + Redis + PostgreSQL
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </footer>
52
+ );
53
+ };
54
+
55
+ export default Footer;
frontend/src/components/Header.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Brain, Github, FileText } from 'lucide-react';
2
+
3
+ const Header = () => {
4
+ return (
5
+ <header className="bg-white border-b border-gray-200 sticky top-0 z-50">
6
+ <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
7
+ <div className="flex items-center justify-between h-16">
8
+ {/* Logo and Title */}
9
+ <div className="flex items-center gap-3">
10
+ <div className="p-2 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg">
11
+ <Brain className="w-6 h-6 text-white" />
12
+ </div>
13
+ <div>
14
+ <h1 className="text-xl font-bold text-gray-900">Sentiment Analysis</h1>
15
+ <p className="text-xs text-gray-500 hidden sm:block">Powered by DistilBERT</p>
16
+ </div>
17
+ </div>
18
+
19
+ {/* Navigation Links */}
20
+ <nav className="flex items-center gap-2 sm:gap-4">
21
+ <a
22
+ href="https://github.com/simplyarfan/Sentiment-API"
23
+ target="_blank"
24
+ rel="noopener noreferrer"
25
+ className="flex items-center gap-2 px-3 py-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
26
+ >
27
+ <Github className="w-4 h-4" />
28
+ <span className="hidden sm:inline">GitHub</span>
29
+ </a>
30
+ <a
31
+ href="https://github.com/simplyarfan/Sentiment-API#api-documentation"
32
+ target="_blank"
33
+ rel="noopener noreferrer"
34
+ className="flex items-center gap-2 px-3 py-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
35
+ >
36
+ <FileText className="w-4 h-4" />
37
+ <span className="hidden sm:inline">API Docs</span>
38
+ </a>
39
+ </nav>
40
+ </div>
41
+ </div>
42
+ </header>
43
+ );
44
+ };
45
+
46
+ export default Header;
frontend/src/components/HistoryList.tsx ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { History, RefreshCw, ChevronDown, Smile, Frown, Filter } from 'lucide-react';
3
+ import { useHistory } from '../hooks/useHistory';
4
+ import type { HistoryItem } from '../types';
5
+ import { formatConfidence, formatRelativeTime, truncateText } from '../utils/formatters';
6
+
7
+ type SentimentFilter = 'all' | 'POSITIVE' | 'NEGATIVE';
8
+
9
+ interface HistoryListProps {
10
+ refreshTrigger?: number;
11
+ }
12
+
13
+ const HistoryList = ({ refreshTrigger }: HistoryListProps) => {
14
+ const { history, total, isLoading, error, refresh, loadMore } = useHistory(10);
15
+ const [filter, setFilter] = useState<SentimentFilter>('all');
16
+ const [expandedId, setExpandedId] = useState<number | null>(null);
17
+
18
+ // Refresh when trigger changes
19
+ useEffect(() => {
20
+ if (refreshTrigger) {
21
+ refresh();
22
+ }
23
+ }, [refreshTrigger, refresh]);
24
+
25
+ const filteredHistory = filter === 'all'
26
+ ? history
27
+ : history.filter(item => item.sentiment === filter);
28
+
29
+ const handleItemClick = (id: number) => {
30
+ setExpandedId(expandedId === id ? null : id);
31
+ };
32
+
33
+ if (error) {
34
+ return (
35
+ <div className="bg-white rounded-xl border border-gray-200 p-6">
36
+ <div className="text-center py-8">
37
+ <p className="text-red-500 mb-4">{error}</p>
38
+ <button
39
+ onClick={refresh}
40
+ className="px-4 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
41
+ >
42
+ Try Again
43
+ </button>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ return (
50
+ <div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
51
+ {/* Header */}
52
+ <div className="flex items-center justify-between p-4 border-b border-gray-100">
53
+ <div className="flex items-center gap-2">
54
+ <History className="w-5 h-5 text-gray-500" />
55
+ <h3 className="font-semibold text-gray-900">Recent Analyses</h3>
56
+ {total > 0 && (
57
+ <span className="px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded-full">
58
+ {total}
59
+ </span>
60
+ )}
61
+ </div>
62
+ <div className="flex items-center gap-2">
63
+ {/* Filter */}
64
+ <div className="relative">
65
+ <select
66
+ value={filter}
67
+ onChange={(e) => setFilter(e.target.value as SentimentFilter)}
68
+ className="appearance-none pl-8 pr-8 py-1.5 text-sm bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-200"
69
+ >
70
+ <option value="all">All</option>
71
+ <option value="POSITIVE">Positive</option>
72
+ <option value="NEGATIVE">Negative</option>
73
+ </select>
74
+ <Filter className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
75
+ </div>
76
+ {/* Refresh Button */}
77
+ <button
78
+ onClick={refresh}
79
+ disabled={isLoading}
80
+ className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
81
+ aria-label="Refresh history"
82
+ >
83
+ <RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
84
+ </button>
85
+ </div>
86
+ </div>
87
+
88
+ {/* List */}
89
+ <div className="divide-y divide-gray-100">
90
+ {filteredHistory.length === 0 ? (
91
+ <div className="text-center py-12 px-4">
92
+ <History className="w-12 h-12 text-gray-300 mx-auto mb-4" />
93
+ <p className="text-gray-500 mb-2">No analysis history yet</p>
94
+ <p className="text-sm text-gray-400">
95
+ Analyze your first text above!
96
+ </p>
97
+ </div>
98
+ ) : (
99
+ filteredHistory.map((item: HistoryItem) => (
100
+ <HistoryItemRow
101
+ key={item.id}
102
+ item={item}
103
+ isExpanded={expandedId === item.id}
104
+ onClick={() => handleItemClick(item.id)}
105
+ />
106
+ ))
107
+ )}
108
+ </div>
109
+
110
+ {/* Load More */}
111
+ {history.length < total && (
112
+ <div className="p-4 border-t border-gray-100">
113
+ <button
114
+ onClick={loadMore}
115
+ disabled={isLoading}
116
+ className="w-full flex items-center justify-center gap-2 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg transition-colors disabled:opacity-50"
117
+ >
118
+ <ChevronDown className="w-4 h-4" />
119
+ Load More ({total - history.length} remaining)
120
+ </button>
121
+ </div>
122
+ )}
123
+ </div>
124
+ );
125
+ };
126
+
127
+ interface HistoryItemRowProps {
128
+ item: HistoryItem;
129
+ isExpanded: boolean;
130
+ onClick: () => void;
131
+ }
132
+
133
+ const HistoryItemRow = ({ item, isExpanded, onClick }: HistoryItemRowProps) => {
134
+ const isPositive = item.sentiment === 'POSITIVE';
135
+
136
+ return (
137
+ <div
138
+ className="p-4 hover:bg-gray-50 cursor-pointer transition-colors"
139
+ onClick={onClick}
140
+ >
141
+ <div className="flex items-start gap-3">
142
+ {/* Sentiment Icon */}
143
+ <div
144
+ className={`p-2 rounded-lg ${
145
+ isPositive ? 'bg-green-100' : 'bg-red-100'
146
+ }`}
147
+ >
148
+ {isPositive ? (
149
+ <Smile className="w-4 h-4 text-green-600" />
150
+ ) : (
151
+ <Frown className="w-4 h-4 text-red-600" />
152
+ )}
153
+ </div>
154
+
155
+ {/* Content */}
156
+ <div className="flex-1 min-w-0">
157
+ <p className="text-sm text-gray-900 mb-1">
158
+ {isExpanded ? item.text : truncateText(item.text, 60)}
159
+ </p>
160
+ <div className="flex flex-wrap items-center gap-2 text-xs text-gray-500">
161
+ <span
162
+ className={`px-2 py-0.5 rounded-full font-medium ${
163
+ isPositive
164
+ ? 'bg-green-100 text-green-700'
165
+ : 'bg-red-100 text-red-700'
166
+ }`}
167
+ >
168
+ {item.sentiment}
169
+ </span>
170
+ <span className="font-mono">{formatConfidence(item.confidence)}</span>
171
+ <span className="text-gray-400">•</span>
172
+ <span>{formatRelativeTime(item.created_at)}</span>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ );
178
+ };
179
+
180
+ export default HistoryList;
frontend/src/components/ResultCard.tsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Smile, Frown, Zap, Clock, CheckCircle } from 'lucide-react';
2
+ import type { SentimentResult } from '../types';
3
+ import { formatConfidence, formatProcessingTime, getConfidenceLevel, truncateText } from '../utils/formatters';
4
+
5
+ interface ResultCardProps {
6
+ result: SentimentResult;
7
+ }
8
+
9
+ const ResultCard = ({ result }: ResultCardProps) => {
10
+ const isPositive = result.sentiment === 'POSITIVE';
11
+ const confidencePercent = result.confidence * 100;
12
+
13
+ return (
14
+ <div className="animate-slide-up">
15
+ <div
16
+ className={`rounded-xl p-6 border-2 ${
17
+ isPositive
18
+ ? 'bg-gradient-to-br from-green-50 to-emerald-50 border-green-200'
19
+ : 'bg-gradient-to-br from-red-50 to-rose-50 border-red-200'
20
+ }`}
21
+ >
22
+ {/* Main Result */}
23
+ <div className="flex items-center gap-4 mb-6">
24
+ <div
25
+ className={`p-4 rounded-full animate-bounce-subtle ${
26
+ isPositive ? 'bg-green-100' : 'bg-red-100'
27
+ }`}
28
+ >
29
+ {isPositive ? (
30
+ <Smile className="w-12 h-12 text-green-600" />
31
+ ) : (
32
+ <Frown className="w-12 h-12 text-red-600" />
33
+ )}
34
+ </div>
35
+ <div>
36
+ <span
37
+ className={`inline-block px-4 py-2 rounded-full text-lg font-bold ${
38
+ isPositive
39
+ ? 'bg-green-500 text-white'
40
+ : 'bg-red-500 text-white'
41
+ }`}
42
+ >
43
+ {result.sentiment}
44
+ </span>
45
+ <p className="mt-2 text-sm text-gray-600">
46
+ {getConfidenceLevel(result.confidence)}
47
+ </p>
48
+ </div>
49
+ </div>
50
+
51
+ {/* Confidence Bar */}
52
+ <div className="mb-6">
53
+ <div className="flex justify-between items-center mb-2">
54
+ <span className="text-sm font-medium text-gray-700">Confidence</span>
55
+ <span className="text-lg font-bold font-mono">
56
+ {formatConfidence(result.confidence)}
57
+ </span>
58
+ </div>
59
+ <div className="h-3 bg-gray-200 rounded-full overflow-hidden">
60
+ <div
61
+ className={`h-full rounded-full animate-fill-bar ${
62
+ isPositive ? 'bg-green-500' : 'bg-red-500'
63
+ }`}
64
+ style={{ width: `${confidencePercent}%` }}
65
+ />
66
+ </div>
67
+ </div>
68
+
69
+ {/* Metadata */}
70
+ <div className="flex flex-wrap gap-4">
71
+ <div className="flex items-center gap-2">
72
+ {result.cached ? (
73
+ <Zap className="w-4 h-4 text-yellow-500" />
74
+ ) : (
75
+ <Clock className="w-4 h-4 text-blue-500" />
76
+ )}
77
+ <span className="text-sm text-gray-600">
78
+ {result.cached ? (
79
+ <span className="text-green-600 font-medium">
80
+ Cached ({formatProcessingTime(result.processing_time_ms)})
81
+ </span>
82
+ ) : (
83
+ <span className="text-blue-600 font-medium">
84
+ Processed ({formatProcessingTime(result.processing_time_ms)})
85
+ </span>
86
+ )}
87
+ </span>
88
+ </div>
89
+ <div className="flex items-center gap-2">
90
+ <CheckCircle className="w-4 h-4 text-gray-400" />
91
+ <span className="text-sm text-gray-500">
92
+ {truncateText(result.text, 50)}
93
+ </span>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ );
99
+ };
100
+
101
+ export default ResultCard;
frontend/src/components/SentimentAnalyzer.tsx ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import { Send, Loader2, Trash2, Sparkles } from 'lucide-react';
3
+ import { useSentimentAnalysis } from '../hooks/useSentimentAnalysis';
4
+ import { validateText, MAX_TEXT_LENGTH } from '../utils/validators';
5
+ import ResultCard from './ResultCard';
6
+
7
+ const SAMPLE_TEXTS = {
8
+ positive: "I absolutely love this product! It exceeded all my expectations and the customer service was fantastic. Highly recommend to everyone!",
9
+ negative: "This is terrible. I'm extremely disappointed with the quality and service. Complete waste of money and time. Do not buy!",
10
+ };
11
+
12
+ interface SentimentAnalyzerProps {
13
+ onAnalysisComplete?: () => void;
14
+ }
15
+
16
+ const SentimentAnalyzer = ({ onAnalysisComplete }: SentimentAnalyzerProps) => {
17
+ const [text, setText] = useState('');
18
+ const [validationError, setValidationError] = useState<string | null>(null);
19
+ const { result, isLoading, error, analyze, reset } = useSentimentAnalysis();
20
+
21
+ const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
22
+ const newText = e.target.value;
23
+ setText(newText);
24
+
25
+ if (newText.trim()) {
26
+ const validation = validateText(newText);
27
+ setValidationError(validation.error);
28
+ } else {
29
+ setValidationError(null);
30
+ }
31
+ };
32
+
33
+ const handleSubmit = useCallback(async (e?: React.FormEvent) => {
34
+ e?.preventDefault();
35
+
36
+ const validation = validateText(text);
37
+ if (!validation.isValid) {
38
+ setValidationError(validation.error);
39
+ return;
40
+ }
41
+
42
+ await analyze(text.trim());
43
+ onAnalysisComplete?.();
44
+ }, [text, analyze, onAnalysisComplete]);
45
+
46
+ const handleKeyDown = (e: React.KeyboardEvent) => {
47
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
48
+ handleSubmit();
49
+ }
50
+ };
51
+
52
+ const handleClear = () => {
53
+ setText('');
54
+ setValidationError(null);
55
+ reset();
56
+ };
57
+
58
+ const handleSampleText = (type: 'positive' | 'negative') => {
59
+ setText(SAMPLE_TEXTS[type]);
60
+ setValidationError(null);
61
+ reset();
62
+ };
63
+
64
+ // Scroll to result when analysis completes
65
+ useEffect(() => {
66
+ if (result) {
67
+ document.getElementById('result-section')?.scrollIntoView({ behavior: 'smooth' });
68
+ }
69
+ }, [result]);
70
+
71
+ const isValid = text.trim().length > 0 && !validationError;
72
+ const charCount = text.length;
73
+ const charCountClass = charCount > MAX_TEXT_LENGTH
74
+ ? 'text-red-500'
75
+ : charCount > MAX_TEXT_LENGTH * 0.9
76
+ ? 'text-yellow-500'
77
+ : 'text-gray-400';
78
+
79
+ return (
80
+ <div className="space-y-6">
81
+ {/* Hero Section */}
82
+ <div className="text-center mb-8">
83
+ <h2 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-3">
84
+ Analyze Text Sentiment in Real-Time
85
+ </h2>
86
+ <p className="text-gray-600 max-w-2xl mx-auto">
87
+ Powered by DistilBERT transformer model with 99%+ accuracy.
88
+ Experience lightning-fast results with Redis caching.
89
+ </p>
90
+ </div>
91
+
92
+ {/* Input Section */}
93
+ <form onSubmit={handleSubmit} className="space-y-4">
94
+ <div className="relative">
95
+ <textarea
96
+ value={text}
97
+ onChange={handleTextChange}
98
+ onKeyDown={handleKeyDown}
99
+ placeholder="Type or paste your text here to analyze sentiment..."
100
+ className={`w-full min-h-[150px] max-h-[400px] p-4 text-gray-900 bg-white border-2 rounded-xl resize-y focus:outline-none focus:ring-2 transition-all ${
101
+ validationError
102
+ ? 'border-red-300 focus:ring-red-200 focus:border-red-400'
103
+ : 'border-gray-200 focus:ring-blue-200 focus:border-blue-400'
104
+ }`}
105
+ disabled={isLoading}
106
+ aria-label="Text input for sentiment analysis"
107
+ aria-invalid={!!validationError}
108
+ aria-describedby={validationError ? 'validation-error' : undefined}
109
+ />
110
+ <div className="absolute bottom-3 right-3 flex items-center gap-2">
111
+ <span className={`text-sm font-mono ${charCountClass}`}>
112
+ {charCount}/{MAX_TEXT_LENGTH}
113
+ </span>
114
+ </div>
115
+ </div>
116
+
117
+ {/* Validation Error */}
118
+ {validationError && (
119
+ <p id="validation-error" className="text-sm text-red-500 animate-fade-in">
120
+ {validationError}
121
+ </p>
122
+ )}
123
+
124
+ {/* API Error */}
125
+ {error && (
126
+ <div className="p-4 bg-red-50 border border-red-200 rounded-lg animate-fade-in">
127
+ <p className="text-sm text-red-600">{error}</p>
128
+ </div>
129
+ )}
130
+
131
+ {/* Sample Text Buttons */}
132
+ <div className="flex flex-wrap items-center gap-2">
133
+ <span className="text-sm text-gray-500">Try examples:</span>
134
+ <button
135
+ type="button"
136
+ onClick={() => handleSampleText('positive')}
137
+ className="flex items-center gap-1 px-3 py-1.5 text-sm text-green-700 bg-green-50 hover:bg-green-100 rounded-lg transition-colors"
138
+ disabled={isLoading}
139
+ >
140
+ <Sparkles className="w-3 h-3" />
141
+ Positive
142
+ </button>
143
+ <button
144
+ type="button"
145
+ onClick={() => handleSampleText('negative')}
146
+ className="flex items-center gap-1 px-3 py-1.5 text-sm text-red-700 bg-red-50 hover:bg-red-100 rounded-lg transition-colors"
147
+ disabled={isLoading}
148
+ >
149
+ <Sparkles className="w-3 h-3" />
150
+ Negative
151
+ </button>
152
+ {text && (
153
+ <button
154
+ type="button"
155
+ onClick={handleClear}
156
+ className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
157
+ disabled={isLoading}
158
+ >
159
+ <Trash2 className="w-3 h-3" />
160
+ Clear
161
+ </button>
162
+ )}
163
+ </div>
164
+
165
+ {/* Submit Button */}
166
+ <button
167
+ type="submit"
168
+ disabled={!isValid || isLoading}
169
+ className={`w-full flex items-center justify-center gap-2 px-6 py-4 text-lg font-semibold rounded-xl transition-all ${
170
+ isValid && !isLoading
171
+ ? 'bg-gradient-to-r from-blue-500 to-purple-600 text-white hover:from-blue-600 hover:to-purple-700 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5'
172
+ : 'bg-gray-200 text-gray-400 cursor-not-allowed'
173
+ }`}
174
+ >
175
+ {isLoading ? (
176
+ <>
177
+ <Loader2 className="w-5 h-5 animate-spin" />
178
+ Analyzing...
179
+ </>
180
+ ) : (
181
+ <>
182
+ <Send className="w-5 h-5" />
183
+ Analyze Sentiment
184
+ </>
185
+ )}
186
+ </button>
187
+
188
+ <p className="text-center text-xs text-gray-400">
189
+ Press <kbd className="px-1.5 py-0.5 bg-gray-100 rounded text-gray-600">Ctrl</kbd> + <kbd className="px-1.5 py-0.5 bg-gray-100 rounded text-gray-600">Enter</kbd> to analyze
190
+ </p>
191
+ </form>
192
+
193
+ {/* Result Section */}
194
+ {result && (
195
+ <div id="result-section" className="pt-4">
196
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">Result</h3>
197
+ <ResultCard result={result} />
198
+ </div>
199
+ )}
200
+ </div>
201
+ );
202
+ };
203
+
204
+ export default SentimentAnalyzer;
frontend/src/hooks/useCacheStats.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import { getCacheStats } from '../services/api';
3
+ import type { CacheStats } from '../types';
4
+
5
+ interface UseCacheStatsReturn {
6
+ stats: CacheStats | null;
7
+ isLoading: boolean;
8
+ error: string | null;
9
+ refresh: () => Promise<void>;
10
+ }
11
+
12
+ export const useCacheStats = (autoRefreshInterval: number = 30000): UseCacheStatsReturn => {
13
+ const [stats, setStats] = useState<CacheStats | null>(null);
14
+ const [isLoading, setIsLoading] = useState(false);
15
+ const [error, setError] = useState<string | null>(null);
16
+
17
+ const fetchStats = useCallback(async () => {
18
+ setIsLoading(true);
19
+ setError(null);
20
+
21
+ try {
22
+ const response = await getCacheStats();
23
+ setStats(response);
24
+ } catch (err) {
25
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load cache stats';
26
+ setError(errorMessage);
27
+ } finally {
28
+ setIsLoading(false);
29
+ }
30
+ }, []);
31
+
32
+ const refresh = useCallback(async () => {
33
+ await fetchStats();
34
+ }, [fetchStats]);
35
+
36
+ // Initial fetch
37
+ useEffect(() => {
38
+ fetchStats();
39
+ }, [fetchStats]);
40
+
41
+ // Auto-refresh
42
+ useEffect(() => {
43
+ if (autoRefreshInterval > 0) {
44
+ const interval = setInterval(fetchStats, autoRefreshInterval);
45
+ return () => clearInterval(interval);
46
+ }
47
+ }, [fetchStats, autoRefreshInterval]);
48
+
49
+ return {
50
+ stats,
51
+ isLoading,
52
+ error,
53
+ refresh,
54
+ };
55
+ };
frontend/src/hooks/useHistory.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import { getHistory } from '../services/api';
3
+ import type { HistoryItem } from '../types';
4
+
5
+ interface UseHistoryReturn {
6
+ history: HistoryItem[];
7
+ total: number;
8
+ isLoading: boolean;
9
+ error: string | null;
10
+ refresh: () => Promise<void>;
11
+ loadMore: () => Promise<void>;
12
+ }
13
+
14
+ export const useHistory = (initialLimit: number = 10): UseHistoryReturn => {
15
+ const [history, setHistory] = useState<HistoryItem[]>([]);
16
+ const [total, setTotal] = useState(0);
17
+ const [limit, setLimit] = useState(initialLimit);
18
+ const [isLoading, setIsLoading] = useState(false);
19
+ const [error, setError] = useState<string | null>(null);
20
+
21
+ const fetchHistory = useCallback(async (fetchLimit: number) => {
22
+ setIsLoading(true);
23
+ setError(null);
24
+
25
+ try {
26
+ const response = await getHistory(fetchLimit);
27
+ setHistory(response.analyses);
28
+ setTotal(response.total);
29
+ } catch (err) {
30
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load history';
31
+ setError(errorMessage);
32
+ } finally {
33
+ setIsLoading(false);
34
+ }
35
+ }, []);
36
+
37
+ const refresh = useCallback(async () => {
38
+ await fetchHistory(limit);
39
+ }, [fetchHistory, limit]);
40
+
41
+ const loadMore = useCallback(async () => {
42
+ const newLimit = limit + 10;
43
+ setLimit(newLimit);
44
+ await fetchHistory(newLimit);
45
+ }, [fetchHistory, limit]);
46
+
47
+ useEffect(() => {
48
+ fetchHistory(initialLimit);
49
+ }, [fetchHistory, initialLimit]);
50
+
51
+ return {
52
+ history,
53
+ total,
54
+ isLoading,
55
+ error,
56
+ refresh,
57
+ loadMore,
58
+ };
59
+ };
frontend/src/hooks/useSentimentAnalysis.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react';
2
+ import { analyzeSentiment } from '../services/api';
3
+ import type { SentimentResult } from '../types';
4
+
5
+ interface UseSentimentAnalysisReturn {
6
+ result: SentimentResult | null;
7
+ isLoading: boolean;
8
+ error: string | null;
9
+ analyze: (text: string) => Promise<void>;
10
+ reset: () => void;
11
+ }
12
+
13
+ export const useSentimentAnalysis = (): UseSentimentAnalysisReturn => {
14
+ const [result, setResult] = useState<SentimentResult | null>(null);
15
+ const [isLoading, setIsLoading] = useState(false);
16
+ const [error, setError] = useState<string | null>(null);
17
+
18
+ const analyze = useCallback(async (text: string) => {
19
+ setIsLoading(true);
20
+ setError(null);
21
+
22
+ try {
23
+ const response = await analyzeSentiment(text);
24
+ setResult(response);
25
+ } catch (err) {
26
+ const errorMessage = err instanceof Error ? err.message : 'Analysis failed';
27
+ setError(errorMessage);
28
+ setResult(null);
29
+ } finally {
30
+ setIsLoading(false);
31
+ }
32
+ }, []);
33
+
34
+ const reset = useCallback(() => {
35
+ setResult(null);
36
+ setError(null);
37
+ setIsLoading(false);
38
+ }, []);
39
+
40
+ return {
41
+ result,
42
+ isLoading,
43
+ error,
44
+ analyze,
45
+ reset,
46
+ };
47
+ };
frontend/src/index.css ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ /* Base styles */
4
+ body {
5
+ font-family: 'Inter', system-ui, sans-serif;
6
+ @apply bg-gray-50 text-gray-900 antialiased;
7
+ }
8
+
9
+ /* Custom animations */
10
+ @keyframes fadeIn {
11
+ from { opacity: 0; transform: translateY(10px); }
12
+ to { opacity: 1; transform: translateY(0); }
13
+ }
14
+
15
+ @keyframes slideUp {
16
+ from { opacity: 0; transform: translateY(20px); }
17
+ to { opacity: 1; transform: translateY(0); }
18
+ }
19
+
20
+ @keyframes pulse-slow {
21
+ 0%, 100% { opacity: 1; }
22
+ 50% { opacity: 0.7; }
23
+ }
24
+
25
+ @keyframes bounce-subtle {
26
+ 0%, 100% { transform: translateY(0); }
27
+ 50% { transform: translateY(-5px); }
28
+ }
29
+
30
+ .animate-fade-in {
31
+ animation: fadeIn 0.3s ease-out forwards;
32
+ }
33
+
34
+ .animate-slide-up {
35
+ animation: slideUp 0.4s ease-out forwards;
36
+ }
37
+
38
+ .animate-pulse-slow {
39
+ animation: pulse-slow 2s ease-in-out infinite;
40
+ }
41
+
42
+ .animate-bounce-subtle {
43
+ animation: bounce-subtle 1s ease-in-out;
44
+ }
45
+
46
+ /* Progress bar animation */
47
+ @keyframes fillBar {
48
+ from { width: 0%; }
49
+ }
50
+
51
+ .animate-fill-bar {
52
+ animation: fillBar 0.8s ease-out forwards;
53
+ }
54
+
55
+ /* Monospace for metrics */
56
+ .font-mono {
57
+ font-family: 'Fira Code', 'Courier New', monospace;
58
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/src/services/api.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios, { type AxiosError } from 'axios';
2
+ import type { SentimentResult, HistoryResponse, CacheStats, ApiError } from '../types';
3
+
4
+ // API base URL - configurable via environment variable
5
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
6
+
7
+ // Create axios instance with default config
8
+ const api = axios.create({
9
+ baseURL: API_BASE_URL,
10
+ timeout: 10000,
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ });
15
+
16
+ // Error handler
17
+ const handleApiError = (error: AxiosError<ApiError>): never => {
18
+ if (error.response) {
19
+ // Server responded with error
20
+ const message = error.response.data?.detail || 'An error occurred';
21
+ throw new Error(message);
22
+ } else if (error.request) {
23
+ // Request made but no response
24
+ throw new Error('Unable to reach the server. Please check if the API is running.');
25
+ } else {
26
+ // Request setup error
27
+ throw new Error(error.message || 'An unexpected error occurred');
28
+ }
29
+ };
30
+
31
+ // API functions
32
+ export const analyzeSentiment = async (text: string): Promise<SentimentResult> => {
33
+ try {
34
+ const response = await api.post<SentimentResult>('/analyze', { text });
35
+ return response.data;
36
+ } catch (error) {
37
+ return handleApiError(error as AxiosError<ApiError>);
38
+ }
39
+ };
40
+
41
+ export const getHistory = async (limit: number = 10): Promise<HistoryResponse> => {
42
+ try {
43
+ const response = await api.get<HistoryResponse>(`/history?limit=${limit}`);
44
+ return response.data;
45
+ } catch (error) {
46
+ return handleApiError(error as AxiosError<ApiError>);
47
+ }
48
+ };
49
+
50
+ export const getCacheStats = async (): Promise<CacheStats> => {
51
+ try {
52
+ const response = await api.get<CacheStats>('/cache/stats');
53
+ return response.data;
54
+ } catch (error) {
55
+ return handleApiError(error as AxiosError<ApiError>);
56
+ }
57
+ };
58
+
59
+ export const clearCache = async (): Promise<void> => {
60
+ try {
61
+ await api.delete('/cache/clear');
62
+ } catch (error) {
63
+ return handleApiError(error as AxiosError<ApiError>);
64
+ }
65
+ };
66
+
67
+ export const checkHealth = async (): Promise<boolean> => {
68
+ try {
69
+ await api.get('/health');
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ };
75
+
76
+ export default api;
frontend/src/types/index.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // API Response Types
2
+ export interface SentimentResult {
3
+ text: string;
4
+ sentiment: 'POSITIVE' | 'NEGATIVE';
5
+ confidence: number;
6
+ processing_time_ms: number;
7
+ cached: boolean;
8
+ created_at?: string;
9
+ }
10
+
11
+ export interface HistoryItem {
12
+ id: number;
13
+ text: string;
14
+ sentiment: 'POSITIVE' | 'NEGATIVE';
15
+ confidence: number;
16
+ processing_time_ms: number;
17
+ created_at: string;
18
+ }
19
+
20
+ export interface HistoryResponse {
21
+ total: number;
22
+ analyses: HistoryItem[];
23
+ }
24
+
25
+ export interface CacheStats {
26
+ status: string;
27
+ total_keys: number;
28
+ sentiment_keys: number;
29
+ memory_used_mb: number;
30
+ hits: number;
31
+ misses: number;
32
+ hit_rate: number;
33
+ }
34
+
35
+ export interface ApiError {
36
+ detail: string;
37
+ }
38
+
39
+ // Component Props Types
40
+ export interface ResultCardProps {
41
+ result: SentimentResult;
42
+ }
43
+
44
+ export interface HistoryItemProps {
45
+ item: HistoryItem;
46
+ onClick?: () => void;
47
+ }
frontend/src/utils/formatters.ts ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Format confidence as percentage
2
+ export const formatConfidence = (confidence: number): string => {
3
+ return `${(confidence * 100).toFixed(1)}%`;
4
+ };
5
+
6
+ // Format processing time
7
+ export const formatProcessingTime = (ms: number): string => {
8
+ if (ms < 1000) {
9
+ return `${ms}ms`;
10
+ }
11
+ return `${(ms / 1000).toFixed(2)}s`;
12
+ };
13
+
14
+ // Format relative time (e.g., "2 mins ago")
15
+ export const formatRelativeTime = (dateString: string): string => {
16
+ const date = new Date(dateString);
17
+ const now = new Date();
18
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
19
+
20
+ if (diffInSeconds < 60) {
21
+ return 'just now';
22
+ }
23
+
24
+ const diffInMinutes = Math.floor(diffInSeconds / 60);
25
+ if (diffInMinutes < 60) {
26
+ return `${diffInMinutes} min${diffInMinutes > 1 ? 's' : ''} ago`;
27
+ }
28
+
29
+ const diffInHours = Math.floor(diffInMinutes / 60);
30
+ if (diffInHours < 24) {
31
+ return `${diffInHours} hour${diffInHours > 1 ? 's' : ''} ago`;
32
+ }
33
+
34
+ const diffInDays = Math.floor(diffInHours / 24);
35
+ if (diffInDays < 7) {
36
+ return `${diffInDays} day${diffInDays > 1 ? 's' : ''} ago`;
37
+ }
38
+
39
+ return date.toLocaleDateString();
40
+ };
41
+
42
+ // Truncate text with ellipsis
43
+ export const truncateText = (text: string, maxLength: number): string => {
44
+ if (text.length <= maxLength) {
45
+ return text;
46
+ }
47
+ return `${text.substring(0, maxLength)}...`;
48
+ };
49
+
50
+ // Format memory size
51
+ export const formatMemory = (mb: number): string => {
52
+ if (mb < 1) {
53
+ return `${(mb * 1024).toFixed(0)} KB`;
54
+ }
55
+ return `${mb.toFixed(2)} MB`;
56
+ };
57
+
58
+ // Get confidence level label
59
+ export const getConfidenceLevel = (confidence: number): string => {
60
+ if (confidence >= 0.9) return 'High confidence';
61
+ if (confidence >= 0.7) return 'Medium confidence';
62
+ return 'Low confidence';
63
+ };
frontend/src/utils/validators.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const MIN_TEXT_LENGTH = 1;
2
+ export const MAX_TEXT_LENGTH = 512;
3
+
4
+ export interface ValidationResult {
5
+ isValid: boolean;
6
+ error: string | null;
7
+ }
8
+
9
+ export const validateText = (text: string): ValidationResult => {
10
+ const trimmedText = text.trim();
11
+
12
+ if (trimmedText.length === 0) {
13
+ return {
14
+ isValid: false,
15
+ error: 'Please enter some text to analyze',
16
+ };
17
+ }
18
+
19
+ if (trimmedText.length < MIN_TEXT_LENGTH) {
20
+ return {
21
+ isValid: false,
22
+ error: `Text must be at least ${MIN_TEXT_LENGTH} character`,
23
+ };
24
+ }
25
+
26
+ if (trimmedText.length > MAX_TEXT_LENGTH) {
27
+ return {
28
+ isValid: false,
29
+ error: `Text must be ${MAX_TEXT_LENGTH} characters or less`,
30
+ };
31
+ }
32
+
33
+ return {
34
+ isValid: true,
35
+ error: null,
36
+ };
37
+ };
frontend/tsconfig.app.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true
26
+ },
27
+ "include": ["src"]
28
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ base: '/Sentiment-API/', // For GitHub Pages deployment
9
+ })