eli02 commited on
Commit
3299552
·
1 Parent(s): 9b06cac

feat: Implement MainLayout component for consistent layout structure

Browse files

feat: 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 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d5990b52b6a2c9a5cd8fe71c63b5f1eac65f45b6f8b2d2c8517180df35851d80
3
+ size 676451
package.json CHANGED
@@ -1,40 +1,3 @@
1
- {
2
- "name": "enlightenqalb-frontend",
3
- "version": "1.0.0",
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
public/index.html CHANGED
@@ -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>
src/App.js CHANGED
@@ -1,30 +1,63 @@
1
- import React from 'react';
2
- import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
3
- import { ThemeProvider } from 'styled-components';
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 { GlobalStyles } from './styles/GlobalStyles';
10
- import theme from './styles/theme';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  function App() {
 
 
 
 
 
 
13
  return (
14
- <ThemeProvider theme={theme}>
15
- <GlobalStyles />
16
  <AuthProvider>
17
- <Router>
18
- <MainLayout>
19
- <Routes>
20
- <Route path="/" element={<Home />} />
21
- <Route path="/dashboard" element={<Dashboard />} />
22
- <Route path="/about" element={<About />} />
23
- </Routes>
24
- </MainLayout>
25
- </Router>
 
 
 
 
 
 
 
 
 
 
26
  </AuthProvider>
27
- </ThemeProvider>
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
 
src/components/Auth/Login.jsx ADDED
@@ -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;
src/components/Auth/Logout.jsx ADDED
@@ -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;
src/components/Auth/PrivateRoute.jsx ADDED
@@ -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;
src/components/Auth/TokenHandler.jsx ADDED
@@ -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;
src/components/Layout/Footer.jsx ADDED
@@ -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;
src/components/Layout/Header.js DELETED
@@ -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;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/Layout/Header.jsx ADDED
@@ -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;
src/components/Layout/MainLayout.jsx ADDED
@@ -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;
src/components/Search/FeedbackForm.jsx ADDED
@@ -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;
src/components/Search/SearchForm.jsx CHANGED
@@ -1,85 +1,89 @@
1
  import React, { useState } from 'react';
2
- import styled from 'styled-components';
3
- import { useSearch } from '../../hooks/useSearch';
4
- import LoadingSpinner from '../UI/LoadingSpinner';
5
- import ErrorMessage from '../UI/ErrorMessage';
 
 
 
 
 
 
 
 
 
6
 
7
- const SearchContainer = styled.div`
8
- max-width: 800px;
9
- margin: 2rem auto;
10
- padding: 2rem;
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 = styled.form`
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 { search, isLoading, error } = useSearch();
57
-
58
- const handleSubmit = async (e) => {
59
  e.preventDefault();
60
- if (!query.trim()) return;
61
-
62
- const results = await search(query);
63
- onSearchComplete(results);
64
  };
65
 
66
  return (
67
- <SearchContainer>
68
- <SearchForm onSubmit={handleSubmit}>
69
- <SearchInput
70
- type="text"
 
 
 
 
 
 
 
 
 
71
  value={query}
72
  onChange={(e) => setQuery(e.target.value)}
73
- placeholder="Ask a question about Al-Ghazali's teachings..."
74
- aria-label="Search query"
 
 
 
 
 
 
 
 
 
 
 
75
  />
76
- <SearchButton type="submit" disabled={isLoading || !query.trim()}>
77
- {isLoading ? <LoadingSpinner size="small" /> : 'Search Wisdom'}
78
- </SearchButton>
79
- {error && <ErrorMessage message={error} />}
80
- </SearchForm>
81
- </SearchContainer>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  );
83
  };
84
 
85
- export default SearchFormComponent;
 
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;
src/components/Search/SearchResults.jsx ADDED
@@ -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;
src/components/SearchResults.jsx DELETED
@@ -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;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/UI/ErrorBoundary.jsx ADDED
@@ -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;
src/components/UI/ErrorMessage.jsx ADDED
@@ -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;
src/components/UI/LoadingSpinner.jsx ADDED
@@ -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;
src/components/UI/ThemeToggle.jsx ADDED
@@ -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;
src/hooks/useAuth.js ADDED
@@ -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
+ };
src/hooks/useFeedback.js ADDED
@@ -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
+ };
src/hooks/useLocalStorage.js ADDED
@@ -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
+ };
src/hooks/useSearch.js ADDED
@@ -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
+ };
src/hooks/useTheme.js ADDED
@@ -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;
src/index.js ADDED
@@ -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
+ );
src/pages/About.jsx ADDED
@@ -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;
src/pages/Dashboard.jsx ADDED
@@ -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;
src/pages/Home.jsx ADDED
@@ -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;
src/services/api.js CHANGED
@@ -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://your-hf-space.ngrok-free.app';
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 token = localStorage.getItem('access_token');
17
- if (token) {
18
- config.headers['Authorization'] = `Bearer ${token}`;
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 refreshToken = localStorage.getItem('refresh_token');
38
- if (!refreshToken) throw new Error('No refresh token');
39
-
40
  const response = await axios.post(`${API_BASE_URL}/refresh`, {
41
- refresh_token: refreshToken
42
  });
43
-
44
- localStorage.setItem('access_token', response.data.access_token);
45
- localStorage.setItem('refresh_token', response.data.refresh_token);
46
-
47
- originalRequest.headers['Authorization'] = `Bearer ${response.data.access_token}`;
 
 
 
48
  return api(originalRequest);
49
  } catch (refreshError) {
50
- localStorage.removeItem('access_token');
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
  );
src/services/auth.js ADDED
@@ -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
+ };
src/services/search.js ADDED
@@ -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
+ };
src/styles/GlobalStyles.js ADDED
@@ -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;
src/styles/animations.js ADDED
@@ -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
+ };
src/styles/fonts.css ADDED
@@ -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
+ }
src/styles/spiritualStyles.js ADDED
@@ -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
+ `;
src/styles/theme.js CHANGED
@@ -1,64 +1,25 @@
1
- export const lightTheme = {
2
- colors: {
3
- primary: '#4a6fa5', // Deep blue
4
- secondary: '#6b8cae', // Lighter blue
5
- accent1: '#166088', // Darker blue
6
- accent2: '#4fc3a1', // Teal
7
- background: '#ffffff',
8
- cardBg: '#f8f9fa',
9
- text: '#333333',
10
- muted: '#6c757d',
11
- disabled: '#cccccc',
12
- error: '#dc3545',
13
- success: '#28a745',
14
- },
15
- fonts: {
16
- primary: '"Open Sans", sans-serif',
17
- secondary: '"Merriweather", serif',
18
- },
19
- fontSizes: {
20
- small: '0.875rem',
21
- medium: '1rem',
22
- large: '1.25rem',
23
- xlarge: '1.5rem',
24
- xxlarge: '2rem',
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;