Spaces:
Running
feat: Implement MainLayout component for consistent layout structure
Browse filesfeat: Add FeedbackForm component for user feedback submission
feat: Create SearchResults component to display search results with feedback option
feat: Introduce ErrorBoundary component for handling errors gracefully
feat: Develop ErrorMessage component for displaying error alerts
feat: Add LoadingSpinner component for loading state indication
feat: Implement ThemeToggle component for switching between light and dark themes
feat: Create useAuth hook for managing authentication state and token handling
feat: Add useFeedback hook for managing feedback submission state
feat: Implement useLocalStorage hook for persistent state management
feat: Create useSearch hook for handling search queries and results
feat: Develop useTheme hook for managing theme state and preferences
chore: Set up index.js for application entry point with error boundary and theme providers
feat: Create About page to provide information about the project
feat: Develop Dashboard page for authenticated user interactions
feat: Implement Home page with call-to-action for users
feat: Create auth service for handling login, logout, and token refresh
feat: Develop search service for querying and saving feedback
style: Add global styles for consistent theming and animations
style: Implement animations for smooth transitions and effects
style: Add custom fonts for enhanced typography
style: Create spiritual styles for specific UI components
- package-lock.json +3 -0
- package.json +3 -40
- public/index.html +2 -1
- src/App.js +51 -18
- src/components/Auth/Login.jsx +92 -0
- src/components/Auth/Logout.jsx +32 -0
- src/components/Auth/PrivateRoute.jsx +15 -0
- src/components/Auth/TokenHandler.jsx +34 -0
- src/components/Layout/Footer.jsx +73 -0
- src/components/Layout/Header.js +0 -80
- src/components/Layout/Header.jsx +104 -0
- src/components/Layout/MainLayout.jsx +27 -0
- src/components/Search/FeedbackForm.jsx +151 -0
- src/components/Search/SearchForm.jsx +75 -71
- src/components/Search/SearchResults.jsx +111 -0
- src/components/SearchResults.jsx +0 -81
- src/components/UI/ErrorBoundary.jsx +55 -0
- src/components/UI/ErrorMessage.jsx +66 -0
- src/components/UI/LoadingSpinner.jsx +43 -0
- src/components/UI/ThemeToggle.jsx +29 -0
- src/hooks/useAuth.js +120 -0
- src/hooks/useFeedback.js +39 -0
- src/hooks/useLocalStorage.js +26 -0
- src/hooks/useSearch.js +48 -0
- src/hooks/useTheme.js +57 -0
- src/index.js +27 -0
- src/pages/About.jsx +70 -0
- src/pages/Dashboard.jsx +75 -0
- src/pages/Home.jsx +68 -0
- src/services/api.js +28 -22
- src/services/auth.js +77 -0
- src/services/search.js +27 -0
- src/styles/GlobalStyles.js +69 -0
- src/styles/animations.js +56 -0
- src/styles/fonts.css +28 -0
- src/styles/spiritualStyles.js +73 -0
- src/styles/theme.js +25 -64
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:d5990b52b6a2c9a5cd8fe71c63b5f1eac65f45b6f8b2d2c8517180df35851d80
|
3 |
+
size 676451
|
@@ -1,40 +1,3 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
"private": true,
|
5 |
-
"dependencies": {
|
6 |
-
"@testing-library/jest-dom": "^5.16.5",
|
7 |
-
"@testing-library/react": "^13.4.0",
|
8 |
-
"@testing-library/user-event": "^13.5.0",
|
9 |
-
"axios": "^1.3.4",
|
10 |
-
"react": "^18.2.0",
|
11 |
-
"react-dom": "^18.2.0",
|
12 |
-
"react-router-dom": "^6.8.1",
|
13 |
-
"styled-components": "^5.3.6",
|
14 |
-
"web-vitals": "^2.1.4"
|
15 |
-
},
|
16 |
-
"scripts": {
|
17 |
-
"start": "react-scripts start",
|
18 |
-
"build": "react-scripts build",
|
19 |
-
"test": "react-scripts test",
|
20 |
-
"eject": "react-scripts eject"
|
21 |
-
},
|
22 |
-
"eslintConfig": {
|
23 |
-
"extends": [
|
24 |
-
"react-app",
|
25 |
-
"react-app/jest"
|
26 |
-
]
|
27 |
-
},
|
28 |
-
"browserslist": {
|
29 |
-
"production": [
|
30 |
-
">0.2%",
|
31 |
-
"not dead",
|
32 |
-
"not op_mini all"
|
33 |
-
],
|
34 |
-
"development": [
|
35 |
-
"last 1 chrome version",
|
36 |
-
"last 1 firefox version",
|
37 |
-
"last 1 safari version"
|
38 |
-
]
|
39 |
-
}
|
40 |
-
}
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:e608424869e0ed703e601a85028efaa3b2d1b05d0211fda96887bff1dd8feee8
|
3 |
+
size 863
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -18,7 +18,8 @@
|
|
18 |
|
19 |
<!-- Google Fonts -->
|
20 |
<link href="https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&family=Open+Sans:wght@400;500;600&display=swap" rel="stylesheet">
|
21 |
-
|
|
|
22 |
<title>EnlightenQalb | Al-Ghazali Wisdom Search</title>
|
23 |
</head>
|
24 |
<body>
|
|
|
18 |
|
19 |
<!-- Google Fonts -->
|
20 |
<link href="https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&family=Open+Sans:wght@400;500;600&display=swap" rel="stylesheet">
|
21 |
+
<! -- Poppins Font -->
|
22 |
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;700&display=swap" rel="stylesheet">
|
23 |
<title>EnlightenQalb | Al-Ghazali Wisdom Search</title>
|
24 |
</head>
|
25 |
<body>
|
@@ -1,30 +1,63 @@
|
|
1 |
-
import React from 'react';
|
2 |
-
import {
|
3 |
-
import {
|
4 |
-
import { AuthProvider } from './context/AuthContext';
|
5 |
import MainLayout from './components/Layout/MainLayout';
|
6 |
import Home from './pages/Home';
|
7 |
import Dashboard from './pages/Dashboard';
|
8 |
import About from './pages/About';
|
9 |
-
import
|
10 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
|
12 |
function App() {
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
return (
|
14 |
-
|
15 |
-
<
|
16 |
<AuthProvider>
|
17 |
-
<
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
</AuthProvider>
|
27 |
-
|
28 |
);
|
29 |
}
|
30 |
|
|
|
1 |
+
import React, { useEffect } from 'react';
|
2 |
+
import { Routes, Route, useLocation } from 'react-router-dom'; // Remove BrowserRouter import
|
3 |
+
import { AuthProvider } from './hooks/useAuth';
|
|
|
4 |
import MainLayout from './components/Layout/MainLayout';
|
5 |
import Home from './pages/Home';
|
6 |
import Dashboard from './pages/Dashboard';
|
7 |
import About from './pages/About';
|
8 |
+
import Login from './components/Auth/Login';
|
9 |
+
import Logout from './components/Auth/Logout';
|
10 |
+
import TokenHandler from './components/Auth/TokenHandler';
|
11 |
+
import PrivateRoute from './components/Auth/PrivateRoute';
|
12 |
+
import useTheme from './hooks/useTheme';
|
13 |
+
import LoadingSpinner from './components/UI/LoadingSpinner';
|
14 |
+
import { CssBaseline } from '@mui/material';
|
15 |
+
|
16 |
+
const ScrollToTop = () => {
|
17 |
+
const { pathname } = useLocation();
|
18 |
+
|
19 |
+
useEffect(() => {
|
20 |
+
window.scrollTo({
|
21 |
+
top: 0,
|
22 |
+
behavior: 'smooth',
|
23 |
+
});
|
24 |
+
}, [pathname]);
|
25 |
+
|
26 |
+
return null;
|
27 |
+
};
|
28 |
|
29 |
function App() {
|
30 |
+
const { themeLoading } = useTheme();
|
31 |
+
|
32 |
+
if (themeLoading) {
|
33 |
+
return <LoadingSpinner fullScreen />;
|
34 |
+
}
|
35 |
+
|
36 |
return (
|
37 |
+
<>
|
38 |
+
<CssBaseline />
|
39 |
<AuthProvider>
|
40 |
+
<ScrollToTop />
|
41 |
+
<MainLayout>
|
42 |
+
<Routes>
|
43 |
+
<Route path="/" element={<Home />} />
|
44 |
+
<Route path="/login" element={<Login />} />
|
45 |
+
<Route path="/logout" element={<Logout />} />
|
46 |
+
<Route path="/token-refresh" element={<TokenHandler />} />
|
47 |
+
<Route path="/about" element={<About />} />
|
48 |
+
<Route
|
49 |
+
path="/dashboard"
|
50 |
+
element={
|
51 |
+
<PrivateRoute>
|
52 |
+
<Dashboard />
|
53 |
+
</PrivateRoute>
|
54 |
+
}
|
55 |
+
/>
|
56 |
+
<Route path="*" element={<Home />} />
|
57 |
+
</Routes>
|
58 |
+
</MainLayout>
|
59 |
</AuthProvider>
|
60 |
+
</>
|
61 |
);
|
62 |
}
|
63 |
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
+
import { useNavigate } from 'react-router-dom';
|
3 |
+
import { useAuth } from '../../hooks/useAuth';
|
4 |
+
import { Button, TextField, Box, Typography, Paper, CircularProgress } from '@mui/material';
|
5 |
+
import styled from '@emotion/styled';
|
6 |
+
|
7 |
+
const StyledPaper = styled(Paper)(({ theme }) => ({
|
8 |
+
padding: theme.spacing(4),
|
9 |
+
maxWidth: 400,
|
10 |
+
margin: 'auto',
|
11 |
+
marginTop: theme.spacing(8),
|
12 |
+
}));
|
13 |
+
|
14 |
+
const Login = () => {
|
15 |
+
const [username, setUsername] = useState('');
|
16 |
+
const [password, setPassword] = useState('');
|
17 |
+
const [error, setError] = useState('');
|
18 |
+
const [loading, setLoading] = useState(false);
|
19 |
+
const { login } = useAuth();
|
20 |
+
const navigate = useNavigate();
|
21 |
+
|
22 |
+
const handleSubmit = async (e) => {
|
23 |
+
e.preventDefault();
|
24 |
+
setLoading(true);
|
25 |
+
setError('');
|
26 |
+
|
27 |
+
try {
|
28 |
+
const result = await login(username, password);
|
29 |
+
if (result.success) {
|
30 |
+
navigate('/dashboard');
|
31 |
+
} else {
|
32 |
+
setError(result.error || 'Login failed. Please try again.');
|
33 |
+
}
|
34 |
+
} catch (err) {
|
35 |
+
setError(err.message || 'Login failed. Please try again.');
|
36 |
+
} finally {
|
37 |
+
setLoading(false);
|
38 |
+
}
|
39 |
+
};
|
40 |
+
|
41 |
+
return (
|
42 |
+
<StyledPaper elevation={3}>
|
43 |
+
<Typography variant="h4" gutterBottom align="center" sx={{ fontWeight: 'bold' }}>
|
44 |
+
EnlightenQalb
|
45 |
+
</Typography>
|
46 |
+
<Typography variant="subtitle1" gutterBottom align="center" color="textSecondary">
|
47 |
+
Sign in to explore Al-Ghazali's wisdom
|
48 |
+
</Typography>
|
49 |
+
|
50 |
+
{error && (
|
51 |
+
<Typography color="error" align="center" sx={{ my: 2 }}>
|
52 |
+
{error}
|
53 |
+
</Typography>
|
54 |
+
)}
|
55 |
+
|
56 |
+
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 3 }}>
|
57 |
+
<TextField
|
58 |
+
fullWidth
|
59 |
+
label="Username"
|
60 |
+
variant="outlined"
|
61 |
+
margin="normal"
|
62 |
+
value={username}
|
63 |
+
onChange={(e) => setUsername(e.target.value)}
|
64 |
+
required
|
65 |
+
/>
|
66 |
+
<TextField
|
67 |
+
fullWidth
|
68 |
+
label="Password"
|
69 |
+
type="password"
|
70 |
+
variant="outlined"
|
71 |
+
margin="normal"
|
72 |
+
value={password}
|
73 |
+
onChange={(e) => setPassword(e.target.value)}
|
74 |
+
required
|
75 |
+
/>
|
76 |
+
<Button
|
77 |
+
fullWidth
|
78 |
+
type="submit"
|
79 |
+
variant="contained"
|
80 |
+
color="primary"
|
81 |
+
size="large"
|
82 |
+
sx={{ mt: 3 }}
|
83 |
+
disabled={loading}
|
84 |
+
>
|
85 |
+
{loading ? <CircularProgress size={24} /> : 'Sign In'}
|
86 |
+
</Button>
|
87 |
+
</Box>
|
88 |
+
</StyledPaper>
|
89 |
+
);
|
90 |
+
};
|
91 |
+
|
92 |
+
export default Login;
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useEffect } from 'react';
|
2 |
+
import { useAuth } from '../../hooks/useAuth';
|
3 |
+
import { Box, Typography, CircularProgress } from '@mui/material';
|
4 |
+
|
5 |
+
const Logout = () => {
|
6 |
+
const { logout } = useAuth();
|
7 |
+
|
8 |
+
useEffect(() => {
|
9 |
+
const performLogout = async () => {
|
10 |
+
try {
|
11 |
+
await logout();
|
12 |
+
// navigation is now handled inside logout()
|
13 |
+
} catch (error) {
|
14 |
+
console.error('Logout error:', error);
|
15 |
+
// navigation fallback is handled in useAuth.js
|
16 |
+
}
|
17 |
+
};
|
18 |
+
|
19 |
+
performLogout();
|
20 |
+
}, [logout]);
|
21 |
+
|
22 |
+
return (
|
23 |
+
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', mt: 8 }}>
|
24 |
+
<Typography variant="h6" gutterBottom>
|
25 |
+
Logging out...
|
26 |
+
</Typography>
|
27 |
+
<CircularProgress />
|
28 |
+
</Box>
|
29 |
+
);
|
30 |
+
};
|
31 |
+
|
32 |
+
export default Logout;
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Navigate } from 'react-router-dom';
|
3 |
+
import { useAuth } from '../../hooks/useAuth';
|
4 |
+
|
5 |
+
const PrivateRoute = ({ children }) => {
|
6 |
+
const { isAuthenticated } = useAuth();
|
7 |
+
|
8 |
+
if (!isAuthenticated) {
|
9 |
+
return <Navigate to="/login" replace />;
|
10 |
+
}
|
11 |
+
|
12 |
+
return children;
|
13 |
+
};
|
14 |
+
|
15 |
+
export default PrivateRoute;
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useEffect } from 'react';
|
2 |
+
import { useNavigate, useLocation } from 'react-router-dom';
|
3 |
+
import { useAuth } from '../../hooks/useAuth';
|
4 |
+
import LoadingSpinner from '../UI/LoadingSpinner';
|
5 |
+
|
6 |
+
const TokenHandler = () => {
|
7 |
+
const { refreshToken } = useAuth();
|
8 |
+
const navigate = useNavigate();
|
9 |
+
const location = useLocation();
|
10 |
+
|
11 |
+
useEffect(() => {
|
12 |
+
const handleToken = async () => {
|
13 |
+
try {
|
14 |
+
await refreshToken();
|
15 |
+
// Redirect to the intended page or dashboard
|
16 |
+
const redirectTo = new URLSearchParams(location.search).get('redirect') || '/dashboard';
|
17 |
+
navigate(redirectTo, { replace: true });
|
18 |
+
} catch (error) {
|
19 |
+
console.error('Token refresh error:', error);
|
20 |
+
navigate('/login', { replace: true });
|
21 |
+
}
|
22 |
+
};
|
23 |
+
|
24 |
+
handleToken();
|
25 |
+
}, [refreshToken, navigate, location]);
|
26 |
+
|
27 |
+
return (
|
28 |
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
29 |
+
<LoadingSpinner message="Authenticating..." />
|
30 |
+
</div>
|
31 |
+
);
|
32 |
+
};
|
33 |
+
|
34 |
+
export default TokenHandler;
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Box, Typography, Link, Divider, IconButton } from '@mui/material';
|
3 |
+
import { GitHub, Twitter, Book } from '@mui/icons-material';
|
4 |
+
import styled from '@emotion/styled';
|
5 |
+
|
6 |
+
const FooterContainer = styled(Box)(({ theme }) => ({
|
7 |
+
padding: theme.spacing(4),
|
8 |
+
backgroundColor: theme.palette.background.paper,
|
9 |
+
marginTop: 'auto',
|
10 |
+
}));
|
11 |
+
|
12 |
+
const Footer = () => {
|
13 |
+
return (
|
14 |
+
<FooterContainer component="footer">
|
15 |
+
<Divider sx={{ mb: 3 }} />
|
16 |
+
<Box
|
17 |
+
display="flex"
|
18 |
+
flexDirection={{ xs: 'column', md: 'row' }}
|
19 |
+
justifyContent="space-between"
|
20 |
+
alignItems="center"
|
21 |
+
gap={2}
|
22 |
+
>
|
23 |
+
<Box>
|
24 |
+
<Typography variant="h6" color="primary" gutterBottom>
|
25 |
+
EnlightenQalb
|
26 |
+
</Typography>
|
27 |
+
<Typography variant="body2" color="textSecondary">
|
28 |
+
Exploring the wisdom of Al-Ghazali through modern AI
|
29 |
+
</Typography>
|
30 |
+
</Box>
|
31 |
+
|
32 |
+
<Box display="flex" gap={2}>
|
33 |
+
<IconButton
|
34 |
+
aria-label="GitHub"
|
35 |
+
href="https://github.com/humblebeeintel"
|
36 |
+
target="_blank"
|
37 |
+
rel="noopener"
|
38 |
+
>
|
39 |
+
<GitHub />
|
40 |
+
</IconButton>
|
41 |
+
<IconButton
|
42 |
+
aria-label="Twitter"
|
43 |
+
href="https://twitter.com/your-handle"
|
44 |
+
target="_blank"
|
45 |
+
rel="noopener"
|
46 |
+
>
|
47 |
+
<Twitter />
|
48 |
+
</IconButton>
|
49 |
+
<IconButton
|
50 |
+
aria-label="Original Text"
|
51 |
+
href="https://www.goodreads.com/book/show/27167514-the-alchemy-of-happiness"
|
52 |
+
target="_blank"
|
53 |
+
rel="noopener"
|
54 |
+
>
|
55 |
+
<Book />
|
56 |
+
</IconButton>
|
57 |
+
</Box>
|
58 |
+
|
59 |
+
<Box textAlign={{ xs: 'center', md: 'right' }}>
|
60 |
+
<Typography variant="caption" display="block" color="textSecondary">
|
61 |
+
© {new Date().getFullYear()} EnlightenQalb
|
62 |
+
</Typography>
|
63 |
+
<Typography variant="caption" display="block" color="textSecondary">
|
64 |
+
<Link href="/privacy" color="inherit">Privacy Policy</Link> | {' '}
|
65 |
+
<Link href="/terms" color="inherit">Terms</Link>
|
66 |
+
</Typography>
|
67 |
+
</Box>
|
68 |
+
</Box>
|
69 |
+
</FooterContainer>
|
70 |
+
);
|
71 |
+
};
|
72 |
+
|
73 |
+
export default Footer;
|
@@ -1,80 +0,0 @@
|
|
1 |
-
import React from 'react';
|
2 |
-
import styled from 'styled-components';
|
3 |
-
import { Link } from 'react-router-dom';
|
4 |
-
import ThemeToggle from '../UI/ThemeToggle';
|
5 |
-
import { useAuth } from '../../hooks/useAuth';
|
6 |
-
|
7 |
-
const HeaderContainer = styled.header`
|
8 |
-
background: ${({ theme }) => theme.colors.primary};
|
9 |
-
color: white;
|
10 |
-
padding: 1rem 2rem;
|
11 |
-
box-shadow: ${({ theme }) => theme.shadows.medium};
|
12 |
-
`;
|
13 |
-
|
14 |
-
const Nav = styled.nav`
|
15 |
-
display: flex;
|
16 |
-
justify-content: space-between;
|
17 |
-
align-items: center;
|
18 |
-
max-width: 1200px;
|
19 |
-
margin: 0 auto;
|
20 |
-
`;
|
21 |
-
|
22 |
-
const Logo = styled(Link)`
|
23 |
-
font-size: ${({ theme }) => theme.fontSizes.xxlarge};
|
24 |
-
font-weight: bold;
|
25 |
-
color: white;
|
26 |
-
text-decoration: none;
|
27 |
-
font-family: ${({ theme }) => theme.fonts.secondary};
|
28 |
-
`;
|
29 |
-
|
30 |
-
const NavLinks = styled.div`
|
31 |
-
display: flex;
|
32 |
-
gap: 2rem;
|
33 |
-
align-items: center;
|
34 |
-
`;
|
35 |
-
|
36 |
-
const NavLink = styled(Link)`
|
37 |
-
color: white;
|
38 |
-
text-decoration: none;
|
39 |
-
font-weight: 500;
|
40 |
-
transition: opacity 0.3s ease;
|
41 |
-
|
42 |
-
&:hover {
|
43 |
-
opacity: 0.8;
|
44 |
-
}
|
45 |
-
`;
|
46 |
-
|
47 |
-
const Header = () => {
|
48 |
-
const { isAuthenticated } = useAuth();
|
49 |
-
|
50 |
-
return (
|
51 |
-
<HeaderContainer>
|
52 |
-
<Nav>
|
53 |
-
<Logo to="/">EnlightenQalb</Logo>
|
54 |
-
<NavLinks>
|
55 |
-
<NavLink to="/">Home</NavLink>
|
56 |
-
{isAuthenticated && <NavLink to="/dashboard">Dashboard</NavLink>}
|
57 |
-
<NavLink to="/about">About</NavLink>
|
58 |
-
<ThemeToggle />
|
59 |
-
{isAuthenticated ? <LogoutButton /> : <LoginButton />}
|
60 |
-
</NavLinks>
|
61 |
-
</Nav>
|
62 |
-
</HeaderContainer>
|
63 |
-
);
|
64 |
-
};
|
65 |
-
|
66 |
-
const LoginButton = () => (
|
67 |
-
<NavLink to="/login">Login</NavLink>
|
68 |
-
);
|
69 |
-
|
70 |
-
const LogoutButton = () => {
|
71 |
-
const { logout } = useAuth();
|
72 |
-
|
73 |
-
return (
|
74 |
-
<NavLink as="button" onClick={logout} style={{ background: 'none', border: 'none' }}>
|
75 |
-
Logout
|
76 |
-
</NavLink>
|
77 |
-
);
|
78 |
-
};
|
79 |
-
|
80 |
-
export default Header;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import {
|
3 |
+
AppBar,
|
4 |
+
Toolbar,
|
5 |
+
Typography,
|
6 |
+
Button,
|
7 |
+
IconButton,
|
8 |
+
Box,
|
9 |
+
useScrollTrigger,
|
10 |
+
Slide
|
11 |
+
} from '@mui/material';
|
12 |
+
import MenuIcon from '@mui/icons-material/Menu';
|
13 |
+
import { useAuth } from '../../hooks/useAuth';
|
14 |
+
import { Link } from 'react-router-dom';
|
15 |
+
import ThemeToggle from '../UI/ThemeToggle';
|
16 |
+
|
17 |
+
function HideOnScroll({ children }) {
|
18 |
+
const trigger = useScrollTrigger();
|
19 |
+
return (
|
20 |
+
<Slide appear={false} direction="down" in={!trigger}>
|
21 |
+
{children}
|
22 |
+
</Slide>
|
23 |
+
);
|
24 |
+
}
|
25 |
+
|
26 |
+
const Header = () => {
|
27 |
+
const { isAuthenticated, logout } = useAuth();
|
28 |
+
|
29 |
+
return (
|
30 |
+
<HideOnScroll>
|
31 |
+
<AppBar position="sticky" elevation={1}>
|
32 |
+
<Toolbar>
|
33 |
+
<IconButton
|
34 |
+
edge="start"
|
35 |
+
color="inherit"
|
36 |
+
aria-label="menu"
|
37 |
+
sx={{ mr: 2 }}
|
38 |
+
>
|
39 |
+
<MenuIcon />
|
40 |
+
</IconButton>
|
41 |
+
|
42 |
+
<Typography
|
43 |
+
variant="h6"
|
44 |
+
component={Link}
|
45 |
+
to="/"
|
46 |
+
sx={{
|
47 |
+
flexGrow: 1,
|
48 |
+
textDecoration: 'none',
|
49 |
+
color: 'inherit',
|
50 |
+
fontWeight: 'bold'
|
51 |
+
}}
|
52 |
+
>
|
53 |
+
EnlightenQalb
|
54 |
+
</Typography>
|
55 |
+
|
56 |
+
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
57 |
+
<Button
|
58 |
+
color="inherit"
|
59 |
+
component={Link}
|
60 |
+
to="/about"
|
61 |
+
sx={{ mx: 1 }}
|
62 |
+
>
|
63 |
+
About
|
64 |
+
</Button>
|
65 |
+
{isAuthenticated ? (
|
66 |
+
<>
|
67 |
+
<Button
|
68 |
+
color="inherit"
|
69 |
+
component={Link}
|
70 |
+
to="/dashboard"
|
71 |
+
sx={{ mx: 1 }}
|
72 |
+
>
|
73 |
+
Dashboard
|
74 |
+
</Button>
|
75 |
+
<Button
|
76 |
+
color="inherit"
|
77 |
+
onClick={logout}
|
78 |
+
sx={{ mx: 1 }}
|
79 |
+
>
|
80 |
+
Logout
|
81 |
+
</Button>
|
82 |
+
</>
|
83 |
+
) : (
|
84 |
+
<Button
|
85 |
+
color="inherit"
|
86 |
+
component={Link}
|
87 |
+
to="/login"
|
88 |
+
sx={{ mx: 1 }}
|
89 |
+
>
|
90 |
+
Login
|
91 |
+
</Button>
|
92 |
+
)}
|
93 |
+
</Box>
|
94 |
+
|
95 |
+
<Box sx={{ display: 'flex', alignItems: 'center', ml: 2 }}>
|
96 |
+
<ThemeToggle />
|
97 |
+
</Box>
|
98 |
+
</Toolbar>
|
99 |
+
</AppBar>
|
100 |
+
</HideOnScroll>
|
101 |
+
);
|
102 |
+
};
|
103 |
+
|
104 |
+
export default Header;
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Box } from '@mui/material';
|
3 |
+
import Header from './Header';
|
4 |
+
import Footer from './Footer';
|
5 |
+
import styled from '@emotion/styled';
|
6 |
+
|
7 |
+
const MainContent = styled(Box)(({ theme }) => ({
|
8 |
+
minHeight: 'calc(100vh - 128px)', // Adjust based on your header/footer height
|
9 |
+
padding: theme.spacing(3),
|
10 |
+
[theme.breakpoints.down('sm')]: {
|
11 |
+
padding: theme.spacing(2),
|
12 |
+
},
|
13 |
+
}));
|
14 |
+
|
15 |
+
const MainLayout = ({ children }) => {
|
16 |
+
return (
|
17 |
+
<Box display="flex" flexDirection="column" minHeight="100vh">
|
18 |
+
<Header />
|
19 |
+
<MainContent component="main">
|
20 |
+
{children}
|
21 |
+
</MainContent>
|
22 |
+
<Footer />
|
23 |
+
</Box>
|
24 |
+
);
|
25 |
+
};
|
26 |
+
|
27 |
+
export default MainLayout;
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
+
import { useAuth } from '../../hooks/useAuth';
|
3 |
+
import {
|
4 |
+
Box,
|
5 |
+
Button,
|
6 |
+
Typography,
|
7 |
+
Paper,
|
8 |
+
Radio,
|
9 |
+
RadioGroup,
|
10 |
+
FormControlLabel,
|
11 |
+
FormControl,
|
12 |
+
FormLabel,
|
13 |
+
Slider,
|
14 |
+
Rating,
|
15 |
+
Alert
|
16 |
+
} from '@mui/material';
|
17 |
+
import styled from '@emotion/styled';
|
18 |
+
import { saveFeedback } from '../../services/search';
|
19 |
+
|
20 |
+
const StyledPaper = styled(Paper)(({ theme }) => ({
|
21 |
+
padding: theme.spacing(3),
|
22 |
+
marginTop: theme.spacing(3),
|
23 |
+
backgroundColor: theme.palette.background.paper,
|
24 |
+
}));
|
25 |
+
|
26 |
+
const FeedbackForm = ({ searchResult, query, onFeedbackSubmitted }) => {
|
27 |
+
const { user, authTokens } = useAuth();
|
28 |
+
const [reaction, setReaction] = useState('');
|
29 |
+
const [confidence, setConfidence] = useState(5);
|
30 |
+
const [rating, setRating] = useState(3);
|
31 |
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
32 |
+
const [error, setError] = useState(null);
|
33 |
+
const [success, setSuccess] = useState(false);
|
34 |
+
|
35 |
+
// Debug to check what searchResult contains
|
36 |
+
console.log('SearchResult:', searchResult);
|
37 |
+
|
38 |
+
// Ensure we have a proper searchResult object
|
39 |
+
if (!searchResult || typeof searchResult !== 'object') {
|
40 |
+
return (
|
41 |
+
<StyledPaper elevation={2}>
|
42 |
+
<Alert severity="error">Invalid search result data</Alert>
|
43 |
+
</StyledPaper>
|
44 |
+
);
|
45 |
+
}
|
46 |
+
|
47 |
+
const handleSubmit = async (e) => {
|
48 |
+
e.preventDefault();
|
49 |
+
setIsSubmitting(true);
|
50 |
+
setError(null);
|
51 |
+
|
52 |
+
try {
|
53 |
+
// Extract only the needed text properties safely
|
54 |
+
const feedbackData = {
|
55 |
+
user_type: 'user',
|
56 |
+
username: user || 'anonymous',
|
57 |
+
query: query || '',
|
58 |
+
retrieved_text: searchResult.content || searchResult.text || JSON.stringify(searchResult),
|
59 |
+
model_type: searchResult.model_type || 'unknown',
|
60 |
+
reaction,
|
61 |
+
confidence_score: confidence,
|
62 |
+
rating
|
63 |
+
};
|
64 |
+
|
65 |
+
await saveFeedback(feedbackData, authTokens.access);
|
66 |
+
|
67 |
+
setSuccess(true);
|
68 |
+
if (onFeedbackSubmitted) onFeedbackSubmitted();
|
69 |
+
// Reset form
|
70 |
+
setReaction('');
|
71 |
+
setConfidence(5);
|
72 |
+
setRating(3);
|
73 |
+
} catch (err) {
|
74 |
+
console.error("Feedback submission error:", err);
|
75 |
+
setError(typeof err === 'string' ? err : (err.message || 'Failed to submit feedback'));
|
76 |
+
} finally {
|
77 |
+
setIsSubmitting(false);
|
78 |
+
}
|
79 |
+
};
|
80 |
+
|
81 |
+
return (
|
82 |
+
<StyledPaper elevation={2}>
|
83 |
+
<Typography variant="h6" gutterBottom>
|
84 |
+
Was this result helpful?
|
85 |
+
</Typography>
|
86 |
+
|
87 |
+
{error && (
|
88 |
+
<Alert severity="error" sx={{ mb: 2 }}>
|
89 |
+
{typeof error === 'string' ? error : 'An error occurred'}
|
90 |
+
</Alert>
|
91 |
+
)}
|
92 |
+
|
93 |
+
{success ? (
|
94 |
+
<Alert severity="success" sx={{ mb: 2 }}>
|
95 |
+
Thank you for your feedback!
|
96 |
+
</Alert>
|
97 |
+
) : (
|
98 |
+
<Box component="form" onSubmit={handleSubmit}>
|
99 |
+
<FormControl component="fieldset" sx={{ mb: 3 }} fullWidth>
|
100 |
+
<FormLabel component="legend">Your reaction to this result:</FormLabel>
|
101 |
+
<RadioGroup
|
102 |
+
row
|
103 |
+
value={reaction}
|
104 |
+
onChange={(e) => setReaction(e.target.value)}
|
105 |
+
>
|
106 |
+
<FormControlLabel value="relevant" control={<Radio />} label="Relevant" />
|
107 |
+
<FormControlLabel value="somewhat_relevant" control={<Radio />} label="Somewhat Relevant" />
|
108 |
+
<FormControlLabel value="not_relevant" control={<Radio />} label="Not Relevant" />
|
109 |
+
</RadioGroup>
|
110 |
+
</FormControl>
|
111 |
+
|
112 |
+
<Box sx={{ mb: 3 }}>
|
113 |
+
<Typography gutterBottom>How confident are you in this rating?</Typography>
|
114 |
+
<Slider
|
115 |
+
value={confidence}
|
116 |
+
onChange={(e, newValue) => setConfidence(newValue)}
|
117 |
+
min={1}
|
118 |
+
max={10}
|
119 |
+
step={1}
|
120 |
+
marks
|
121 |
+
valueLabelDisplay="auto"
|
122 |
+
aria-labelledby="confidence-slider"
|
123 |
+
/>
|
124 |
+
</Box>
|
125 |
+
|
126 |
+
<Box sx={{ mb: 3 }}>
|
127 |
+
<Typography component="legend">Overall quality rating</Typography>
|
128 |
+
<Rating
|
129 |
+
name="quality-rating"
|
130 |
+
value={rating}
|
131 |
+
onChange={(e, newValue) => setRating(newValue)}
|
132 |
+
precision={0.5}
|
133 |
+
/>
|
134 |
+
</Box>
|
135 |
+
|
136 |
+
<Button
|
137 |
+
type="submit"
|
138 |
+
variant="contained"
|
139 |
+
color="primary"
|
140 |
+
disabled={!reaction || isSubmitting}
|
141 |
+
fullWidth
|
142 |
+
>
|
143 |
+
{isSubmitting ? 'Submitting...' : 'Submit Feedback'}
|
144 |
+
</Button>
|
145 |
+
</Box>
|
146 |
+
)}
|
147 |
+
</StyledPaper>
|
148 |
+
);
|
149 |
+
};
|
150 |
+
|
151 |
+
export default FeedbackForm;
|
@@ -1,85 +1,89 @@
|
|
1 |
import React, { useState } from 'react';
|
2 |
-
import
|
3 |
-
|
4 |
-
|
5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
-
const
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
background: ${({ theme }) => theme.colors.cardBg};
|
12 |
-
border-radius: ${({ theme }) => theme.radii.large};
|
13 |
-
box-shadow: ${({ theme }) => theme.shadows.medium};
|
14 |
-
`;
|
15 |
|
16 |
-
const SearchForm =
|
17 |
-
display: flex;
|
18 |
-
flex-direction: column;
|
19 |
-
gap: 1rem;
|
20 |
-
`;
|
21 |
-
|
22 |
-
const SearchInput = styled.input`
|
23 |
-
padding: 1rem;
|
24 |
-
border: 2px solid ${({ theme }) => theme.colors.primary};
|
25 |
-
border-radius: ${({ theme }) => theme.radii.medium};
|
26 |
-
font-size: 1rem;
|
27 |
-
&:focus {
|
28 |
-
outline: none;
|
29 |
-
border-color: ${({ theme }) => theme.colors.secondary};
|
30 |
-
}
|
31 |
-
`;
|
32 |
-
|
33 |
-
const SearchButton = styled.button`
|
34 |
-
padding: 1rem;
|
35 |
-
background: ${({ theme }) => theme.colors.primary};
|
36 |
-
color: white;
|
37 |
-
border: none;
|
38 |
-
border-radius: ${({ theme }) => theme.radii.medium};
|
39 |
-
cursor: pointer;
|
40 |
-
font-size: 1rem;
|
41 |
-
transition: all 0.3s ease;
|
42 |
-
|
43 |
-
&:hover {
|
44 |
-
background: ${({ theme }) => theme.colors.secondary};
|
45 |
-
transform: translateY(-2px);
|
46 |
-
}
|
47 |
-
|
48 |
-
&:disabled {
|
49 |
-
background: ${({ theme }) => theme.colors.disabled};
|
50 |
-
cursor: not-allowed;
|
51 |
-
}
|
52 |
-
`;
|
53 |
-
|
54 |
-
const SearchFormComponent = ({ onSearchComplete }) => {
|
55 |
const [query, setQuery] = useState('');
|
56 |
-
const
|
57 |
-
|
58 |
-
const handleSubmit =
|
59 |
e.preventDefault();
|
60 |
-
if (
|
61 |
-
|
62 |
-
|
63 |
-
onSearchComplete(results);
|
64 |
};
|
65 |
|
66 |
return (
|
67 |
-
<
|
68 |
-
<
|
69 |
-
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
value={query}
|
72 |
onChange={(e) => setQuery(e.target.value)}
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
/>
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
);
|
83 |
};
|
84 |
|
85 |
-
export default
|
|
|
1 |
import React, { useState } from 'react';
|
2 |
+
import {
|
3 |
+
Box,
|
4 |
+
TextField,
|
5 |
+
Button,
|
6 |
+
Typography,
|
7 |
+
Paper,
|
8 |
+
CircularProgress,
|
9 |
+
Grid,
|
10 |
+
ToggleButton,
|
11 |
+
ToggleButtonGroup
|
12 |
+
} from '@mui/material';
|
13 |
+
import SearchIcon from '@mui/icons-material/Search';
|
14 |
+
import styled from '@emotion/styled';
|
15 |
|
16 |
+
const StyledPaper = styled(Paper)(({ theme }) => ({
|
17 |
+
padding: theme.spacing(3),
|
18 |
+
marginBottom: theme.spacing(3),
|
19 |
+
}));
|
|
|
|
|
|
|
|
|
20 |
|
21 |
+
const SearchForm = ({ onSearch, isLoading }) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
const [query, setQuery] = useState('');
|
23 |
+
const [modelPreference, setModelPreference] = useState('both');
|
24 |
+
|
25 |
+
const handleSubmit = (e) => {
|
26 |
e.preventDefault();
|
27 |
+
if (query.trim()) {
|
28 |
+
onSearch(query, modelPreference);
|
29 |
+
}
|
|
|
30 |
};
|
31 |
|
32 |
return (
|
33 |
+
<StyledPaper elevation={2}>
|
34 |
+
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold' }}>
|
35 |
+
Explore Al-Ghazali's Wisdom
|
36 |
+
</Typography>
|
37 |
+
<Typography variant="subtitle1" gutterBottom color="textSecondary">
|
38 |
+
Search through "The Alchemy of Happiness"
|
39 |
+
</Typography>
|
40 |
+
|
41 |
+
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
42 |
+
<TextField
|
43 |
+
fullWidth
|
44 |
+
variant="outlined"
|
45 |
+
label="What would you like to know?"
|
46 |
value={query}
|
47 |
onChange={(e) => setQuery(e.target.value)}
|
48 |
+
InputProps={{
|
49 |
+
endAdornment: (
|
50 |
+
<Button
|
51 |
+
type="submit"
|
52 |
+
color="primary"
|
53 |
+
variant="contained"
|
54 |
+
disabled={isLoading}
|
55 |
+
sx={{ ml: 1 }}
|
56 |
+
>
|
57 |
+
{isLoading ? <CircularProgress size={24} /> : <SearchIcon />}
|
58 |
+
</Button>
|
59 |
+
),
|
60 |
+
}}
|
61 |
/>
|
62 |
+
|
63 |
+
<Box sx={{ mt: 2 }}>
|
64 |
+
<Typography variant="subtitle2" gutterBottom>
|
65 |
+
Model Preference:
|
66 |
+
</Typography>
|
67 |
+
<ToggleButtonGroup
|
68 |
+
value={modelPreference}
|
69 |
+
exclusive
|
70 |
+
onChange={(e, newValue) => newValue && setModelPreference(newValue)}
|
71 |
+
aria-label="model preference"
|
72 |
+
>
|
73 |
+
<ToggleButton value="uae" aria-label="uae model">
|
74 |
+
UAE-Large
|
75 |
+
</ToggleButton>
|
76 |
+
<ToggleButton value="bge" aria-label="bge model">
|
77 |
+
BGE-Large
|
78 |
+
</ToggleButton>
|
79 |
+
<ToggleButton value="both" aria-label="both models">
|
80 |
+
Both
|
81 |
+
</ToggleButton>
|
82 |
+
</ToggleButtonGroup>
|
83 |
+
</Box>
|
84 |
+
</Box>
|
85 |
+
</StyledPaper>
|
86 |
);
|
87 |
};
|
88 |
|
89 |
+
export default SearchForm;
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
+
import {
|
3 |
+
Box,
|
4 |
+
Paper,
|
5 |
+
Typography,
|
6 |
+
Divider,
|
7 |
+
Chip,
|
8 |
+
CircularProgress,
|
9 |
+
Accordion,
|
10 |
+
AccordionSummary,
|
11 |
+
AccordionDetails,
|
12 |
+
Alert
|
13 |
+
} from '@mui/material';
|
14 |
+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
15 |
+
import styled from '@emotion/styled';
|
16 |
+
import FeedbackForm from './FeedbackForm';
|
17 |
+
|
18 |
+
const StyledPaper = styled(Paper)(({ theme }) => ({
|
19 |
+
padding: theme.spacing(2),
|
20 |
+
marginBottom: theme.spacing(2),
|
21 |
+
backgroundColor: theme.palette.background.paper,
|
22 |
+
}));
|
23 |
+
|
24 |
+
const ModelChip = styled(Chip)(({ theme, modeltype }) => ({
|
25 |
+
marginLeft: theme.spacing(1),
|
26 |
+
backgroundColor: modeltype === 'WhereIsAI_UAE_Large_V1'
|
27 |
+
? theme.palette.primary.light
|
28 |
+
: theme.palette.secondary.light,
|
29 |
+
color: theme.palette.getContrastText(
|
30 |
+
modeltype === 'WhereIsAI_UAE_Large_V1'
|
31 |
+
? theme.palette.primary.light
|
32 |
+
: theme.palette.secondary.light
|
33 |
+
),
|
34 |
+
}));
|
35 |
+
|
36 |
+
const SearchResults = ({ results, query, isLoading, error }) => {
|
37 |
+
const [expandedResult, setExpandedResult] = useState(null);
|
38 |
+
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
|
39 |
+
|
40 |
+
if (isLoading) {
|
41 |
+
return (
|
42 |
+
<Box display="flex" justifyContent="center" my={4}>
|
43 |
+
<CircularProgress />
|
44 |
+
</Box>
|
45 |
+
);
|
46 |
+
}
|
47 |
+
|
48 |
+
if (error) {
|
49 |
+
return (
|
50 |
+
<Alert severity="error" sx={{ my: 2 }}>
|
51 |
+
{error}
|
52 |
+
</Alert>
|
53 |
+
);
|
54 |
+
}
|
55 |
+
|
56 |
+
if (!results || results.length === 0) {
|
57 |
+
return (
|
58 |
+
<Typography variant="body1" color="textSecondary" align="center" sx={{ my: 4 }}>
|
59 |
+
No results found. Try a different query.
|
60 |
+
</Typography>
|
61 |
+
);
|
62 |
+
}
|
63 |
+
|
64 |
+
return (
|
65 |
+
<Box>
|
66 |
+
<Typography variant="h6" gutterBottom>
|
67 |
+
Search Results
|
68 |
+
</Typography>
|
69 |
+
|
70 |
+
{results.map((result, index) => (
|
71 |
+
<Accordion
|
72 |
+
key={index}
|
73 |
+
expanded={expandedResult === index}
|
74 |
+
onChange={() => setExpandedResult(expandedResult === index ? null : index)}
|
75 |
+
>
|
76 |
+
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
77 |
+
<Box width="100%">
|
78 |
+
<Box display="flex" alignItems="center">
|
79 |
+
<Typography variant="subtitle1" noWrap sx={{ flexGrow: 1 }}>
|
80 |
+
Excerpt from "{result.model_type.includes('UAE') ? 'UAE' : 'BGE'} Model"
|
81 |
+
</Typography>
|
82 |
+
<ModelChip
|
83 |
+
label={`${(result.similarity * 100).toFixed(1)}% match`}
|
84 |
+
modeltype={result.model_type}
|
85 |
+
/>
|
86 |
+
</Box>
|
87 |
+
<Typography variant="caption" color="textSecondary">
|
88 |
+
Click to {expandedResult === index ? 'collapse' : 'expand'}
|
89 |
+
</Typography>
|
90 |
+
</Box>
|
91 |
+
</AccordionSummary>
|
92 |
+
<AccordionDetails>
|
93 |
+
<Typography variant="body1" paragraph>
|
94 |
+
{result.text}
|
95 |
+
</Typography>
|
96 |
+
|
97 |
+
<Divider sx={{ my: 2 }} />
|
98 |
+
|
99 |
+
<FeedbackForm
|
100 |
+
searchResult={result}
|
101 |
+
query={query}
|
102 |
+
onFeedbackSubmitted={() => setFeedbackSubmitted(true)}
|
103 |
+
/>
|
104 |
+
</AccordionDetails>
|
105 |
+
</Accordion>
|
106 |
+
))}
|
107 |
+
</Box>
|
108 |
+
);
|
109 |
+
};
|
110 |
+
|
111 |
+
export default SearchResults;
|
@@ -1,81 +0,0 @@
|
|
1 |
-
import React from 'react';
|
2 |
-
import styled from 'styled-components';
|
3 |
-
import FeedbackForm from './FeedbackForm';
|
4 |
-
|
5 |
-
const ResultsContainer = styled.div`
|
6 |
-
max-width: 800px;
|
7 |
-
margin: 2rem auto;
|
8 |
-
padding: 2rem;
|
9 |
-
background: ${({ theme }) => theme.colors.cardBg};
|
10 |
-
border-radius: ${({ theme }) => theme.radii.large};
|
11 |
-
box-shadow: ${({ theme }) => theme.shadows.medium};
|
12 |
-
`;
|
13 |
-
|
14 |
-
const ResultItem = styled.div`
|
15 |
-
margin-bottom: 2rem;
|
16 |
-
padding: 1.5rem;
|
17 |
-
background: ${({ theme }) => theme.colors.background};
|
18 |
-
border-radius: ${({ theme }) => theme.radii.medium};
|
19 |
-
border-left: 4px solid ${({ theme }) => theme.colors.primary};
|
20 |
-
`;
|
21 |
-
|
22 |
-
const ResultText = styled.p`
|
23 |
-
font-size: 1.1rem;
|
24 |
-
line-height: 1.6;
|
25 |
-
color: ${({ theme }) => theme.colors.text};
|
26 |
-
margin-bottom: 1rem;
|
27 |
-
`;
|
28 |
-
|
29 |
-
const MetaInfo = styled.div`
|
30 |
-
display: flex;
|
31 |
-
justify-content: space-between;
|
32 |
-
font-size: 0.9rem;
|
33 |
-
color: ${({ theme }) => theme.colors.muted};
|
34 |
-
`;
|
35 |
-
|
36 |
-
const ModelBadge = styled.span`
|
37 |
-
padding: 0.25rem 0.5rem;
|
38 |
-
border-radius: ${({ theme }) => theme.radii.small};
|
39 |
-
background: ${({ theme, modelType }) =>
|
40 |
-
modelType.includes('UAE') ? theme.colors.accent1 : theme.colors.accent2};
|
41 |
-
color: white;
|
42 |
-
font-weight: bold;
|
43 |
-
`;
|
44 |
-
|
45 |
-
const SimilarityScore = styled.span`
|
46 |
-
font-weight: bold;
|
47 |
-
color: ${({ theme }) => theme.colors.primary};
|
48 |
-
`;
|
49 |
-
|
50 |
-
const SearchResults = ({ results, onFeedbackSubmit }) => {
|
51 |
-
if (!results || results.length === 0) return null;
|
52 |
-
|
53 |
-
return (
|
54 |
-
<ResultsContainer>
|
55 |
-
<h2>Wisdom from Al-Ghazali</h2>
|
56 |
-
{results.map((result, index) => (
|
57 |
-
<div key={index}>
|
58 |
-
<ResultItem>
|
59 |
-
<ResultText>{result.text}</ResultText>
|
60 |
-
<MetaInfo>
|
61 |
-
<ModelBadge modelType={result.model_type}>
|
62 |
-
{result.model_type.split('_')[0]}
|
63 |
-
</ModelBadge>
|
64 |
-
<SimilarityScore>
|
65 |
-
{(result.similarity * 100).toFixed(1)}% match
|
66 |
-
</SimilarityScore>
|
67 |
-
</MetaInfo>
|
68 |
-
</ResultItem>
|
69 |
-
<FeedbackForm
|
70 |
-
query={result.query}
|
71 |
-
retrievedText={result.text}
|
72 |
-
modelType={result.model_type}
|
73 |
-
onSubmit={onFeedbackSubmit}
|
74 |
-
/>
|
75 |
-
</div>
|
76 |
-
))}
|
77 |
-
</ResultsContainer>
|
78 |
-
);
|
79 |
-
};
|
80 |
-
|
81 |
-
export default SearchResults;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Box, Typography, Button } from '@mui/material';
|
3 |
+
import ErrorIcon from '@mui/icons-material/Error';
|
4 |
+
|
5 |
+
class ErrorBoundary extends React.Component {
|
6 |
+
constructor(props) {
|
7 |
+
super(props);
|
8 |
+
this.state = { hasError: false, error: null };
|
9 |
+
}
|
10 |
+
|
11 |
+
static getDerivedStateFromError(error) {
|
12 |
+
return { hasError: true, error };
|
13 |
+
}
|
14 |
+
|
15 |
+
componentDidCatch(error, errorInfo) {
|
16 |
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
17 |
+
}
|
18 |
+
|
19 |
+
render() {
|
20 |
+
if (this.state.hasError) {
|
21 |
+
return (
|
22 |
+
<Box
|
23 |
+
sx={{
|
24 |
+
display: 'flex',
|
25 |
+
flexDirection: 'column',
|
26 |
+
alignItems: 'center',
|
27 |
+
justifyContent: 'center',
|
28 |
+
padding: 4,
|
29 |
+
minHeight: '100vh',
|
30 |
+
textAlign: 'center',
|
31 |
+
}}
|
32 |
+
>
|
33 |
+
<ErrorIcon color="error" sx={{ fontSize: 60, mb: 2 }} />
|
34 |
+
<Typography variant="h4" gutterBottom>
|
35 |
+
Something went wrong
|
36 |
+
</Typography>
|
37 |
+
<Typography variant="body1" color="textSecondary" sx={{ mb: 3 }}>
|
38 |
+
{this.state.error && this.state.error.toString()}
|
39 |
+
</Typography>
|
40 |
+
<Button
|
41 |
+
variant="contained"
|
42 |
+
color="primary"
|
43 |
+
onClick={() => window.location.reload()}
|
44 |
+
>
|
45 |
+
Reload page
|
46 |
+
</Button>
|
47 |
+
</Box>
|
48 |
+
);
|
49 |
+
}
|
50 |
+
|
51 |
+
return this.props.children;
|
52 |
+
}
|
53 |
+
}
|
54 |
+
|
55 |
+
export default ErrorBoundary;
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Alert, AlertTitle, Button, Box, Collapse } from '@mui/material';
|
3 |
+
import { Close } from '@mui/icons-material';
|
4 |
+
import styled from '@emotion/styled';
|
5 |
+
|
6 |
+
const ErrorContainer = styled(Box)(({ theme }) => ({
|
7 |
+
marginBottom: theme.spacing(2),
|
8 |
+
width: '100%',
|
9 |
+
}));
|
10 |
+
|
11 |
+
const ErrorMessage = ({
|
12 |
+
error,
|
13 |
+
onRetry,
|
14 |
+
onClose,
|
15 |
+
severity = 'error',
|
16 |
+
title = 'Error',
|
17 |
+
retryText = 'Try Again',
|
18 |
+
closable = true
|
19 |
+
}) => {
|
20 |
+
const [open, setOpen] = React.useState(true);
|
21 |
+
|
22 |
+
const handleClose = () => {
|
23 |
+
setOpen(false);
|
24 |
+
if (onClose) onClose();
|
25 |
+
};
|
26 |
+
|
27 |
+
if (!error || !open) return null;
|
28 |
+
|
29 |
+
return (
|
30 |
+
<ErrorContainer>
|
31 |
+
<Collapse in={open}>
|
32 |
+
<Alert
|
33 |
+
severity={severity}
|
34 |
+
action={
|
35 |
+
<Box display="flex" gap={1}>
|
36 |
+
{onRetry && (
|
37 |
+
<Button
|
38 |
+
color="inherit"
|
39 |
+
size="small"
|
40 |
+
onClick={onRetry}
|
41 |
+
>
|
42 |
+
{retryText}
|
43 |
+
</Button>
|
44 |
+
)}
|
45 |
+
{closable && (
|
46 |
+
<Button
|
47 |
+
color="inherit"
|
48 |
+
size="small"
|
49 |
+
onClick={handleClose}
|
50 |
+
endIcon={<Close fontSize="small" />}
|
51 |
+
>
|
52 |
+
Close
|
53 |
+
</Button>
|
54 |
+
)}
|
55 |
+
</Box>
|
56 |
+
}
|
57 |
+
>
|
58 |
+
<AlertTitle>{title}</AlertTitle>
|
59 |
+
{typeof error === 'string' ? error : error.message || 'An unknown error occurred'}
|
60 |
+
</Alert>
|
61 |
+
</Collapse>
|
62 |
+
</ErrorContainer>
|
63 |
+
);
|
64 |
+
};
|
65 |
+
|
66 |
+
export default ErrorMessage;
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { CircularProgress, Box, Typography, Fade } from '@mui/material';
|
3 |
+
import { styled } from '@mui/material/styles';
|
4 |
+
|
5 |
+
// Fix the styled component to use proper spacing
|
6 |
+
const StyledBox = styled(Box)(({ theme }) => ({
|
7 |
+
display: 'flex',
|
8 |
+
flexDirection: 'column',
|
9 |
+
alignItems: 'center',
|
10 |
+
justifyContent: 'center',
|
11 |
+
padding: theme.spacing ? theme.spacing(2) : '16px', // Provide fallback
|
12 |
+
textAlign: 'center',
|
13 |
+
}));
|
14 |
+
|
15 |
+
const LoadingSpinner = ({ message = 'Loading...', size = 40 }) => {
|
16 |
+
return (
|
17 |
+
<Fade in={true} timeout={600}>
|
18 |
+
<StyledBox>
|
19 |
+
<CircularProgress
|
20 |
+
size={size}
|
21 |
+
thickness={4}
|
22 |
+
sx={{
|
23 |
+
color: (theme) => theme.palette.primary.main,
|
24 |
+
marginBottom: (theme) => theme.spacing?.(2) || '16px',
|
25 |
+
}}
|
26 |
+
/>
|
27 |
+
{message && (
|
28 |
+
<Typography
|
29 |
+
variant="body1"
|
30 |
+
sx={{
|
31 |
+
fontWeight: 500,
|
32 |
+
color: (theme) => theme.palette.text.secondary,
|
33 |
+
}}
|
34 |
+
>
|
35 |
+
{message}
|
36 |
+
</Typography>
|
37 |
+
)}
|
38 |
+
</StyledBox>
|
39 |
+
</Fade>
|
40 |
+
);
|
41 |
+
};
|
42 |
+
|
43 |
+
export default LoadingSpinner;
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { IconButton, Tooltip } from '@mui/material';
|
3 |
+
import { Brightness4, Brightness7 } from '@mui/icons-material';
|
4 |
+
import { useTheme as useMuiTheme } from '@mui/material/styles';
|
5 |
+
import { useTheme } from '../../hooks/useTheme';
|
6 |
+
|
7 |
+
const ThemeToggle = ({ size = 'medium' }) => {
|
8 |
+
const { toggleTheme, isDark } = useTheme();
|
9 |
+
const theme = useMuiTheme();
|
10 |
+
|
11 |
+
return (
|
12 |
+
<Tooltip title={`Switch to ${isDark ? 'light' : 'dark'} mode`}>
|
13 |
+
<IconButton
|
14 |
+
onClick={toggleTheme}
|
15 |
+
color="inherit"
|
16 |
+
size={size}
|
17 |
+
aria-label="toggle theme"
|
18 |
+
>
|
19 |
+
{isDark ? (
|
20 |
+
<Brightness7 sx={{ color: theme.palette.warning?.main || theme.palette.primary.main }} />
|
21 |
+
) : (
|
22 |
+
<Brightness4 sx={{ color: theme.palette.grey[600] }} />
|
23 |
+
)}
|
24 |
+
</IconButton>
|
25 |
+
</Tooltip>
|
26 |
+
);
|
27 |
+
};
|
28 |
+
|
29 |
+
export default ThemeToggle;
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// src/hooks/useAuth.js
|
2 |
+
import { useState, useContext, createContext, useCallback, useEffect } from 'react';
|
3 |
+
import { useNavigate } from 'react-router-dom';
|
4 |
+
import { login as apiLogin, logout as apiLogout, refreshToken as apiRefreshToken } from '../services/auth';
|
5 |
+
import { useLocalStorage } from './useLocalStorage';
|
6 |
+
|
7 |
+
const AuthContext = createContext();
|
8 |
+
|
9 |
+
export const AuthProvider = ({ children }) => {
|
10 |
+
const [user, setUser] = useState(null);
|
11 |
+
const [authTokens, setAuthTokens] = useLocalStorage('authTokens', null);
|
12 |
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
13 |
+
const navigate = useNavigate();
|
14 |
+
|
15 |
+
// Auto-refresh token when it's about to expire
|
16 |
+
useEffect(() => {
|
17 |
+
const refreshInterval = setInterval(() => {
|
18 |
+
if (authTokens?.access) {
|
19 |
+
const { exp } = parseJwt(authTokens.access);
|
20 |
+
// Refresh token if it expires in less than 5 minutes
|
21 |
+
if (exp * 1000 - Date.now() < 300000) {
|
22 |
+
refreshToken();
|
23 |
+
}
|
24 |
+
}
|
25 |
+
}, 60000); // Check every minute
|
26 |
+
|
27 |
+
return () => clearInterval(refreshInterval);
|
28 |
+
}, [authTokens]);
|
29 |
+
|
30 |
+
const parseJwt = (token) => {
|
31 |
+
try {
|
32 |
+
return JSON.parse(atob(token.split('.')[1]));
|
33 |
+
} catch (e) {
|
34 |
+
return null;
|
35 |
+
}
|
36 |
+
};
|
37 |
+
|
38 |
+
const login = useCallback(async (username, password) => {
|
39 |
+
try {
|
40 |
+
const { access_token, refresh_token } = await apiLogin(username, password);
|
41 |
+
setAuthTokens({
|
42 |
+
access: access_token,
|
43 |
+
refresh: refresh_token
|
44 |
+
});
|
45 |
+
setUser(username);
|
46 |
+
return { success: true };
|
47 |
+
} catch (error) {
|
48 |
+
return { success: false, error: error.message };
|
49 |
+
}
|
50 |
+
}, [setAuthTokens]);
|
51 |
+
|
52 |
+
const logout = useCallback(async () => {
|
53 |
+
try {
|
54 |
+
if (authTokens?.access) {
|
55 |
+
await apiLogout(authTokens.access);
|
56 |
+
}
|
57 |
+
setAuthTokens(null);
|
58 |
+
setUser(null);
|
59 |
+
navigate('/login');
|
60 |
+
} catch (error) {
|
61 |
+
console.error('Logout error:', error);
|
62 |
+
// Even if logout API fails, clear local tokens
|
63 |
+
setAuthTokens(null);
|
64 |
+
setUser(null);
|
65 |
+
}
|
66 |
+
}, [authTokens, setAuthTokens, navigate]);
|
67 |
+
|
68 |
+
const refreshToken = useCallback(async () => {
|
69 |
+
if (!authTokens?.refresh || isRefreshing) return;
|
70 |
+
|
71 |
+
setIsRefreshing(true);
|
72 |
+
try {
|
73 |
+
const { access_token, refresh_token } = await apiRefreshToken({
|
74 |
+
refresh_token: authTokens.refresh
|
75 |
+
});
|
76 |
+
setAuthTokens({
|
77 |
+
access: access_token,
|
78 |
+
refresh: refresh_token
|
79 |
+
});
|
80 |
+
return access_token;
|
81 |
+
} catch (error) {
|
82 |
+
console.error('Token refresh failed:', error);
|
83 |
+
logout(); // Full logout if refresh fails
|
84 |
+
throw error;
|
85 |
+
} finally {
|
86 |
+
setIsRefreshing(false);
|
87 |
+
}
|
88 |
+
}, [authTokens, isRefreshing, setAuthTokens, logout]);
|
89 |
+
|
90 |
+
const getAccessToken = useCallback(async () => {
|
91 |
+
if (!authTokens?.access) return null;
|
92 |
+
|
93 |
+
const { exp } = parseJwt(authTokens.access);
|
94 |
+
if (exp * 1000 - Date.now() < 30000) { // If expires in <30 seconds
|
95 |
+
return await refreshToken();
|
96 |
+
}
|
97 |
+
return authTokens.access;
|
98 |
+
}, [authTokens, refreshToken]);
|
99 |
+
|
100 |
+
const value = {
|
101 |
+
user,
|
102 |
+
authTokens,
|
103 |
+
isAuthenticated: !!authTokens?.access,
|
104 |
+
isRefreshing,
|
105 |
+
login,
|
106 |
+
logout,
|
107 |
+
refreshToken,
|
108 |
+
getAccessToken
|
109 |
+
};
|
110 |
+
|
111 |
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
112 |
+
};
|
113 |
+
|
114 |
+
export const useAuth = () => {
|
115 |
+
const context = useContext(AuthContext);
|
116 |
+
if (!context) {
|
117 |
+
throw new Error('useAuth must be used within an AuthProvider');
|
118 |
+
}
|
119 |
+
return context;
|
120 |
+
};
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// src/hooks/useFeedback.js
|
2 |
+
import { useState, useCallback } from 'react';
|
3 |
+
import { saveFeedback } from '../services/api';
|
4 |
+
import { useAuth } from './useAuth';
|
5 |
+
|
6 |
+
export const useFeedback = () => {
|
7 |
+
const { authTokens } = useAuth();
|
8 |
+
const [isSaving, setIsSaving] = useState(false);
|
9 |
+
const [saveError, setSaveError] = useState(null);
|
10 |
+
const [saveSuccess, setSaveSuccess] = useState(false);
|
11 |
+
|
12 |
+
const submitFeedback = useCallback(async (feedbackData) => {
|
13 |
+
setIsSaving(true);
|
14 |
+
setSaveError(null);
|
15 |
+
setSaveSuccess(false);
|
16 |
+
|
17 |
+
try {
|
18 |
+
await saveFeedback(feedbackData, authTokens.access);
|
19 |
+
setSaveSuccess(true);
|
20 |
+
} catch (err) {
|
21 |
+
setSaveError(err.message || 'Failed to save feedback');
|
22 |
+
} finally {
|
23 |
+
setIsSaving(false);
|
24 |
+
}
|
25 |
+
}, [authTokens.access]);
|
26 |
+
|
27 |
+
const resetFeedbackState = useCallback(() => {
|
28 |
+
setSaveError(null);
|
29 |
+
setSaveSuccess(false);
|
30 |
+
}, []);
|
31 |
+
|
32 |
+
return {
|
33 |
+
isSaving,
|
34 |
+
saveError,
|
35 |
+
saveSuccess,
|
36 |
+
submitFeedback,
|
37 |
+
resetFeedbackState,
|
38 |
+
};
|
39 |
+
};
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// src/hooks/useLocalStorage.js
|
2 |
+
import { useState } from 'react';
|
3 |
+
|
4 |
+
export const useLocalStorage = (key, initialValue) => {
|
5 |
+
const [storedValue, setStoredValue] = useState(() => {
|
6 |
+
try {
|
7 |
+
const item = window.localStorage.getItem(key);
|
8 |
+
return item ? JSON.parse(item) : initialValue;
|
9 |
+
} catch (error) {
|
10 |
+
console.error(error);
|
11 |
+
return initialValue;
|
12 |
+
}
|
13 |
+
});
|
14 |
+
|
15 |
+
const setValue = (value) => {
|
16 |
+
try {
|
17 |
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
18 |
+
setStoredValue(valueToStore);
|
19 |
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
20 |
+
} catch (error) {
|
21 |
+
console.error(error);
|
22 |
+
}
|
23 |
+
};
|
24 |
+
|
25 |
+
return [storedValue, setValue];
|
26 |
+
};
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// src/hooks/useSearch.js
|
2 |
+
import { useState, useCallback } from 'react';
|
3 |
+
import { searchQuery } from '../services/search';
|
4 |
+
import { useAuth } from './useAuth';
|
5 |
+
|
6 |
+
export const useSearch = () => {
|
7 |
+
const { authTokens } = useAuth();
|
8 |
+
const [results, setResults] = useState([]);
|
9 |
+
const [isLoading, setIsLoading] = useState(false);
|
10 |
+
const [error, setError] = useState(null);
|
11 |
+
|
12 |
+
const handleSearch = useCallback(async (query) => {
|
13 |
+
if (!query.trim()) {
|
14 |
+
setResults([]);
|
15 |
+
return;
|
16 |
+
}
|
17 |
+
|
18 |
+
setIsLoading(true);
|
19 |
+
setError(null);
|
20 |
+
|
21 |
+
try {
|
22 |
+
const searchResults = await searchQuery(query, authTokens.access);
|
23 |
+
|
24 |
+
// Combine and sort results by similarity (highest first)
|
25 |
+
const sortedResults = [...searchResults].sort((a, b) => b.similarity - a.similarity);
|
26 |
+
|
27 |
+
setResults(sortedResults);
|
28 |
+
} catch (err) {
|
29 |
+
setError(err.message || 'Failed to perform search');
|
30 |
+
setResults([]);
|
31 |
+
} finally {
|
32 |
+
setIsLoading(false);
|
33 |
+
}
|
34 |
+
}, [authTokens.access]);
|
35 |
+
|
36 |
+
const clearResults = useCallback(() => {
|
37 |
+
setResults([]);
|
38 |
+
setError(null);
|
39 |
+
}, []);
|
40 |
+
|
41 |
+
return {
|
42 |
+
results,
|
43 |
+
isLoading,
|
44 |
+
error,
|
45 |
+
handleSearch,
|
46 |
+
clearResults,
|
47 |
+
};
|
48 |
+
};
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect, useCallback, createContext, useContext } from 'react';
|
2 |
+
|
3 |
+
// Create a context for theme
|
4 |
+
export const ThemeContext = createContext();
|
5 |
+
|
6 |
+
export const useTheme = () => {
|
7 |
+
const context = useContext(ThemeContext);
|
8 |
+
if (!context) {
|
9 |
+
throw new Error('useTheme must be used within a ThemeProvider');
|
10 |
+
}
|
11 |
+
return context;
|
12 |
+
};
|
13 |
+
|
14 |
+
const useThemeState = () => {
|
15 |
+
const [isDark, setIsDark] = useState(() => {
|
16 |
+
// Check localStorage for saved preference
|
17 |
+
const savedMode = localStorage.getItem('themeMode');
|
18 |
+
if (savedMode) return savedMode === 'dark';
|
19 |
+
|
20 |
+
// Fallback to system preference
|
21 |
+
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
22 |
+
});
|
23 |
+
const [themeLoading, setThemeLoading] = useState(true);
|
24 |
+
|
25 |
+
useEffect(() => {
|
26 |
+
// Apply the theme class to document body
|
27 |
+
if (isDark) {
|
28 |
+
document.body.classList.add('dark-mode');
|
29 |
+
document.body.classList.remove('light-mode');
|
30 |
+
} else {
|
31 |
+
document.body.classList.add('light-mode');
|
32 |
+
document.body.classList.remove('dark-mode');
|
33 |
+
}
|
34 |
+
|
35 |
+
// Save preference to localStorage
|
36 |
+
localStorage.setItem('themeMode', isDark ? 'dark' : 'light');
|
37 |
+
setThemeLoading(false);
|
38 |
+
}, [isDark]);
|
39 |
+
|
40 |
+
const toggleTheme = useCallback(() => {
|
41 |
+
setIsDark(prev => !prev);
|
42 |
+
}, []);
|
43 |
+
|
44 |
+
return { isDark, toggleTheme, themeLoading };
|
45 |
+
};
|
46 |
+
|
47 |
+
// Theme provider component
|
48 |
+
export const ThemeProvider = ({ children }) => {
|
49 |
+
const themeState = useThemeState();
|
50 |
+
return (
|
51 |
+
<ThemeContext.Provider value={themeState}>
|
52 |
+
{children}
|
53 |
+
</ThemeContext.Provider>
|
54 |
+
);
|
55 |
+
};
|
56 |
+
|
57 |
+
export default useThemeState;
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { createRoot } from 'react-dom/client';
|
3 |
+
import { BrowserRouter } from 'react-router-dom';
|
4 |
+
import App from './App';
|
5 |
+
import GlobalStyles from './styles/GlobalStyles';
|
6 |
+
import './styles/fonts.css';
|
7 |
+
import ErrorBoundary from './components/UI/ErrorBoundary';
|
8 |
+
import { ThemeProvider as ThemeStateProvider } from './hooks/useTheme';
|
9 |
+
import { ThemeProvider as MuiThemeProvider } from './styles/theme';
|
10 |
+
|
11 |
+
const container = document.getElementById('root');
|
12 |
+
const root = createRoot(container);
|
13 |
+
|
14 |
+
root.render(
|
15 |
+
<React.StrictMode>
|
16 |
+
<ErrorBoundary>
|
17 |
+
<BrowserRouter>
|
18 |
+
<ThemeStateProvider>
|
19 |
+
<MuiThemeProvider>
|
20 |
+
<GlobalStyles />
|
21 |
+
<App />
|
22 |
+
</MuiThemeProvider>
|
23 |
+
</ThemeStateProvider>
|
24 |
+
</BrowserRouter>
|
25 |
+
</ErrorBoundary>
|
26 |
+
</React.StrictMode>
|
27 |
+
);
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Container, Typography, Box, Link } from '@mui/material';
|
2 |
+
import { styled } from '@mui/system';
|
3 |
+
|
4 |
+
const Section = styled('section')(({ theme }) => ({
|
5 |
+
marginBottom: theme.spacing(6),
|
6 |
+
}));
|
7 |
+
|
8 |
+
const About = () => {
|
9 |
+
return (
|
10 |
+
<Container maxWidth="md" sx={{ py: 4 }}>
|
11 |
+
<Section>
|
12 |
+
<Typography variant="h3" component="h1" gutterBottom sx={{ fontWeight: 700 }}>
|
13 |
+
About EnlightenQalb
|
14 |
+
</Typography>
|
15 |
+
<Typography variant="body1" paragraph>
|
16 |
+
EnlightenQalb (Enlighten the Heart) is a project that brings the timeless wisdom of
|
17 |
+
Imam Al-Ghazali to the modern age through advanced semantic search technology.
|
18 |
+
</Typography>
|
19 |
+
</Section>
|
20 |
+
|
21 |
+
<Section>
|
22 |
+
<Typography variant="h4" component="h2" gutterBottom sx={{ fontWeight: 600 }}>
|
23 |
+
The Technology
|
24 |
+
</Typography>
|
25 |
+
<Typography variant="body1" paragraph>
|
26 |
+
Our platform uses state-of-the-art sentence embedding models (UAE-Large and BGE-Large)
|
27 |
+
to find the most relevant passages from Al-Ghazali's "The Alchemy of Happiness"
|
28 |
+
in response to your spiritual queries.
|
29 |
+
</Typography>
|
30 |
+
<Typography variant="body1" paragraph>
|
31 |
+
The backend employs cosine similarity to match your questions with the most appropriate
|
32 |
+
sections of the text, providing you with direct access to the Imam's guidance.
|
33 |
+
</Typography>
|
34 |
+
</Section>
|
35 |
+
|
36 |
+
<Section>
|
37 |
+
<Typography variant="h4" component="h2" gutterBottom sx={{ fontWeight: 600 }}>
|
38 |
+
The Book
|
39 |
+
</Typography>
|
40 |
+
<Typography variant="body1" paragraph>
|
41 |
+
"The Alchemy of Happiness" (Kimiya-yi Sa'ādat) is a Persian work by the famous
|
42 |
+
Islamic theologian and philosopher Imam Abu Hamid al-Ghazali. The book emphasizes
|
43 |
+
the importance of observing the ritual requirements of Islam and the actions that
|
44 |
+
would lead to happiness in the afterlife.
|
45 |
+
</Typography>
|
46 |
+
</Section>
|
47 |
+
|
48 |
+
<Section>
|
49 |
+
<Typography variant="h4" component="h2" gutterBottom sx={{ fontWeight: 600 }}>
|
50 |
+
The Team
|
51 |
+
</Typography>
|
52 |
+
<Typography variant="body1" paragraph>
|
53 |
+
EnlightenQalb is developed by HumbleBeeAI, a team passionate about making classical
|
54 |
+
Islamic knowledge more accessible through modern technology.
|
55 |
+
</Typography>
|
56 |
+
</Section>
|
57 |
+
|
58 |
+
<Box sx={{ mt: 4 }}>
|
59 |
+
<Typography variant="body2">
|
60 |
+
For more information about the technical implementation, visit our{' '}
|
61 |
+
<Link href="https://github.com/humblebeeintel/gazzoliy-therapy" target="_blank">
|
62 |
+
GitHub repository
|
63 |
+
</Link>.
|
64 |
+
</Typography>
|
65 |
+
</Box>
|
66 |
+
</Container>
|
67 |
+
);
|
68 |
+
};
|
69 |
+
|
70 |
+
export default About;
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from 'react';
|
2 |
+
import { useNavigate } from 'react-router-dom';
|
3 |
+
import { useAuth } from '../hooks/useAuth';
|
4 |
+
import { useSearch } from '../hooks/useSearch';
|
5 |
+
import SearchForm from '../components/Search/SearchForm';
|
6 |
+
import SearchResults from '../components/Search/SearchResults';
|
7 |
+
import FeedbackForm from '../components/Search/FeedbackForm';
|
8 |
+
import LoadingSpinner from '../components/UI/LoadingSpinner';
|
9 |
+
import ErrorMessage from '../components/UI/ErrorMessage';
|
10 |
+
import { Box, Typography, Container } from '@mui/material';
|
11 |
+
|
12 |
+
const Dashboard = () => {
|
13 |
+
const { isAuthenticated, user } = useAuth();
|
14 |
+
const {
|
15 |
+
searchQuery,
|
16 |
+
results,
|
17 |
+
loading,
|
18 |
+
error,
|
19 |
+
handleSearch,
|
20 |
+
handleFeedbackSubmit
|
21 |
+
} = useSearch();
|
22 |
+
const [selectedResult, setSelectedResult] = useState(null);
|
23 |
+
const navigate = useNavigate();
|
24 |
+
|
25 |
+
useEffect(() => {
|
26 |
+
if (!isAuthenticated) {
|
27 |
+
navigate('/login');
|
28 |
+
}
|
29 |
+
}, [isAuthenticated, navigate]);
|
30 |
+
|
31 |
+
const handleResultSelect = (result) => {
|
32 |
+
setSelectedResult(result);
|
33 |
+
};
|
34 |
+
|
35 |
+
if (!isAuthenticated) {
|
36 |
+
return null;
|
37 |
+
}
|
38 |
+
|
39 |
+
return (
|
40 |
+
<Container maxWidth="lg" sx={{ py: 4 }}>
|
41 |
+
<Typography variant="h4" component="h1" gutterBottom sx={{ fontWeight: 600 }}>
|
42 |
+
Welcome, {user}
|
43 |
+
</Typography>
|
44 |
+
|
45 |
+
<Box sx={{ mb: 4 }}>
|
46 |
+
<SearchForm onSearch={handleSearch} />
|
47 |
+
</Box>
|
48 |
+
|
49 |
+
{loading && <LoadingSpinner />}
|
50 |
+
{error && <ErrorMessage message={error} />}
|
51 |
+
|
52 |
+
{results && (
|
53 |
+
<Box sx={{ mb: 4 }}>
|
54 |
+
<SearchResults
|
55 |
+
results={results}
|
56 |
+
query={searchQuery}
|
57 |
+
onSelect={handleResultSelect}
|
58 |
+
/>
|
59 |
+
</Box>
|
60 |
+
)}
|
61 |
+
|
62 |
+
{selectedResult && (
|
63 |
+
<Box sx={{ mb: 4 }}>
|
64 |
+
<FeedbackForm
|
65 |
+
searchResult={selectedResult}
|
66 |
+
query={searchQuery}
|
67 |
+
onFeedbackSubmitted={handleFeedbackSubmit}
|
68 |
+
/>
|
69 |
+
</Box>
|
70 |
+
)}
|
71 |
+
</Container>
|
72 |
+
);
|
73 |
+
};
|
74 |
+
|
75 |
+
export default Dashboard;
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect } from 'react';
|
2 |
+
import { useNavigate } from 'react-router-dom';
|
3 |
+
import { useAuth } from '../hooks/useAuth';
|
4 |
+
import { Button } from '@mui/material';
|
5 |
+
import { styled } from '@mui/system';
|
6 |
+
|
7 |
+
const HeroSection = styled('section')(({ theme }) => ({
|
8 |
+
minHeight: '80vh',
|
9 |
+
display: 'flex',
|
10 |
+
flexDirection: 'column',
|
11 |
+
justifyContent: 'center',
|
12 |
+
alignItems: 'center',
|
13 |
+
textAlign: 'center',
|
14 |
+
padding: theme.spacing(4),
|
15 |
+
background: theme.palette.background.paper,
|
16 |
+
color: theme.palette.text.primary,
|
17 |
+
}));
|
18 |
+
|
19 |
+
const Title = styled('h1')(({ theme }) => ({
|
20 |
+
fontSize: '3rem',
|
21 |
+
marginBottom: theme.spacing(2),
|
22 |
+
fontWeight: 700,
|
23 |
+
color: theme.palette.primary.main,
|
24 |
+
}));
|
25 |
+
|
26 |
+
const Subtitle = styled('p')(({ theme }) => ({
|
27 |
+
fontSize: '1.5rem',
|
28 |
+
marginBottom: theme.spacing(4),
|
29 |
+
maxWidth: '800px',
|
30 |
+
}));
|
31 |
+
|
32 |
+
const CTAButton = styled(Button)(({ theme }) => ({
|
33 |
+
padding: theme.spacing(1.5, 4),
|
34 |
+
fontSize: '1.1rem',
|
35 |
+
borderRadius: '50px',
|
36 |
+
fontWeight: 600,
|
37 |
+
}));
|
38 |
+
|
39 |
+
const Home = () => {
|
40 |
+
const { isAuthenticated } = useAuth();
|
41 |
+
const navigate = useNavigate();
|
42 |
+
|
43 |
+
useEffect(() => {
|
44 |
+
if (isAuthenticated) {
|
45 |
+
navigate('/dashboard');
|
46 |
+
}
|
47 |
+
}, [isAuthenticated, navigate]);
|
48 |
+
|
49 |
+
return (
|
50 |
+
<HeroSection>
|
51 |
+
<Title>EnlightenQalb</Title>
|
52 |
+
<Subtitle>
|
53 |
+
Explore the wisdom of Imam Al-Ghazali through modern semantic search technology.
|
54 |
+
Discover relevant passages from "The Alchemy of Happiness" for your spiritual queries.
|
55 |
+
</Subtitle>
|
56 |
+
<CTAButton
|
57 |
+
variant="contained"
|
58 |
+
color="primary"
|
59 |
+
size="large"
|
60 |
+
onClick={() => navigate('/login')}
|
61 |
+
>
|
62 |
+
Begin Your Journey
|
63 |
+
</CTAButton>
|
64 |
+
</HeroSection>
|
65 |
+
);
|
66 |
+
};
|
67 |
+
|
68 |
+
export default Home;
|
@@ -1,7 +1,7 @@
|
|
1 |
import axios from 'axios';
|
2 |
|
3 |
// Replace with your actual HF Space backend URL
|
4 |
-
const API_BASE_URL = process.env.REACT_APP_API_URL || 'https://
|
5 |
|
6 |
const api = axios.create({
|
7 |
baseURL: API_BASE_URL,
|
@@ -10,18 +10,22 @@ const api = axios.create({
|
|
10 |
},
|
11 |
});
|
12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
// Request interceptor to add auth token
|
14 |
api.interceptors.request.use(
|
15 |
(config) => {
|
16 |
-
const
|
17 |
-
if (
|
18 |
-
config.headers['Authorization'] = `Bearer ${
|
19 |
}
|
20 |
return config;
|
21 |
},
|
22 |
-
(error) =>
|
23 |
-
return Promise.reject(error);
|
24 |
-
}
|
25 |
);
|
26 |
|
27 |
// Response interceptor to handle token refresh
|
@@ -29,31 +33,33 @@ api.interceptors.response.use(
|
|
29 |
(response) => response,
|
30 |
async (error) => {
|
31 |
const originalRequest = error.config;
|
32 |
-
|
33 |
-
if (error.response.status === 401 && !originalRequest._retry) {
|
34 |
originalRequest._retry = true;
|
35 |
-
|
36 |
try {
|
37 |
-
const
|
38 |
-
if (!
|
39 |
-
|
40 |
const response = await axios.post(`${API_BASE_URL}/refresh`, {
|
41 |
-
refresh_token:
|
42 |
});
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
|
|
|
|
|
|
48 |
return api(originalRequest);
|
49 |
} catch (refreshError) {
|
50 |
-
localStorage.removeItem('
|
51 |
-
localStorage.removeItem('refresh_token');
|
52 |
window.location.href = '/';
|
53 |
return Promise.reject(refreshError);
|
54 |
}
|
55 |
}
|
56 |
-
|
57 |
return Promise.reject(error);
|
58 |
}
|
59 |
);
|
|
|
1 |
import axios from 'axios';
|
2 |
|
3 |
// Replace with your actual HF Space backend URL
|
4 |
+
const API_BASE_URL = process.env.REACT_APP_API_URL || 'https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space';
|
5 |
|
6 |
const api = axios.create({
|
7 |
baseURL: API_BASE_URL,
|
|
|
10 |
},
|
11 |
});
|
12 |
|
13 |
+
// Helper to get tokens from localStorage
|
14 |
+
function getAuthTokens() {
|
15 |
+
const tokens = localStorage.getItem('authTokens');
|
16 |
+
return tokens ? JSON.parse(tokens) : null;
|
17 |
+
}
|
18 |
+
|
19 |
// Request interceptor to add auth token
|
20 |
api.interceptors.request.use(
|
21 |
(config) => {
|
22 |
+
const tokens = getAuthTokens();
|
23 |
+
if (tokens?.access) {
|
24 |
+
config.headers['Authorization'] = `Bearer ${tokens.access}`;
|
25 |
}
|
26 |
return config;
|
27 |
},
|
28 |
+
(error) => Promise.reject(error)
|
|
|
|
|
29 |
);
|
30 |
|
31 |
// Response interceptor to handle token refresh
|
|
|
33 |
(response) => response,
|
34 |
async (error) => {
|
35 |
const originalRequest = error.config;
|
36 |
+
|
37 |
+
if (error.response && error.response.status === 401 && !originalRequest._retry) {
|
38 |
originalRequest._retry = true;
|
39 |
+
|
40 |
try {
|
41 |
+
const tokens = getAuthTokens();
|
42 |
+
if (!tokens?.refresh) throw new Error('No refresh token');
|
43 |
+
|
44 |
const response = await axios.post(`${API_BASE_URL}/refresh`, {
|
45 |
+
refresh_token: tokens.refresh
|
46 |
});
|
47 |
+
|
48 |
+
const newTokens = {
|
49 |
+
access: response.data.access_token,
|
50 |
+
refresh: response.data.refresh_token
|
51 |
+
};
|
52 |
+
localStorage.setItem('authTokens', JSON.stringify(newTokens));
|
53 |
+
|
54 |
+
originalRequest.headers['Authorization'] = `Bearer ${newTokens.access}`;
|
55 |
return api(originalRequest);
|
56 |
} catch (refreshError) {
|
57 |
+
localStorage.removeItem('authTokens');
|
|
|
58 |
window.location.href = '/';
|
59 |
return Promise.reject(refreshError);
|
60 |
}
|
61 |
}
|
62 |
+
|
63 |
return Promise.reject(error);
|
64 |
}
|
65 |
);
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// src/services/auth.js
|
2 |
+
import axios from './api';
|
3 |
+
|
4 |
+
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space';
|
5 |
+
|
6 |
+
export const login = async (username, password) => {
|
7 |
+
try {
|
8 |
+
const response = await axios.post(
|
9 |
+
`${API_BASE_URL}/login`,
|
10 |
+
new URLSearchParams({
|
11 |
+
username,
|
12 |
+
password,
|
13 |
+
grant_type: 'password'
|
14 |
+
}),
|
15 |
+
{
|
16 |
+
headers: {
|
17 |
+
'Content-Type': 'application/x-www-form-urlencoded'
|
18 |
+
}
|
19 |
+
}
|
20 |
+
);
|
21 |
+
|
22 |
+
return {
|
23 |
+
access_token: response.data.access_token,
|
24 |
+
refresh_token: response.data.refresh_token,
|
25 |
+
token_type: response.data.token_type
|
26 |
+
};
|
27 |
+
} catch (error) {
|
28 |
+
if (error.response) {
|
29 |
+
throw new Error(
|
30 |
+
error.response.data.detail ||
|
31 |
+
error.response.data.message ||
|
32 |
+
'Login failed'
|
33 |
+
);
|
34 |
+
}
|
35 |
+
throw new Error('Network error. Please try again.');
|
36 |
+
}
|
37 |
+
};
|
38 |
+
|
39 |
+
export const logout = async (accessToken) => {
|
40 |
+
try {
|
41 |
+
await axios.post(
|
42 |
+
`${API_BASE_URL}/logout`,
|
43 |
+
{},
|
44 |
+
{
|
45 |
+
headers: {
|
46 |
+
Authorization: `Bearer ${accessToken}`
|
47 |
+
}
|
48 |
+
}
|
49 |
+
);
|
50 |
+
} catch (error) {
|
51 |
+
console.error('Logout error:', error);
|
52 |
+
// Proceed with client-side cleanup regardless
|
53 |
+
}
|
54 |
+
};
|
55 |
+
|
56 |
+
export const refreshToken = async (refreshToken) => {
|
57 |
+
try {
|
58 |
+
const response = await axios.post(
|
59 |
+
`${API_BASE_URL}/refresh`,
|
60 |
+
{ refresh_token: refreshToken }
|
61 |
+
);
|
62 |
+
|
63 |
+
return {
|
64 |
+
access_token: response.data.access_token,
|
65 |
+
refresh_token: response.data.refresh_token || refreshToken, // Fallback to existing if not provided
|
66 |
+
token_type: response.data.token_type || 'bearer'
|
67 |
+
};
|
68 |
+
} catch (error) {
|
69 |
+
if (error.response) {
|
70 |
+
throw new Error(
|
71 |
+
error.response.data.detail ||
|
72 |
+
'Session expired. Please login again.'
|
73 |
+
);
|
74 |
+
}
|
75 |
+
throw new Error('Network error during token refresh');
|
76 |
+
}
|
77 |
+
};
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import axios from './api';
|
2 |
+
|
3 |
+
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space';
|
4 |
+
|
5 |
+
export const searchQuery = async (query) => {
|
6 |
+
try {
|
7 |
+
const response = await axios.post(`${API_BASE_URL}/search`, { query });
|
8 |
+
return response.data;
|
9 |
+
} catch (error) {
|
10 |
+
throw new Error(error.response?.data?.detail || 'Search failed');
|
11 |
+
}
|
12 |
+
};
|
13 |
+
|
14 |
+
export const saveFeedback = async (feedbackData, token) => {
|
15 |
+
try {
|
16 |
+
await axios.post(`${API_BASE_URL}/save`,
|
17 |
+
{ items: [feedbackData] },
|
18 |
+
{
|
19 |
+
headers: {
|
20 |
+
Authorization: `Bearer ${token}`
|
21 |
+
}
|
22 |
+
}
|
23 |
+
);
|
24 |
+
} catch (error) {
|
25 |
+
throw new Error(error.response?.data?.detail || 'Failed to save feedback');
|
26 |
+
}
|
27 |
+
};
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { GlobalStyles as MuiGlobalStyles } from '@mui/material';
|
2 |
+
import { fadeIn } from './animations'; // Import the missing fadeIn animation
|
3 |
+
|
4 |
+
const GlobalStyles = () => (
|
5 |
+
<MuiGlobalStyles
|
6 |
+
styles={(theme) => ({
|
7 |
+
html: {
|
8 |
+
scrollBehavior: 'smooth',
|
9 |
+
},
|
10 |
+
body: {
|
11 |
+
margin: 0,
|
12 |
+
padding: 0,
|
13 |
+
transition: 'all 0.3s ease',
|
14 |
+
backgroundColor: theme.palette.background.default,
|
15 |
+
color: theme.palette.text.primary,
|
16 |
+
fontFamily: theme.typography.fontFamily,
|
17 |
+
lineHeight: '1.6',
|
18 |
+
'&.dark-mode': {
|
19 |
+
backgroundColor: theme.palette.background.default,
|
20 |
+
color: theme.palette.text.primary,
|
21 |
+
},
|
22 |
+
'&.light-mode': {
|
23 |
+
backgroundColor: theme.palette.background.default,
|
24 |
+
color: theme.palette.text.primary,
|
25 |
+
},
|
26 |
+
},
|
27 |
+
a: {
|
28 |
+
color: theme.palette.primary.main,
|
29 |
+
textDecoration: 'none',
|
30 |
+
transition: 'color 0.3s ease',
|
31 |
+
'&:hover': {
|
32 |
+
textDecoration: 'underline',
|
33 |
+
color: theme.palette.primary.dark,
|
34 |
+
},
|
35 |
+
},
|
36 |
+
'h1, h2, h3, h4, h5, h6': {
|
37 |
+
fontWeight: 700,
|
38 |
+
marginTop: theme.spacing(3),
|
39 |
+
marginBottom: theme.spacing(2),
|
40 |
+
},
|
41 |
+
'::selection': {
|
42 |
+
backgroundColor: theme.palette.primary.main,
|
43 |
+
color: theme.palette.primary.contrastText,
|
44 |
+
},
|
45 |
+
'.spiritual-highlight': {
|
46 |
+
position: 'relative',
|
47 |
+
'&::after': {
|
48 |
+
content: '""',
|
49 |
+
position: 'absolute',
|
50 |
+
bottom: -2,
|
51 |
+
left: 0,
|
52 |
+
width: '100%',
|
53 |
+
height: 2,
|
54 |
+
background: `linear-gradient(90deg, ${theme.palette.primary.main}, ${theme.palette.secondary.main})`,
|
55 |
+
transform: 'scaleX(0)',
|
56 |
+
transition: 'transform 0.3s ease',
|
57 |
+
},
|
58 |
+
'&:hover::after': {
|
59 |
+
transform: 'scaleX(1)',
|
60 |
+
},
|
61 |
+
},
|
62 |
+
'.page-transition': {
|
63 |
+
animation: `${fadeIn} 0.5s ease forwards`,
|
64 |
+
},
|
65 |
+
})}
|
66 |
+
/>
|
67 |
+
);
|
68 |
+
|
69 |
+
export default GlobalStyles;
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { keyframes } from '@emotion/react';
|
2 |
+
|
3 |
+
export const fadeIn = keyframes`
|
4 |
+
from {
|
5 |
+
opacity: 0;
|
6 |
+
}
|
7 |
+
to {
|
8 |
+
opacity: 1;
|
9 |
+
}
|
10 |
+
`;
|
11 |
+
|
12 |
+
export const slideUp = keyframes`
|
13 |
+
from {
|
14 |
+
transform: translateY(20px);
|
15 |
+
opacity: 0;
|
16 |
+
}
|
17 |
+
to {
|
18 |
+
transform: translateY(0);
|
19 |
+
opacity: 1;
|
20 |
+
}
|
21 |
+
`;
|
22 |
+
|
23 |
+
export const pulse = keyframes`
|
24 |
+
0% {
|
25 |
+
transform: scale(1);
|
26 |
+
}
|
27 |
+
50% {
|
28 |
+
transform: scale(1.05);
|
29 |
+
}
|
30 |
+
100% {
|
31 |
+
transform: scale(1);
|
32 |
+
}
|
33 |
+
`;
|
34 |
+
|
35 |
+
export const spiritualGlow = keyframes`
|
36 |
+
0% {
|
37 |
+
box-shadow: 0 0 5px rgba(63, 81, 181, 0.5);
|
38 |
+
}
|
39 |
+
50% {
|
40 |
+
box-shadow: 0 0 20px rgba(63, 81, 181, 0.8);
|
41 |
+
}
|
42 |
+
100% {
|
43 |
+
box-shadow: 0 0 5px rgba(63, 81, 181, 0.5);
|
44 |
+
}
|
45 |
+
`;
|
46 |
+
|
47 |
+
export const pageTransition = {
|
48 |
+
enter: {
|
49 |
+
opacity: 0,
|
50 |
+
animation: `${fadeIn} 0.5s ease forwards`,
|
51 |
+
},
|
52 |
+
exit: {
|
53 |
+
opacity: 1,
|
54 |
+
animation: `${fadeIn} 0.5s ease reverse`,
|
55 |
+
},
|
56 |
+
};
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
|
2 |
+
@import url('https://fonts.googleapis.com/css2?family=Amiri:wght@400;700&display=swap');
|
3 |
+
|
4 |
+
/* Arabic font for Quranic/hadith references */
|
5 |
+
@font-face {
|
6 |
+
font-family: 'Traditional Arabic';
|
7 |
+
font-weight: normal;
|
8 |
+
src: local('Traditional Arabic'),
|
9 |
+
url('https://fonts.cdnfonts.com/css/traditional-arabic') format('truetype');
|
10 |
+
}
|
11 |
+
|
12 |
+
body {
|
13 |
+
font-family: 'Poppins', sans-serif;
|
14 |
+
}
|
15 |
+
|
16 |
+
.arabic-text {
|
17 |
+
font-family: 'Traditional Arabic', 'Amiri', serif;
|
18 |
+
font-size: 1.5rem;
|
19 |
+
line-height: 2.5rem;
|
20 |
+
direction: rtl;
|
21 |
+
}
|
22 |
+
|
23 |
+
.spiritual-quote {
|
24 |
+
font-family: 'Amiri', serif;
|
25 |
+
font-size: 1.2rem;
|
26 |
+
line-height: 2rem;
|
27 |
+
font-style: italic;
|
28 |
+
}
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { css } from '@emotion/react';
|
2 |
+
|
3 |
+
export const spiritualCard = css`
|
4 |
+
position: relative;
|
5 |
+
padding: 2rem;
|
6 |
+
border-radius: 12px;
|
7 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
8 |
+
transition: all 0.3s ease;
|
9 |
+
&:hover {
|
10 |
+
transform: translateY(-5px);
|
11 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
12 |
+
}
|
13 |
+
`;
|
14 |
+
|
15 |
+
export const quoteStyle = (theme) => css`
|
16 |
+
padding: 1.5rem;
|
17 |
+
border-left: 4px solid ${theme.palette.spiritual.quoteBorder};
|
18 |
+
background-color: ${theme.palette.background.paper};
|
19 |
+
font-family: 'Amiri', serif;
|
20 |
+
font-size: 1.2rem;
|
21 |
+
line-height: 2rem;
|
22 |
+
margin: 1.5rem 0;
|
23 |
+
border-radius: 0 8px 8px 0;
|
24 |
+
`;
|
25 |
+
|
26 |
+
export const arabicTextStyle = css`
|
27 |
+
font-family: 'Traditional Arabic', 'Amiri', serif;
|
28 |
+
font-size: 1.5rem;
|
29 |
+
line-height: 2.5rem;
|
30 |
+
direction: rtl;
|
31 |
+
text-align: right;
|
32 |
+
`;
|
33 |
+
|
34 |
+
export const fadeInAnimation = css`
|
35 |
+
animation: fadeIn 0.8s ease forwards;
|
36 |
+
`;
|
37 |
+
|
38 |
+
export const prayerMatStyle = (theme) => css`
|
39 |
+
background: linear-gradient(145deg, ${theme.palette.spiritual.light} 0%, ${theme.palette.spiritual.dark} 100%);
|
40 |
+
padding: 2rem;
|
41 |
+
border-radius: 0 0 20px 20px;
|
42 |
+
position: relative;
|
43 |
+
&:before {
|
44 |
+
content: '';
|
45 |
+
position: absolute;
|
46 |
+
top: 10px;
|
47 |
+
left: 50%;
|
48 |
+
transform: translateX(-50%);
|
49 |
+
width: 60%;
|
50 |
+
height: 3px;
|
51 |
+
background: ${theme.palette.spiritual.accent};
|
52 |
+
border-radius: 3px;
|
53 |
+
}
|
54 |
+
`;
|
55 |
+
|
56 |
+
export const illuminationEffect = css`
|
57 |
+
position: relative;
|
58 |
+
&:after {
|
59 |
+
content: '';
|
60 |
+
position: absolute;
|
61 |
+
top: -10px;
|
62 |
+
left: -10px;
|
63 |
+
right: -10px;
|
64 |
+
bottom: -10px;
|
65 |
+
background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 70%);
|
66 |
+
z-index: -1;
|
67 |
+
opacity: 0;
|
68 |
+
transition: opacity 0.3s ease;
|
69 |
+
}
|
70 |
+
&:hover:after {
|
71 |
+
opacity: 1;
|
72 |
+
}
|
73 |
+
`;
|
@@ -1,64 +1,25 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
spacing: {
|
27 |
-
small: '0.5rem',
|
28 |
-
medium: '1rem',
|
29 |
-
large: '1.5rem',
|
30 |
-
xlarge: '2rem',
|
31 |
-
},
|
32 |
-
radii: {
|
33 |
-
small: '4px',
|
34 |
-
medium: '8px',
|
35 |
-
large: '16px',
|
36 |
-
},
|
37 |
-
shadows: {
|
38 |
-
small: '0 2px 4px rgba(0,0,0,0.1)',
|
39 |
-
medium: '0 4px 8px rgba(0,0,0,0.1)',
|
40 |
-
large: '0 8px 16px rgba(0,0,0,0.1)',
|
41 |
-
},
|
42 |
-
breakpoints: {
|
43 |
-
mobile: '576px',
|
44 |
-
tablet: '768px',
|
45 |
-
desktop: '992px',
|
46 |
-
},
|
47 |
-
};
|
48 |
-
|
49 |
-
export const darkTheme = {
|
50 |
-
...lightTheme,
|
51 |
-
colors: {
|
52 |
-
primary: '#6b8cae',
|
53 |
-
secondary: '#4a6fa5',
|
54 |
-
accent1: '#4fc3a1',
|
55 |
-
accent2: '#166088',
|
56 |
-
background: '#1a1a2e',
|
57 |
-
cardBg: '#16213e',
|
58 |
-
text: '#e6e6e6',
|
59 |
-
muted: '#aaaaaa',
|
60 |
-
disabled: '#555555',
|
61 |
-
error: '#ff6b6b',
|
62 |
-
success: '#51cf66',
|
63 |
-
},
|
64 |
-
};
|
|
|
1 |
+
import { createTheme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
|
2 |
+
import React, { useMemo } from 'react';
|
3 |
+
import useTheme from '../hooks/useTheme';
|
4 |
+
|
5 |
+
const getTheme = (isDark) => createTheme({
|
6 |
+
palette: {
|
7 |
+
mode: isDark ? 'dark' : 'light',
|
8 |
+
primary: {
|
9 |
+
main: isDark ? '#7986cb' : '#3f51b5',
|
10 |
+
},
|
11 |
+
background: {
|
12 |
+
default: isDark ? '#121212' : '#f9f9f9',
|
13 |
+
},
|
14 |
+
},
|
15 |
+
});
|
16 |
+
|
17 |
+
export const ThemeProvider = ({ children }) => {
|
18 |
+
const { isDark } = useTheme();
|
19 |
+
|
20 |
+
const theme = useMemo(() => getTheme(isDark), [isDark]);
|
21 |
+
|
22 |
+
return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>;
|
23 |
+
};
|
24 |
+
|
25 |
+
export default getTheme;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|