Spaces:
Runtime error
Runtime error
Commit
·
0509f82
1
Parent(s):
f154c9d
Sentiment Analysis
Browse files- .gitignore +23 -0
- Dockerfile +25 -0
- package-lock.json +0 -0
- package.json +44 -0
- public/favicon.ico +0 -0
- public/index.html +14 -0
- public/logo192.png +0 -0
- public/logo512.png +0 -0
- public/manifest.json +25 -0
- public/robots.txt +3 -0
- src/App.css +38 -0
- src/App.js +86 -0
- src/App.test.js +8 -0
- src/components/AboutSection.js +119 -0
- src/components/Footer.js +70 -0
- src/components/Header.js +33 -0
- src/components/PreviousAnalysis.js +99 -0
- src/components/ReviewInput.js +85 -0
- src/components/SentimentResult.js +124 -0
- src/index.css +13 -0
- src/index.js +17 -0
- src/lib/api.js +27 -0
- src/logo.svg +1 -0
- src/pages/Home.js +91 -0
- src/pages/not-found.js +44 -0
- src/reportWebVitals.js +13 -0
- src/setupTests.js +5 -0
.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
# dependencies
|
4 |
+
/node_modules
|
5 |
+
/.pnp
|
6 |
+
.pnp.js
|
7 |
+
|
8 |
+
# testing
|
9 |
+
/coverage
|
10 |
+
|
11 |
+
# production
|
12 |
+
/build
|
13 |
+
|
14 |
+
# misc
|
15 |
+
.DS_Store
|
16 |
+
.env.local
|
17 |
+
.env.development.local
|
18 |
+
.env.test.local
|
19 |
+
.env.production.local
|
20 |
+
|
21 |
+
npm-debug.log*
|
22 |
+
yarn-debug.log*
|
23 |
+
yarn-error.log*
|
Dockerfile
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use an official Node.js runtime as a parent image
|
2 |
+
FROM node:16
|
3 |
+
|
4 |
+
# Set the working directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy package.json and install dependencies
|
8 |
+
COPY package.json .
|
9 |
+
RUN npm install
|
10 |
+
|
11 |
+
# Copy the rest of the application code
|
12 |
+
COPY . .
|
13 |
+
|
14 |
+
# Build the React app
|
15 |
+
RUN npm run build
|
16 |
+
|
17 |
+
# Use an Nginx image to serve the built app
|
18 |
+
FROM nginx:alpine
|
19 |
+
COPY --from=0 /app/build /usr/share/nginx/html
|
20 |
+
|
21 |
+
# Expose the port Nginx runs on
|
22 |
+
EXPOSE 80
|
23 |
+
|
24 |
+
# Start Nginx
|
25 |
+
CMD ["nginx", "-g", "daemon off;"]
|
package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
package.json
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "sentimental-analysis",
|
3 |
+
"version": "0.1.0",
|
4 |
+
"private": true,
|
5 |
+
"dependencies": {
|
6 |
+
"@testing-library/dom": "^10.4.0",
|
7 |
+
"@testing-library/jest-dom": "^6.6.3",
|
8 |
+
"@testing-library/react": "^16.3.0",
|
9 |
+
"@testing-library/user-event": "^13.5.0",
|
10 |
+
"react": "^19.1.0",
|
11 |
+
"react-dom": "^19.1.0",
|
12 |
+
"react-scripts": "5.0.1",
|
13 |
+
"web-vitals": "^2.1.4",
|
14 |
+
"@mui/icons-material": "^7.0.2",
|
15 |
+
"@mui/material": "^7.0.2",
|
16 |
+
"wouter": "^3.3.5",
|
17 |
+
"@emotion/react": "^11.14.0",
|
18 |
+
"@emotion/styled": "^11.14.0"
|
19 |
+
},
|
20 |
+
"scripts": {
|
21 |
+
"start": "react-scripts start",
|
22 |
+
"build": "react-scripts build",
|
23 |
+
"test": "react-scripts test",
|
24 |
+
"eject": "react-scripts eject"
|
25 |
+
},
|
26 |
+
"eslintConfig": {
|
27 |
+
"extends": [
|
28 |
+
"react-app",
|
29 |
+
"react-app/jest"
|
30 |
+
]
|
31 |
+
},
|
32 |
+
"browserslist": {
|
33 |
+
"production": [
|
34 |
+
">0.2%",
|
35 |
+
"not dead",
|
36 |
+
"not op_mini all"
|
37 |
+
],
|
38 |
+
"development": [
|
39 |
+
"last 1 chrome version",
|
40 |
+
"last 1 firefox version",
|
41 |
+
"last 1 safari version"
|
42 |
+
]
|
43 |
+
}
|
44 |
+
}
|
public/favicon.ico
ADDED
|
public/index.html
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
6 |
+
<meta name="theme-color" content="#000000" />
|
7 |
+
<meta name="description" content="Movie Review Sentiment Analysis Tool" />
|
8 |
+
<title>Movie Review Sentiment Analysis</title>
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
12 |
+
<div id="root"></div>
|
13 |
+
</body>
|
14 |
+
</html>
|
public/logo192.png
ADDED
![]() |
public/logo512.png
ADDED
![]() |
public/manifest.json
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"short_name": "React App",
|
3 |
+
"name": "Create React App Sample",
|
4 |
+
"icons": [
|
5 |
+
{
|
6 |
+
"src": "favicon.ico",
|
7 |
+
"sizes": "64x64 32x32 24x24 16x16",
|
8 |
+
"type": "image/x-icon"
|
9 |
+
},
|
10 |
+
{
|
11 |
+
"src": "logo192.png",
|
12 |
+
"type": "image/png",
|
13 |
+
"sizes": "192x192"
|
14 |
+
},
|
15 |
+
{
|
16 |
+
"src": "logo512.png",
|
17 |
+
"type": "image/png",
|
18 |
+
"sizes": "512x512"
|
19 |
+
}
|
20 |
+
],
|
21 |
+
"start_url": ".",
|
22 |
+
"display": "standalone",
|
23 |
+
"theme_color": "#000000",
|
24 |
+
"background_color": "#ffffff"
|
25 |
+
}
|
public/robots.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# https://www.robotstxt.org/robotstxt.html
|
2 |
+
User-agent: *
|
3 |
+
Disallow:
|
src/App.css
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.App {
|
2 |
+
text-align: center;
|
3 |
+
}
|
4 |
+
|
5 |
+
.App-logo {
|
6 |
+
height: 40vmin;
|
7 |
+
pointer-events: none;
|
8 |
+
}
|
9 |
+
|
10 |
+
@media (prefers-reduced-motion: no-preference) {
|
11 |
+
.App-logo {
|
12 |
+
animation: App-logo-spin infinite 20s linear;
|
13 |
+
}
|
14 |
+
}
|
15 |
+
|
16 |
+
.App-header {
|
17 |
+
background-color: #282c34;
|
18 |
+
min-height: 100vh;
|
19 |
+
display: flex;
|
20 |
+
flex-direction: column;
|
21 |
+
align-items: center;
|
22 |
+
justify-content: center;
|
23 |
+
font-size: calc(10px + 2vmin);
|
24 |
+
color: white;
|
25 |
+
}
|
26 |
+
|
27 |
+
.App-link {
|
28 |
+
color: #61dafb;
|
29 |
+
}
|
30 |
+
|
31 |
+
@keyframes App-logo-spin {
|
32 |
+
from {
|
33 |
+
transform: rotate(0deg);
|
34 |
+
}
|
35 |
+
to {
|
36 |
+
transform: rotate(360deg);
|
37 |
+
}
|
38 |
+
}
|
src/App.js
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Route, Switch } from 'wouter';
|
3 |
+
import { CssBaseline, Box } from '@mui/material';
|
4 |
+
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
5 |
+
import Home from './pages/Home';
|
6 |
+
import NotFound from './pages/not-found';
|
7 |
+
import Header from './components/Header';
|
8 |
+
import Footer from './components/Footer';
|
9 |
+
|
10 |
+
// Create a global theme with movie-themed colors
|
11 |
+
const theme = createTheme({
|
12 |
+
palette: {
|
13 |
+
primary: {
|
14 |
+
main: '#2C3E50', // Navy blue
|
15 |
+
contrastText: '#ffffff',
|
16 |
+
},
|
17 |
+
secondary: {
|
18 |
+
main: '#E74C3C', // Review red
|
19 |
+
contrastText: '#ffffff',
|
20 |
+
},
|
21 |
+
success: {
|
22 |
+
main: '#2ECC71', // Positive green
|
23 |
+
contrastText: '#ffffff',
|
24 |
+
},
|
25 |
+
warning: {
|
26 |
+
main: '#F1C40F', // Neutral yellow
|
27 |
+
contrastText: '#ffffff',
|
28 |
+
},
|
29 |
+
background: {
|
30 |
+
default: '#F8F9FA',
|
31 |
+
paper: '#ffffff',
|
32 |
+
},
|
33 |
+
},
|
34 |
+
typography: {
|
35 |
+
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
36 |
+
h1: {
|
37 |
+
fontSize: '2.5rem',
|
38 |
+
fontWeight: 700,
|
39 |
+
marginBottom: '1rem',
|
40 |
+
},
|
41 |
+
h2: {
|
42 |
+
fontSize: '1.75rem',
|
43 |
+
fontWeight: 600,
|
44 |
+
marginBottom: '0.75rem',
|
45 |
+
},
|
46 |
+
h3: {
|
47 |
+
fontSize: '1.5rem',
|
48 |
+
fontWeight: 600,
|
49 |
+
marginBottom: '0.5rem',
|
50 |
+
},
|
51 |
+
},
|
52 |
+
shape: {
|
53 |
+
borderRadius: 8,
|
54 |
+
},
|
55 |
+
spacing: 8,
|
56 |
+
});
|
57 |
+
|
58 |
+
function Router() {
|
59 |
+
return (
|
60 |
+
<Switch>
|
61 |
+
<Route path="/" component={Home} />
|
62 |
+
<Route component={NotFound} />
|
63 |
+
</Switch>
|
64 |
+
);
|
65 |
+
}
|
66 |
+
|
67 |
+
function App() {
|
68 |
+
return (
|
69 |
+
<ThemeProvider theme={theme}>
|
70 |
+
<CssBaseline />
|
71 |
+
<Box sx={{
|
72 |
+
display: 'flex',
|
73 |
+
flexDirection: 'column',
|
74 |
+
minHeight: '100vh'
|
75 |
+
}}>
|
76 |
+
<Header />
|
77 |
+
<Box sx={{ flex: 1 }}>
|
78 |
+
<Router />
|
79 |
+
</Box>
|
80 |
+
<Footer />
|
81 |
+
</Box>
|
82 |
+
</ThemeProvider>
|
83 |
+
);
|
84 |
+
}
|
85 |
+
|
86 |
+
export default App;
|
src/App.test.js
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { render, screen } from '@testing-library/react';
|
2 |
+
import App from './App';
|
3 |
+
|
4 |
+
test('renders learn react link', () => {
|
5 |
+
render(<App />);
|
6 |
+
const linkElement = screen.getByText(/learn react/i);
|
7 |
+
expect(linkElement).toBeInTheDocument();
|
8 |
+
});
|
src/components/AboutSection.js
ADDED
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Box, Typography, Paper, Chip } from '@mui/material';
|
3 |
+
import ThumbUpIcon from '@mui/icons-material/ThumbUp';
|
4 |
+
import ThumbDownIcon from '@mui/icons-material/ThumbDown';
|
5 |
+
import RemoveIcon from '@mui/icons-material/Remove';
|
6 |
+
import RateReviewIcon from '@mui/icons-material/RateReview';
|
7 |
+
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
8 |
+
|
9 |
+
function AboutSection() {
|
10 |
+
return (
|
11 |
+
<Paper
|
12 |
+
elevation={0}
|
13 |
+
sx={{
|
14 |
+
p: 3,
|
15 |
+
mb: 4,
|
16 |
+
borderRadius: 2,
|
17 |
+
bgcolor: 'background.paper',
|
18 |
+
border: '1px solid rgba(0,0,0,0.08)'
|
19 |
+
}}
|
20 |
+
>
|
21 |
+
<Typography variant="h4" component="h2" gutterBottom fontWeight="500">
|
22 |
+
How It Works
|
23 |
+
</Typography>
|
24 |
+
|
25 |
+
<Typography variant="body1" paragraph>
|
26 |
+
This tool uses artificial intelligence to analyze the sentiment of movie reviews.
|
27 |
+
Simply enter or paste a movie review, and our AI will classify it as positive, negative, or neutral,
|
28 |
+
along with a confidence score and detailed analysis.
|
29 |
+
</Typography>
|
30 |
+
|
31 |
+
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 3, mt: 1 }}>
|
32 |
+
<Box sx={{
|
33 |
+
display: 'flex',
|
34 |
+
flexDirection: 'column',
|
35 |
+
alignItems: 'center',
|
36 |
+
textAlign: 'center',
|
37 |
+
p: 2,
|
38 |
+
height: '100%',
|
39 |
+
bgcolor: '#f8f9fa',
|
40 |
+
borderRadius: 2,
|
41 |
+
flex: 1
|
42 |
+
}}>
|
43 |
+
<RateReviewIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
44 |
+
<Typography variant="h6" gutterBottom>
|
45 |
+
Enter Your Review
|
46 |
+
</Typography>
|
47 |
+
<Typography variant="body2" color="text.secondary">
|
48 |
+
Copy and paste a movie review or write your own in the text area.
|
49 |
+
</Typography>
|
50 |
+
</Box>
|
51 |
+
|
52 |
+
<Box sx={{
|
53 |
+
display: 'flex',
|
54 |
+
flexDirection: 'column',
|
55 |
+
alignItems: 'center',
|
56 |
+
textAlign: 'center',
|
57 |
+
p: 2,
|
58 |
+
height: '100%',
|
59 |
+
bgcolor: '#f8f9fa',
|
60 |
+
borderRadius: 2,
|
61 |
+
flex: 1
|
62 |
+
}}>
|
63 |
+
<AutoAwesomeIcon sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
|
64 |
+
<Typography variant="h6" gutterBottom>
|
65 |
+
AI Analysis
|
66 |
+
</Typography>
|
67 |
+
<Typography variant="body2" color="text.secondary">
|
68 |
+
Our AI model analyzes the text to determine the sentiment and confidence level.
|
69 |
+
</Typography>
|
70 |
+
</Box>
|
71 |
+
|
72 |
+
<Box sx={{
|
73 |
+
display: 'flex',
|
74 |
+
flexDirection: 'column',
|
75 |
+
alignItems: 'center',
|
76 |
+
textAlign: 'center',
|
77 |
+
p: 2,
|
78 |
+
height: '100%',
|
79 |
+
bgcolor: '#f8f9fa',
|
80 |
+
borderRadius: 2,
|
81 |
+
flex: 1
|
82 |
+
}}>
|
83 |
+
<Box sx={{ display: 'flex', gap: 1, mb: 1 }}>
|
84 |
+
<Chip
|
85 |
+
icon={<ThumbUpIcon />}
|
86 |
+
label="Positive"
|
87 |
+
size="small"
|
88 |
+
sx={{ bgcolor: 'success.main', color: 'white' }}
|
89 |
+
/>
|
90 |
+
<Chip
|
91 |
+
icon={<ThumbDownIcon />}
|
92 |
+
label="Negative"
|
93 |
+
size="small"
|
94 |
+
sx={{ bgcolor: 'secondary.main', color: 'white' }}
|
95 |
+
/>
|
96 |
+
<Chip
|
97 |
+
icon={<RemoveIcon />}
|
98 |
+
label="Neutral"
|
99 |
+
size="small"
|
100 |
+
sx={{ bgcolor: 'warning.main', color: 'white' }}
|
101 |
+
/>
|
102 |
+
</Box>
|
103 |
+
<Typography variant="h6" gutterBottom>
|
104 |
+
View Results
|
105 |
+
</Typography>
|
106 |
+
<Typography variant="body2" color="text.secondary">
|
107 |
+
See the sentiment classification, confidence score, and detailed reasoning.
|
108 |
+
</Typography>
|
109 |
+
</Box>
|
110 |
+
</Box>
|
111 |
+
|
112 |
+
<Typography variant="body2" color="text.secondary" sx={{ mt: 3, fontStyle: 'italic' }}>
|
113 |
+
This application uses Cohere's large language model API to perform sentiment analysis.
|
114 |
+
</Typography>
|
115 |
+
</Paper>
|
116 |
+
);
|
117 |
+
}
|
118 |
+
|
119 |
+
export default AboutSection;
|
src/components/Footer.js
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Box, Typography, Container, Link, Divider } from '@mui/material';
|
3 |
+
|
4 |
+
function Footer() {
|
5 |
+
return (
|
6 |
+
<Box
|
7 |
+
component="footer"
|
8 |
+
sx={{
|
9 |
+
py: 3,
|
10 |
+
mt: 4,
|
11 |
+
bgcolor: '#f5f5f5'
|
12 |
+
}}
|
13 |
+
>
|
14 |
+
<Divider />
|
15 |
+
<Container maxWidth="lg">
|
16 |
+
<Box sx={{
|
17 |
+
display: 'flex',
|
18 |
+
flexDirection: { xs: 'column', md: 'row' },
|
19 |
+
justifyContent: 'space-between',
|
20 |
+
alignItems: 'center',
|
21 |
+
textAlign: { xs: 'center', md: 'left' },
|
22 |
+
mt: 2
|
23 |
+
}}>
|
24 |
+
<Typography variant="body2" color="text.secondary">
|
25 |
+
© {new Date().getFullYear()} Movie Review Sentiment Analysis
|
26 |
+
</Typography>
|
27 |
+
|
28 |
+
<Box sx={{
|
29 |
+
display: 'flex',
|
30 |
+
gap: 2,
|
31 |
+
mt: { xs: 2, md: 0 }
|
32 |
+
}}>
|
33 |
+
<Link
|
34 |
+
href="https://cohere.com/"
|
35 |
+
target="_blank"
|
36 |
+
rel="noopener noreferrer"
|
37 |
+
color="inherit"
|
38 |
+
underline="hover"
|
39 |
+
>
|
40 |
+
Powered by Cohere AI
|
41 |
+
</Link>
|
42 |
+
<Link
|
43 |
+
href="#"
|
44 |
+
color="inherit"
|
45 |
+
underline="hover"
|
46 |
+
>
|
47 |
+
About
|
48 |
+
</Link>
|
49 |
+
<Link
|
50 |
+
href="#"
|
51 |
+
color="inherit"
|
52 |
+
underline="hover"
|
53 |
+
>
|
54 |
+
Terms
|
55 |
+
</Link>
|
56 |
+
<Link
|
57 |
+
href="#"
|
58 |
+
color="inherit"
|
59 |
+
underline="hover"
|
60 |
+
>
|
61 |
+
Privacy
|
62 |
+
</Link>
|
63 |
+
</Box>
|
64 |
+
</Box>
|
65 |
+
</Container>
|
66 |
+
</Box>
|
67 |
+
);
|
68 |
+
}
|
69 |
+
|
70 |
+
export default Footer;
|
src/components/Header.js
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { AppBar, Toolbar, Typography, Box, Container } from '@mui/material';
|
3 |
+
import MovieIcon from '@mui/icons-material/Movie';
|
4 |
+
import { Link } from 'wouter';
|
5 |
+
|
6 |
+
function Header() {
|
7 |
+
return (
|
8 |
+
<AppBar position="static" sx={{ backgroundColor: '#2C3E50', boxShadow: 2, mb: 2 }}>
|
9 |
+
<Container maxWidth="lg">
|
10 |
+
<Toolbar sx={{ justifyContent: 'space-between' }}>
|
11 |
+
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
12 |
+
<MovieIcon sx={{ mr: 1, fontSize: 28 }} />
|
13 |
+
<Typography
|
14 |
+
variant="h6"
|
15 |
+
component={Link}
|
16 |
+
to="/"
|
17 |
+
sx={{
|
18 |
+
textDecoration: 'none',
|
19 |
+
color: 'white',
|
20 |
+
fontWeight: 'bold',
|
21 |
+
cursor: 'pointer'
|
22 |
+
}}
|
23 |
+
>
|
24 |
+
Movie Review Analyzer
|
25 |
+
</Typography>
|
26 |
+
</Box>
|
27 |
+
</Toolbar>
|
28 |
+
</Container>
|
29 |
+
</AppBar>
|
30 |
+
);
|
31 |
+
}
|
32 |
+
|
33 |
+
export default Header;
|
src/components/PreviousAnalysis.js
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import {
|
3 |
+
Box,
|
4 |
+
Typography,
|
5 |
+
List,
|
6 |
+
ListItem,
|
7 |
+
ListItemButton,
|
8 |
+
ListItemText,
|
9 |
+
ListItemIcon,
|
10 |
+
Chip,
|
11 |
+
Divider
|
12 |
+
} from '@mui/material';
|
13 |
+
import ThumbUpIcon from '@mui/icons-material/ThumbUp';
|
14 |
+
import ThumbDownIcon from '@mui/icons-material/ThumbDown';
|
15 |
+
import RemoveIcon from '@mui/icons-material/Remove';
|
16 |
+
import HistoryIcon from '@mui/icons-material/History';
|
17 |
+
|
18 |
+
function PreviousAnalyses({ analyses, onViewAnalysis }) {
|
19 |
+
if (!analyses.length) {
|
20 |
+
return (
|
21 |
+
<Box sx={{ textAlign: 'center', py: 4 }}>
|
22 |
+
<HistoryIcon sx={{ fontSize: 40, color: 'text.secondary', mb: 2 }} />
|
23 |
+
<Typography variant="body1" color="textSecondary">
|
24 |
+
Previous analyses will appear here
|
25 |
+
</Typography>
|
26 |
+
<Typography variant="body2" color="textSecondary">
|
27 |
+
Analyze your first review to get started
|
28 |
+
</Typography>
|
29 |
+
</Box>
|
30 |
+
);
|
31 |
+
}
|
32 |
+
|
33 |
+
return (
|
34 |
+
<List sx={{ width: '100%', maxHeight: '400px', overflow: 'auto' }}>
|
35 |
+
{analyses.map((analysis, index) => {
|
36 |
+
const getSentimentIcon = (sentiment) => {
|
37 |
+
switch (sentiment) {
|
38 |
+
case 'positive': return <ThumbUpIcon sx={{ color: 'success.main' }} />;
|
39 |
+
case 'negative': return <ThumbDownIcon sx={{ color: 'secondary.main' }} />;
|
40 |
+
default: return <RemoveIcon sx={{ color: 'warning.main' }} />;
|
41 |
+
}
|
42 |
+
};
|
43 |
+
|
44 |
+
const truncateText = (text, maxLength = 50) => {
|
45 |
+
return text.length > maxLength
|
46 |
+
? `${text.substring(0, maxLength)}...`
|
47 |
+
: text;
|
48 |
+
};
|
49 |
+
console.log(analysis)
|
50 |
+
|
51 |
+
return (
|
52 |
+
<React.Fragment key={analysis.id || index}>
|
53 |
+
<ListItem disablePadding sx={{ mb: 1 }}>
|
54 |
+
<ListItemButton
|
55 |
+
sx={{
|
56 |
+
border: '1px solid',
|
57 |
+
borderColor: 'divider',
|
58 |
+
borderRadius: 1,
|
59 |
+
"&:hover": {
|
60 |
+
bgcolor: 'action.hover',
|
61 |
+
}
|
62 |
+
}}
|
63 |
+
>
|
64 |
+
<ListItemIcon>
|
65 |
+
{getSentimentIcon(analysis.sentiment)}
|
66 |
+
</ListItemIcon>
|
67 |
+
<ListItemText
|
68 |
+
primary={truncateText(analysis.text)}
|
69 |
+
secondary={
|
70 |
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
|
71 |
+
<Chip
|
72 |
+
label={`${analysis.sentiment}`}
|
73 |
+
size="small"
|
74 |
+
sx={{
|
75 |
+
bgcolor: analysis.sentiment === 'positive' ? 'success.light' :
|
76 |
+
analysis.sentiment === 'negative' ? 'secondary.light' :
|
77 |
+
'warning.light',
|
78 |
+
color: '#fff',
|
79 |
+
fontWeight: 'medium',
|
80 |
+
fontSize: '0.7rem'
|
81 |
+
}}
|
82 |
+
/>
|
83 |
+
<Typography variant="caption">
|
84 |
+
{analysis.createdAt ? new Date(analysis.createdAt).toLocaleTimeString() : 'Just now'}
|
85 |
+
</Typography>
|
86 |
+
</Box>
|
87 |
+
}
|
88 |
+
/>
|
89 |
+
</ListItemButton>
|
90 |
+
</ListItem>
|
91 |
+
{index < analyses.length - 1 && <Divider variant="fullWidth" component="li" />}
|
92 |
+
</React.Fragment>
|
93 |
+
);
|
94 |
+
})}
|
95 |
+
</List>
|
96 |
+
);
|
97 |
+
}
|
98 |
+
|
99 |
+
export default PreviousAnalyses;
|
src/components/ReviewInput.js
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Box, TextField, Button, Typography, CircularProgress } from '@mui/material';
|
3 |
+
import RateReviewIcon from '@mui/icons-material/RateReview';
|
4 |
+
import { analyzeSentiment } from '../lib/api';
|
5 |
+
|
6 |
+
function ReviewInput({
|
7 |
+
currentReview,
|
8 |
+
setCurrentReview,
|
9 |
+
isLoading,
|
10 |
+
setIsLoading,
|
11 |
+
setHasError,
|
12 |
+
setErrorMessage,
|
13 |
+
setAnalysisResult,
|
14 |
+
addToPreviousAnalyses
|
15 |
+
}) {
|
16 |
+
const handleReviewChange = (e) => {
|
17 |
+
setCurrentReview(e.target.value);
|
18 |
+
};
|
19 |
+
|
20 |
+
const handleSubmit = async (e) => {
|
21 |
+
e.preventDefault();
|
22 |
+
|
23 |
+
if (!currentReview) {
|
24 |
+
setHasError(true);
|
25 |
+
setErrorMessage('Please enter a review to analyze.');
|
26 |
+
return;
|
27 |
+
}
|
28 |
+
|
29 |
+
setIsLoading(true);
|
30 |
+
setHasError(false);
|
31 |
+
setErrorMessage('');
|
32 |
+
|
33 |
+
try {
|
34 |
+
const result = await analyzeSentiment(currentReview);
|
35 |
+
setAnalysisResult(result);
|
36 |
+
addToPreviousAnalyses({ ...result, text: currentReview });
|
37 |
+
} catch (error) {
|
38 |
+
console.error('Error analyzing sentiment:', error);
|
39 |
+
setHasError(true);
|
40 |
+
setErrorMessage(
|
41 |
+
error.message || 'An error occurred while analyzing the review. Please try again.'
|
42 |
+
);
|
43 |
+
} finally {
|
44 |
+
setIsLoading(false);
|
45 |
+
}
|
46 |
+
};
|
47 |
+
|
48 |
+
return (
|
49 |
+
<Box component="form" onSubmit={handleSubmit} noValidate>
|
50 |
+
<Typography variant="h2" component="h2" gutterBottom>
|
51 |
+
Enter Movie Review
|
52 |
+
</Typography>
|
53 |
+
|
54 |
+
<TextField
|
55 |
+
fullWidth
|
56 |
+
variant="outlined"
|
57 |
+
multiline
|
58 |
+
rows={6}
|
59 |
+
placeholder="Type or paste a movie review here..."
|
60 |
+
value={currentReview}
|
61 |
+
onChange={handleReviewChange}
|
62 |
+
sx={{ mb: 2 }}
|
63 |
+
disabled={isLoading}
|
64 |
+
/>
|
65 |
+
|
66 |
+
<Button
|
67 |
+
variant="contained"
|
68 |
+
color="primary"
|
69 |
+
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : <RateReviewIcon />}
|
70 |
+
type="submit"
|
71 |
+
disabled={isLoading || !currentReview}
|
72 |
+
sx={{
|
73 |
+
px: 4,
|
74 |
+
py: 1.5,
|
75 |
+
fontSize: '1rem',
|
76 |
+
fontWeight: 'medium'
|
77 |
+
}}
|
78 |
+
>
|
79 |
+
{isLoading ? 'Analyzing...' : 'Analyze Sentiment'}
|
80 |
+
</Button>
|
81 |
+
</Box>
|
82 |
+
);
|
83 |
+
}
|
84 |
+
|
85 |
+
export default ReviewInput;
|
src/components/SentimentResult.js
ADDED
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Box, Typography, Button, Paper, Chip, CircularProgress } from '@mui/material';
|
3 |
+
import ThumbUpIcon from '@mui/icons-material/ThumbUp';
|
4 |
+
import ThumbDownIcon from '@mui/icons-material/ThumbDown';
|
5 |
+
import RemoveIcon from '@mui/icons-material/Remove';
|
6 |
+
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
7 |
+
|
8 |
+
function SentimentResult({ result, onNewAnalysis, currentReview }) {
|
9 |
+
const sentimentColor =
|
10 |
+
result.sentiment === 'positive' ? 'success.main' :
|
11 |
+
result.sentiment === 'negative' ? 'secondary.main' :
|
12 |
+
'warning.main';
|
13 |
+
|
14 |
+
const SentimentIcon =
|
15 |
+
result.sentiment === 'positive' ? ThumbUpIcon :
|
16 |
+
result.sentiment === 'negative' ? ThumbDownIcon :
|
17 |
+
RemoveIcon;
|
18 |
+
|
19 |
+
return (
|
20 |
+
<Box>
|
21 |
+
<Typography variant="h2" component="h2" gutterBottom>
|
22 |
+
Sentiment Analysis Result
|
23 |
+
</Typography>
|
24 |
+
|
25 |
+
<Box sx={{
|
26 |
+
display: 'flex',
|
27 |
+
flexDirection: { xs: 'column', sm: 'row' },
|
28 |
+
alignItems: 'center',
|
29 |
+
gap: 2,
|
30 |
+
mb: 3
|
31 |
+
}}>
|
32 |
+
<Box sx={{
|
33 |
+
display: 'flex',
|
34 |
+
alignItems: 'center',
|
35 |
+
gap: 1
|
36 |
+
}}>
|
37 |
+
<Typography variant="h3" component="span">
|
38 |
+
Sentiment:
|
39 |
+
</Typography>
|
40 |
+
<Chip
|
41 |
+
icon={<SentimentIcon />}
|
42 |
+
label={result.sentiment.charAt(0).toUpperCase() + result.sentiment.slice(1)}
|
43 |
+
sx={{
|
44 |
+
bgcolor: sentimentColor,
|
45 |
+
color: 'white',
|
46 |
+
fontWeight: 'bold',
|
47 |
+
fontSize: '1rem',
|
48 |
+
px: 1,
|
49 |
+
py: 2.5
|
50 |
+
}}
|
51 |
+
/>
|
52 |
+
</Box>
|
53 |
+
|
54 |
+
<Box sx={{
|
55 |
+
display: 'flex',
|
56 |
+
alignItems: 'center',
|
57 |
+
gap: 1
|
58 |
+
}}>
|
59 |
+
<Typography variant="h3" component="span">
|
60 |
+
Confidence:
|
61 |
+
</Typography>
|
62 |
+
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
63 |
+
<CircularProgress
|
64 |
+
variant="determinate"
|
65 |
+
value={result.confidence}
|
66 |
+
size={60}
|
67 |
+
thickness={5}
|
68 |
+
sx={{ color: sentimentColor }}
|
69 |
+
/>
|
70 |
+
<Box
|
71 |
+
sx={{
|
72 |
+
top: 0,
|
73 |
+
left: 0,
|
74 |
+
bottom: 0,
|
75 |
+
right: 0,
|
76 |
+
position: 'absolute',
|
77 |
+
display: 'flex',
|
78 |
+
alignItems: 'center',
|
79 |
+
justifyContent: 'center',
|
80 |
+
}}
|
81 |
+
>
|
82 |
+
<Typography variant="body1" component="div" fontWeight="bold">
|
83 |
+
{`${Math.round(result.confidence)}%`}
|
84 |
+
</Typography>
|
85 |
+
</Box>
|
86 |
+
</Box>
|
87 |
+
</Box>
|
88 |
+
</Box>
|
89 |
+
|
90 |
+
{result.reasoning && (
|
91 |
+
<Paper variant="outlined" sx={{ p: 2, mb: 3, bgcolor: 'background.default' }}>
|
92 |
+
<Typography variant="h6" gutterBottom>
|
93 |
+
Analysis Details:
|
94 |
+
</Typography>
|
95 |
+
<Typography variant="body1">
|
96 |
+
{result.reasoning}
|
97 |
+
</Typography>
|
98 |
+
</Paper>
|
99 |
+
)}
|
100 |
+
|
101 |
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
102 |
+
<Paper variant="outlined" sx={{ p: 2, flex: 1, mr: 2 }}>
|
103 |
+
<Typography variant="body2" color="text.secondary">
|
104 |
+
Original Review Text:
|
105 |
+
</Typography>
|
106 |
+
<Typography variant="body1" sx={{ mt: 1 }}>
|
107 |
+
{currentReview}
|
108 |
+
</Typography>
|
109 |
+
</Paper>
|
110 |
+
|
111 |
+
<Button
|
112 |
+
variant="outlined"
|
113 |
+
color="primary"
|
114 |
+
startIcon={<ArrowBackIcon />}
|
115 |
+
onClick={onNewAnalysis}
|
116 |
+
>
|
117 |
+
New Analysis
|
118 |
+
</Button>
|
119 |
+
</Box>
|
120 |
+
</Box>
|
121 |
+
);
|
122 |
+
}
|
123 |
+
|
124 |
+
export default SentimentResult;
|
src/index.css
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
margin: 0;
|
3 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
4 |
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
5 |
+
sans-serif;
|
6 |
+
-webkit-font-smoothing: antialiased;
|
7 |
+
-moz-osx-font-smoothing: grayscale;
|
8 |
+
}
|
9 |
+
|
10 |
+
code {
|
11 |
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
12 |
+
monospace;
|
13 |
+
}
|
src/index.js
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import ReactDOM from 'react-dom/client';
|
3 |
+
import './index.css';
|
4 |
+
import App from './App';
|
5 |
+
import reportWebVitals from './reportWebVitals';
|
6 |
+
|
7 |
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
8 |
+
root.render(
|
9 |
+
<React.StrictMode>
|
10 |
+
<App />
|
11 |
+
</React.StrictMode>
|
12 |
+
);
|
13 |
+
|
14 |
+
// If you want to start measuring performance in your app, pass a function
|
15 |
+
// to log results (for example: reportWebVitals(console.log))
|
16 |
+
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
17 |
+
reportWebVitals();
|
src/lib/api.js
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Makes a request to the sentiment analysis API endpoint
|
3 |
+
* @param {string} reviewText - The review text to analyze
|
4 |
+
* @returns {Promise<Object>} - The sentiment analysis result
|
5 |
+
*/
|
6 |
+
export async function analyzeSentiment(reviewText) {
|
7 |
+
try {
|
8 |
+
const response = await fetch(
|
9 |
+
'https://chethanmsrit-movie-sentiment-analysis.hf.space/sentiment', {
|
10 |
+
method: 'POST',
|
11 |
+
headers: {
|
12 |
+
'Content-Type': 'application/json',
|
13 |
+
},
|
14 |
+
body: JSON.stringify({ reviewText }),
|
15 |
+
});
|
16 |
+
|
17 |
+
if (!response.ok) {
|
18 |
+
const errorData = await response.json();
|
19 |
+
throw new Error(errorData.message || 'Failed to analyze sentiment');
|
20 |
+
}
|
21 |
+
|
22 |
+
return await response.json();
|
23 |
+
} catch (error) {
|
24 |
+
console.error('Error analyzing sentiment:', error);
|
25 |
+
throw error;
|
26 |
+
}
|
27 |
+
}
|
src/logo.svg
ADDED
|
src/pages/Home.js
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
+
import { Container, Typography, Box, Paper, Divider } from '@mui/material';
|
3 |
+
import ReviewInput from '../components/ReviewInput';
|
4 |
+
import SentimentResult from '../components/SentimentResult';
|
5 |
+
import PreviousAnalyses from '../components/PreviousAnalysis';
|
6 |
+
import AboutSection from '../components/AboutSection';
|
7 |
+
|
8 |
+
export default function Home() {
|
9 |
+
const [currentReview, setCurrentReview] = useState('');
|
10 |
+
const [isLoading, setIsLoading] = useState(false);
|
11 |
+
const [hasError, setHasError] = useState(false);
|
12 |
+
const [errorMessage, setErrorMessage] = useState('');
|
13 |
+
const [analysisResult, setAnalysisResult] = useState(null);
|
14 |
+
const [previousAnalyses, setPreviousAnalyses] = useState([]);
|
15 |
+
|
16 |
+
const handleViewPreviousAnalysis = (analysis) => {
|
17 |
+
setAnalysisResult(null);
|
18 |
+
setCurrentReview(analysis);
|
19 |
+
};
|
20 |
+
|
21 |
+
const addToPreviousAnalyses = (analysis) => {
|
22 |
+
// Add unique ID if it doesn't exist
|
23 |
+
const analysisWithId = analysis.id
|
24 |
+
? analysis
|
25 |
+
: { ...analysis, id: Date.now(), createdAt: new Date() };
|
26 |
+
|
27 |
+
// Add to the beginning of the array to show newest first
|
28 |
+
setPreviousAnalyses(prev => [analysisWithId, ...prev]);
|
29 |
+
};
|
30 |
+
|
31 |
+
return (
|
32 |
+
<Container maxWidth="lg" sx={{ py: 4 }}>
|
33 |
+
<Box mb={4} textAlign="center">
|
34 |
+
<Typography variant="h1" component="h1" gutterBottom>
|
35 |
+
Movie Review Sentiment Analysis
|
36 |
+
</Typography>
|
37 |
+
<Typography variant="h6" color="textSecondary">
|
38 |
+
Enter a movie review to analyze its sentiment using AI
|
39 |
+
</Typography>
|
40 |
+
</Box>
|
41 |
+
|
42 |
+
<AboutSection />
|
43 |
+
|
44 |
+
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 3 }}>
|
45 |
+
<Box sx={{ flex: { xs: '1', md: '2' } }}>
|
46 |
+
<Paper elevation={2} sx={{ p: 3, mb: { xs: 3, md: 0 } }}>
|
47 |
+
{!analysisResult ? (
|
48 |
+
<ReviewInput
|
49 |
+
currentReview={currentReview}
|
50 |
+
setCurrentReview={setCurrentReview}
|
51 |
+
isLoading={isLoading}
|
52 |
+
setIsLoading={setIsLoading}
|
53 |
+
setHasError={setHasError}
|
54 |
+
setErrorMessage={setErrorMessage}
|
55 |
+
setAnalysisResult={setAnalysisResult}
|
56 |
+
addToPreviousAnalyses={addToPreviousAnalyses}
|
57 |
+
/>
|
58 |
+
) : (
|
59 |
+
<SentimentResult
|
60 |
+
result={analysisResult}
|
61 |
+
onNewAnalysis={() => setAnalysisResult(null)}
|
62 |
+
currentReview={currentReview}
|
63 |
+
/>
|
64 |
+
)}
|
65 |
+
|
66 |
+
{hasError && (
|
67 |
+
<Box sx={{ mt: 2, p: 2, bgcolor: 'error.light', color: 'error.contrastText', borderRadius: 1 }}>
|
68 |
+
<Typography variant="body1">
|
69 |
+
{errorMessage || 'An error occurred while analyzing the review. Please try again.'}
|
70 |
+
</Typography>
|
71 |
+
</Box>
|
72 |
+
)}
|
73 |
+
</Paper>
|
74 |
+
</Box>
|
75 |
+
|
76 |
+
<Box sx={{ flex: { xs: '1', md: '1' } }}>
|
77 |
+
<Paper elevation={2} sx={{ p: 3, mb: { xs: 3, md: 0 } }}>
|
78 |
+
<Typography variant="h2" component="h2" gutterBottom>
|
79 |
+
Previous Analyses
|
80 |
+
</Typography>
|
81 |
+
<Divider sx={{ mb: 2 }} />
|
82 |
+
<PreviousAnalyses
|
83 |
+
analyses={previousAnalyses}
|
84 |
+
onViewAnalysis={handleViewPreviousAnalysis}
|
85 |
+
/>
|
86 |
+
</Paper>
|
87 |
+
</Box>
|
88 |
+
</Box>
|
89 |
+
</Container>
|
90 |
+
);
|
91 |
+
}
|
src/pages/not-found.js
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Box, Typography, Button, Container } from '@mui/material';
|
3 |
+
import { Link } from 'wouter';
|
4 |
+
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
5 |
+
import HomeIcon from '@mui/icons-material/Home';
|
6 |
+
|
7 |
+
export default function NotFound() {
|
8 |
+
return (
|
9 |
+
<Container maxWidth="md">
|
10 |
+
<Box
|
11 |
+
sx={{
|
12 |
+
display: 'flex',
|
13 |
+
flexDirection: 'column',
|
14 |
+
alignItems: 'center',
|
15 |
+
justifyContent: 'center',
|
16 |
+
minHeight: '100vh',
|
17 |
+
textAlign: 'center',
|
18 |
+
py: 4
|
19 |
+
}}
|
20 |
+
>
|
21 |
+
<ErrorOutlineIcon sx={{ fontSize: 100, color: 'error.main', mb: 2 }} />
|
22 |
+
|
23 |
+
<Typography variant="h2" component="h1" gutterBottom>
|
24 |
+
Page Not Found
|
25 |
+
</Typography>
|
26 |
+
|
27 |
+
<Typography variant="h5" color="textSecondary" paragraph>
|
28 |
+
The page you're looking for doesn't exist or has been moved.
|
29 |
+
</Typography>
|
30 |
+
|
31 |
+
<Button
|
32 |
+
component={Link}
|
33 |
+
to="/"
|
34 |
+
variant="contained"
|
35 |
+
color="primary"
|
36 |
+
startIcon={<HomeIcon />}
|
37 |
+
sx={{ mt: 3 }}
|
38 |
+
>
|
39 |
+
Go to Home
|
40 |
+
</Button>
|
41 |
+
</Box>
|
42 |
+
</Container>
|
43 |
+
);
|
44 |
+
}
|
src/reportWebVitals.js
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const reportWebVitals = onPerfEntry => {
|
2 |
+
if (onPerfEntry && onPerfEntry instanceof Function) {
|
3 |
+
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
4 |
+
getCLS(onPerfEntry);
|
5 |
+
getFID(onPerfEntry);
|
6 |
+
getFCP(onPerfEntry);
|
7 |
+
getLCP(onPerfEntry);
|
8 |
+
getTTFB(onPerfEntry);
|
9 |
+
});
|
10 |
+
}
|
11 |
+
};
|
12 |
+
|
13 |
+
export default reportWebVitals;
|
src/setupTests.js
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
2 |
+
// allows you to do things like:
|
3 |
+
// expect(element).toHaveTextContent(/react/i)
|
4 |
+
// learn more: https://github.com/testing-library/jest-dom
|
5 |
+
import '@testing-library/jest-dom';
|