Spaces:
Running
Running
feat: Update Dockerfile, README, and Nginx configuration for improved deployment and documentation; enhance search functionality and UI components
Browse files- Dockerfile +14 -4
- README.md +27 -2
- nginx.conf +3 -2
- src/components/Search/FeedbackForm.jsx +6 -10
- src/components/Search/SearchForm.jsx +2 -28
- src/components/Search/SearchResults.jsx +137 -69
- src/components/UI/ThemeToggle.jsx +14 -5
- src/hooks/useSearch.js +6 -8
- src/hooks/useTheme.js +1 -1
- src/pages/Dashboard.jsx +13 -38
- src/styles/theme.js +75 -2
Dockerfile
CHANGED
@@ -2,16 +2,26 @@
|
|
2 |
FROM node:20-alpine as build
|
3 |
|
4 |
WORKDIR /app
|
|
|
|
|
5 |
COPY package*.json ./
|
6 |
-
RUN npm
|
|
|
|
|
7 |
COPY . .
|
|
|
|
|
8 |
RUN npm run build
|
9 |
|
10 |
-
# Production stage
|
11 |
-
FROM nginx:
|
12 |
|
|
|
13 |
COPY --from=build /app/build /usr/share/nginx/html
|
14 |
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
15 |
|
16 |
-
|
|
|
|
|
|
|
17 |
CMD ["nginx", "-g", "daemon off;"]
|
|
|
2 |
FROM node:20-alpine as build
|
3 |
|
4 |
WORKDIR /app
|
5 |
+
|
6 |
+
# Copy package.json and install dependencies
|
7 |
COPY package*.json ./
|
8 |
+
RUN npm ci
|
9 |
+
|
10 |
+
# Copy the rest of the application code
|
11 |
COPY . .
|
12 |
+
|
13 |
+
# Build the app
|
14 |
RUN npm run build
|
15 |
|
16 |
+
# Production stage with nginx
|
17 |
+
FROM nginx:alpine
|
18 |
|
19 |
+
# Copy built app to nginx
|
20 |
COPY --from=build /app/build /usr/share/nginx/html
|
21 |
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
22 |
|
23 |
+
# Expose port
|
24 |
+
EXPOSE 3000
|
25 |
+
|
26 |
+
# Start nginx
|
27 |
CMD ["nginx", "-g", "daemon off;"]
|
README.md
CHANGED
@@ -8,5 +8,30 @@ app_port: 3000
|
|
8 |
pinned: false
|
9 |
license: mit
|
10 |
duplicated_from: HumbleBeeAI/al-ghazali-rag-retrieval-api
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
pinned: false
|
9 |
license: mit
|
10 |
duplicated_from: HumbleBeeAI/al-ghazali-rag-retrieval-api
|
11 |
+
---
|
12 |
+
|
13 |
+
# EnlightenQalb: Al-Ghazali's Wisdom Search
|
14 |
+
|
15 |
+
A React application that allows users to search through "The Alchemy of Happiness" by Imam Al-Ghazali using modern AI-powered semantic search technology.
|
16 |
+
|
17 |
+
## Features
|
18 |
+
|
19 |
+
- Semantic search across Al-Ghazali's text
|
20 |
+
- Compare results from different embedding models
|
21 |
+
- Simple emoji-based feedback system
|
22 |
+
- Light/dark theme support
|
23 |
+
|
24 |
+
## Technical Details
|
25 |
+
|
26 |
+
This application uses:
|
27 |
+
- React with Material UI for the frontend
|
28 |
+
- Sentence Transformer models (BGE-Large and UAE-Large) on the backend
|
29 |
+
- Docker for containerization
|
30 |
+
|
31 |
+
## Development
|
32 |
+
|
33 |
+
To run locally:
|
34 |
+
|
35 |
+
```bash
|
36 |
+
npm install
|
37 |
+
npm start
|
nginx.conf
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
server {
|
2 |
-
listen 80
|
3 |
server_name localhost;
|
4 |
|
5 |
location / {
|
@@ -10,10 +10,11 @@ server {
|
|
10 |
|
11 |
# Proxy API requests to your backend
|
12 |
location /api/ {
|
13 |
-
proxy_pass https://
|
14 |
proxy_set_header Host $host;
|
15 |
proxy_set_header X-Real-IP $remote_addr;
|
16 |
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
|
17 |
}
|
18 |
|
19 |
error_page 500 502 503 504 /50x.html;
|
|
|
1 |
server {
|
2 |
+
listen 3000; # Changed from 80 to 3000 for HF Spaces
|
3 |
server_name localhost;
|
4 |
|
5 |
location / {
|
|
|
10 |
|
11 |
# Proxy API requests to your backend
|
12 |
location /api/ {
|
13 |
+
proxy_pass https://humblebeeai-al-ghazali-rag-retrieval-api.hf.space/;
|
14 |
proxy_set_header Host $host;
|
15 |
proxy_set_header X-Real-IP $remote_addr;
|
16 |
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
17 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
18 |
}
|
19 |
|
20 |
error_page 500 502 503 504 /50x.html;
|
src/components/Search/FeedbackForm.jsx
CHANGED
@@ -31,16 +31,10 @@ const FeedbackForm = ({ searchResult, query, onFeedbackSubmitted }) => {
|
|
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 |
-
<
|
42 |
-
<Alert severity="error">Invalid search result data</Alert>
|
43 |
-
</StyledPaper>
|
44 |
);
|
45 |
}
|
46 |
|
@@ -50,15 +44,17 @@ const FeedbackForm = ({ searchResult, query, onFeedbackSubmitted }) => {
|
|
50 |
setError(null);
|
51 |
|
52 |
try {
|
53 |
-
// Extract only the needed
|
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 |
|
@@ -148,4 +144,4 @@ const FeedbackForm = ({ searchResult, query, onFeedbackSubmitted }) => {
|
|
148 |
);
|
149 |
};
|
150 |
|
151 |
-
export default FeedbackForm;
|
|
|
31 |
const [isSubmitting, setIsSubmitting] = useState(false);
|
32 |
const [error, setError] = useState(null);
|
33 |
const [success, setSuccess] = useState(false);
|
|
|
|
|
|
|
34 |
|
|
|
35 |
if (!searchResult || typeof searchResult !== 'object') {
|
36 |
return (
|
37 |
+
<Alert severity="error">Invalid search result data</Alert>
|
|
|
|
|
38 |
);
|
39 |
}
|
40 |
|
|
|
44 |
setError(null);
|
45 |
|
46 |
try {
|
47 |
+
// Extract only the needed properties for the backend
|
48 |
const feedbackData = {
|
49 |
user_type: 'user',
|
50 |
username: user || 'anonymous',
|
51 |
query: query || '',
|
52 |
retrieved_text: searchResult.content || searchResult.text || JSON.stringify(searchResult),
|
53 |
model_type: searchResult.model_type || 'unknown',
|
54 |
+
result_label: searchResult.resultLabel || '',
|
55 |
reaction,
|
56 |
+
confidence_score: searchResult.similarity || 0, // Send the model confidence score
|
57 |
+
user_confidence: confidence, // User's confidence in their rating
|
58 |
rating
|
59 |
};
|
60 |
|
|
|
144 |
);
|
145 |
};
|
146 |
|
147 |
+
export default FeedbackForm;
|
src/components/Search/SearchForm.jsx
CHANGED
@@ -5,10 +5,7 @@ import {
|
|
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';
|
@@ -20,12 +17,11 @@ const StyledPaper = styled(Paper)(({ theme }) => ({
|
|
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,
|
29 |
}
|
30 |
};
|
31 |
|
@@ -59,28 +55,6 @@ const SearchForm = ({ onSearch, isLoading }) => {
|
|
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 |
);
|
|
|
5 |
Button,
|
6 |
Typography,
|
7 |
Paper,
|
8 |
+
CircularProgress
|
|
|
|
|
|
|
9 |
} from '@mui/material';
|
10 |
import SearchIcon from '@mui/icons-material/Search';
|
11 |
import styled from '@emotion/styled';
|
|
|
17 |
|
18 |
const SearchForm = ({ onSearch, isLoading }) => {
|
19 |
const [query, setQuery] = useState('');
|
|
|
20 |
|
21 |
const handleSubmit = (e) => {
|
22 |
e.preventDefault();
|
23 |
if (query.trim()) {
|
24 |
+
onSearch(query, 'both'); // Always use both models
|
25 |
}
|
26 |
};
|
27 |
|
|
|
55 |
),
|
56 |
}}
|
57 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
</Box>
|
59 |
</StyledPaper>
|
60 |
);
|
src/components/Search/SearchResults.jsx
CHANGED
@@ -1,49 +1,65 @@
|
|
1 |
-
import React, { useState } from 'react';
|
2 |
import {
|
3 |
Box,
|
4 |
Paper,
|
5 |
Typography,
|
6 |
-
|
7 |
-
|
8 |
-
|
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
|
|
|
17 |
|
18 |
-
const
|
19 |
-
padding: theme.spacing(
|
20 |
-
|
21 |
-
|
|
|
|
|
22 |
}));
|
23 |
|
24 |
-
const
|
25 |
-
|
26 |
-
|
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
|
38 |
-
const [
|
|
|
|
|
39 |
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
|
48 |
if (error) {
|
49 |
return (
|
@@ -61,49 +77,101 @@ const SearchResults = ({ results, query, isLoading, error }) => {
|
|
61 |
);
|
62 |
}
|
63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
return (
|
65 |
<Box>
|
66 |
<Typography variant="h6" gutterBottom>
|
67 |
Search Results
|
68 |
</Typography>
|
69 |
|
70 |
-
{
|
71 |
-
|
72 |
-
key={
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
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 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
</Box>
|
108 |
);
|
109 |
};
|
|
|
1 |
+
import React, { useState, useEffect } from 'react';
|
2 |
import {
|
3 |
Box,
|
4 |
Paper,
|
5 |
Typography,
|
6 |
+
IconButton,
|
7 |
+
Alert,
|
8 |
+
Grid
|
|
|
|
|
|
|
|
|
9 |
} from '@mui/material';
|
|
|
10 |
import styled from '@emotion/styled';
|
11 |
+
import { saveFeedback } from '../../services/search';
|
12 |
+
import { useAuth } from '../../hooks/useAuth';
|
13 |
|
14 |
+
const ResultCard = styled(Paper)(({ theme }) => ({
|
15 |
+
padding: theme.spacing(3),
|
16 |
+
height: '100%',
|
17 |
+
display: 'flex',
|
18 |
+
flexDirection: 'column',
|
19 |
+
position: 'relative',
|
20 |
}));
|
21 |
|
22 |
+
const EmojiButton = styled(IconButton)(({ theme }) => ({
|
23 |
+
fontSize: '1.5rem',
|
24 |
+
padding: theme.spacing(1),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
}));
|
26 |
|
27 |
const SearchResults = ({ results, query, isLoading, error }) => {
|
28 |
+
const { user, authTokens } = useAuth();
|
29 |
+
const [shuffledResults, setShuffledResults] = useState([]);
|
30 |
+
const [feedbackStatus, setFeedbackStatus] = useState({});
|
31 |
+
const [savingFeedback, setSavingFeedback] = useState(false);
|
32 |
|
33 |
+
// Shuffle and limit to 2 results when results change
|
34 |
+
useEffect(() => {
|
35 |
+
if (results && results.length) {
|
36 |
+
// Get one result from each model type if possible
|
37 |
+
const bgeResult = results.find(r => r.model_type?.includes('BGE'));
|
38 |
+
const uaeResult = results.find(r => r.model_type?.includes('UAE'));
|
39 |
+
|
40 |
+
let selectedResults = [];
|
41 |
+
if (bgeResult && uaeResult) {
|
42 |
+
selectedResults = [bgeResult, uaeResult];
|
43 |
+
} else {
|
44 |
+
// If we don't have both models, take top 2 (or all if less than 2)
|
45 |
+
selectedResults = results.slice(0, 2);
|
46 |
+
}
|
47 |
+
|
48 |
+
// Randomly shuffle the order
|
49 |
+
const shuffled = [...selectedResults].sort(() => Math.random() - 0.5);
|
50 |
+
|
51 |
+
// Assign labels (A and B)
|
52 |
+
setShuffledResults(shuffled.map((result, index) => ({
|
53 |
+
...result,
|
54 |
+
resultLabel: index === 0 ? 'A' : 'B'
|
55 |
+
})));
|
56 |
+
|
57 |
+
// Reset feedback status
|
58 |
+
setFeedbackStatus({});
|
59 |
+
} else {
|
60 |
+
setShuffledResults([]);
|
61 |
+
}
|
62 |
+
}, [results]);
|
63 |
|
64 |
if (error) {
|
65 |
return (
|
|
|
77 |
);
|
78 |
}
|
79 |
|
80 |
+
const handleReaction = async (result, reaction) => {
|
81 |
+
// Prevent multiple submissions
|
82 |
+
if (feedbackStatus[result.resultLabel] || savingFeedback) return;
|
83 |
+
|
84 |
+
setSavingFeedback(true);
|
85 |
+
|
86 |
+
try {
|
87 |
+
// Prepare feedback data
|
88 |
+
const feedbackData = {
|
89 |
+
user_type: 'user',
|
90 |
+
username: user || 'anonymous',
|
91 |
+
query: query || '',
|
92 |
+
retrieved_text: result.content || result.text || JSON.stringify(result),
|
93 |
+
model_type: result.model_type || 'unknown',
|
94 |
+
result_label: result.resultLabel || '',
|
95 |
+
reaction: reaction === '👍' ? 'relevant' : reaction === '👎' ? 'not_relevant' : 'somewhat_relevant',
|
96 |
+
confidence_score: result.similarity || 0, // Send the model confidence score
|
97 |
+
};
|
98 |
+
|
99 |
+
// Save the feedback
|
100 |
+
await saveFeedback(feedbackData, authTokens.access);
|
101 |
+
|
102 |
+
// Update UI to show feedback was saved
|
103 |
+
setFeedbackStatus(prev => ({
|
104 |
+
...prev,
|
105 |
+
[result.resultLabel]: { status: 'success', reaction }
|
106 |
+
}));
|
107 |
+
|
108 |
+
} catch (err) {
|
109 |
+
console.error("Feedback submission error:", err);
|
110 |
+
setFeedbackStatus(prev => ({
|
111 |
+
...prev,
|
112 |
+
[result.resultLabel]: { status: 'error', message: 'Failed to save feedback' }
|
113 |
+
}));
|
114 |
+
} finally {
|
115 |
+
setSavingFeedback(false);
|
116 |
+
}
|
117 |
+
};
|
118 |
+
|
119 |
return (
|
120 |
<Box>
|
121 |
<Typography variant="h6" gutterBottom>
|
122 |
Search Results
|
123 |
</Typography>
|
124 |
|
125 |
+
<Grid container spacing={3} sx={{ mb: 4 }}>
|
126 |
+
{shuffledResults.map((result) => (
|
127 |
+
<Grid item xs={12} md={6} key={result.resultLabel}>
|
128 |
+
<ResultCard elevation={3}>
|
129 |
+
<Typography variant="h6" gutterBottom>
|
130 |
+
Result {result.resultLabel}
|
131 |
+
</Typography>
|
132 |
+
<Typography variant="body1" paragraph sx={{ flexGrow: 1 }}>
|
133 |
+
{result.text}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
</Typography>
|
135 |
+
|
136 |
+
<Box mt={2}>
|
137 |
+
{feedbackStatus[result.resultLabel]?.status === 'success' ? (
|
138 |
+
<Typography variant="body2" color="success.main" align="center">
|
139 |
+
Thank you for your feedback! You selected {feedbackStatus[result.resultLabel].reaction}
|
140 |
+
</Typography>
|
141 |
+
) : feedbackStatus[result.resultLabel]?.status === 'error' ? (
|
142 |
+
<Typography variant="body2" color="error" align="center">
|
143 |
+
{feedbackStatus[result.resultLabel].message}
|
144 |
+
</Typography>
|
145 |
+
) : (
|
146 |
+
<Box display="flex" justifyContent="center" gap={2}>
|
147 |
+
<EmojiButton
|
148 |
+
onClick={() => handleReaction(result, '👍')}
|
149 |
+
disabled={savingFeedback}
|
150 |
+
aria-label="Good result"
|
151 |
+
>
|
152 |
+
👍
|
153 |
+
</EmojiButton>
|
154 |
+
<EmojiButton
|
155 |
+
onClick={() => handleReaction(result, '🤷♂️')}
|
156 |
+
disabled={savingFeedback}
|
157 |
+
aria-label="Somewhat relevant"
|
158 |
+
>
|
159 |
+
🤷♂️
|
160 |
+
</EmojiButton>
|
161 |
+
<EmojiButton
|
162 |
+
onClick={() => handleReaction(result, '👎')}
|
163 |
+
disabled={savingFeedback}
|
164 |
+
aria-label="Not relevant"
|
165 |
+
>
|
166 |
+
👎
|
167 |
+
</EmojiButton>
|
168 |
+
</Box>
|
169 |
+
)}
|
170 |
+
</Box>
|
171 |
+
</ResultCard>
|
172 |
+
</Grid>
|
173 |
+
))}
|
174 |
+
</Grid>
|
175 |
</Box>
|
176 |
);
|
177 |
};
|
src/components/UI/ThemeToggle.jsx
CHANGED
@@ -1,23 +1,32 @@
|
|
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={
|
15 |
color="inherit"
|
16 |
size={size}
|
17 |
aria-label="toggle theme"
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
>
|
19 |
{isDark ? (
|
20 |
-
<Brightness7 sx={{ color: theme.palette.warning?.main ||
|
21 |
) : (
|
22 |
<Brightness4 sx={{ color: theme.palette.grey[600] }} />
|
23 |
)}
|
|
|
1 |
+
import React, { useCallback } from 'react';
|
2 |
+
import { IconButton, Tooltip, useTheme as useMuiTheme } from '@mui/material';
|
3 |
import { Brightness4, Brightness7 } from '@mui/icons-material';
|
|
|
4 |
import { useTheme } from '../../hooks/useTheme';
|
5 |
|
6 |
const ThemeToggle = ({ size = 'medium' }) => {
|
7 |
const { toggleTheme, isDark } = useTheme();
|
8 |
const theme = useMuiTheme();
|
9 |
|
10 |
+
const handleToggle = useCallback(() => {
|
11 |
+
toggleTheme();
|
12 |
+
}, [toggleTheme]);
|
13 |
+
|
14 |
return (
|
15 |
<Tooltip title={`Switch to ${isDark ? 'light' : 'dark'} mode`}>
|
16 |
<IconButton
|
17 |
+
onClick={handleToggle}
|
18 |
color="inherit"
|
19 |
size={size}
|
20 |
aria-label="toggle theme"
|
21 |
+
sx={{
|
22 |
+
transition: 'transform 0.3s ease',
|
23 |
+
'&:hover': {
|
24 |
+
transform: 'rotate(15deg)'
|
25 |
+
}
|
26 |
+
}}
|
27 |
>
|
28 |
{isDark ? (
|
29 |
+
<Brightness7 sx={{ color: theme.palette.warning?.main || '#ffc107' }} />
|
30 |
) : (
|
31 |
<Brightness4 sx={{ color: theme.palette.grey[600] }} />
|
32 |
)}
|
src/hooks/useSearch.js
CHANGED
@@ -1,6 +1,5 @@
|
|
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 = () => {
|
@@ -8,6 +7,7 @@ export const useSearch = () => {
|
|
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()) {
|
@@ -15,16 +15,13 @@ export const useSearch = () => {
|
|
15 |
return;
|
16 |
}
|
17 |
|
|
|
18 |
setIsLoading(true);
|
19 |
setError(null);
|
20 |
|
21 |
try {
|
22 |
-
const searchResults = await
|
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([]);
|
@@ -44,5 +41,6 @@ export const useSearch = () => {
|
|
44 |
error,
|
45 |
handleSearch,
|
46 |
clearResults,
|
|
|
47 |
};
|
48 |
};
|
|
|
|
|
1 |
import { useState, useCallback } from 'react';
|
2 |
+
import { searchQuery as searchQueryApi } from '../services/search';
|
3 |
import { useAuth } from './useAuth';
|
4 |
|
5 |
export const useSearch = () => {
|
|
|
7 |
const [results, setResults] = useState([]);
|
8 |
const [isLoading, setIsLoading] = useState(false);
|
9 |
const [error, setError] = useState(null);
|
10 |
+
const [searchQuery, setSearchQuery] = useState('');
|
11 |
|
12 |
const handleSearch = useCallback(async (query) => {
|
13 |
if (!query.trim()) {
|
|
|
15 |
return;
|
16 |
}
|
17 |
|
18 |
+
setSearchQuery(query);
|
19 |
setIsLoading(true);
|
20 |
setError(null);
|
21 |
|
22 |
try {
|
23 |
+
const searchResults = await searchQueryApi(query, authTokens.access);
|
24 |
+
setResults(searchResults);
|
|
|
|
|
|
|
|
|
25 |
} catch (err) {
|
26 |
setError(err.message || 'Failed to perform search');
|
27 |
setResults([]);
|
|
|
41 |
error,
|
42 |
handleSearch,
|
43 |
clearResults,
|
44 |
+
searchQuery,
|
45 |
};
|
46 |
};
|
src/hooks/useTheme.js
CHANGED
@@ -23,7 +23,7 @@ const useThemeState = () => {
|
|
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');
|
|
|
23 |
const [themeLoading, setThemeLoading] = useState(true);
|
24 |
|
25 |
useEffect(() => {
|
26 |
+
// Apply the theme class to document body immediately
|
27 |
if (isDark) {
|
28 |
document.body.classList.add('dark-mode');
|
29 |
document.body.classList.remove('light-mode');
|
src/pages/Dashboard.jsx
CHANGED
@@ -1,40 +1,21 @@
|
|
1 |
-
import { useState
|
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 {
|
14 |
const {
|
15 |
-
searchQuery,
|
16 |
results,
|
17 |
-
|
18 |
error,
|
19 |
-
handleSearch,
|
20 |
-
|
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 }}>
|
@@ -43,28 +24,22 @@ const Dashboard = () => {
|
|
43 |
</Typography>
|
44 |
|
45 |
<Box sx={{ mb: 4 }}>
|
46 |
-
<SearchForm
|
|
|
|
|
|
|
47 |
</Box>
|
48 |
|
49 |
-
{
|
50 |
{error && <ErrorMessage message={error} />}
|
51 |
|
52 |
-
{results && (
|
53 |
<Box sx={{ mb: 4 }}>
|
54 |
<SearchResults
|
55 |
results={results}
|
56 |
query={searchQuery}
|
57 |
-
|
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 |
)}
|
|
|
1 |
+
import { useState } from 'react';
|
|
|
2 |
import { useAuth } from '../hooks/useAuth';
|
3 |
import { useSearch } from '../hooks/useSearch';
|
4 |
import SearchForm from '../components/Search/SearchForm';
|
5 |
import SearchResults from '../components/Search/SearchResults';
|
|
|
6 |
import LoadingSpinner from '../components/UI/LoadingSpinner';
|
7 |
import ErrorMessage from '../components/UI/ErrorMessage';
|
8 |
import { Box, Typography, Container } from '@mui/material';
|
9 |
|
10 |
const Dashboard = () => {
|
11 |
+
const { user } = useAuth();
|
12 |
const {
|
|
|
13 |
results,
|
14 |
+
isLoading,
|
15 |
error,
|
16 |
+
handleSearch,
|
17 |
+
searchQuery
|
18 |
} = useSearch();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
|
20 |
return (
|
21 |
<Container maxWidth="lg" sx={{ py: 4 }}>
|
|
|
24 |
</Typography>
|
25 |
|
26 |
<Box sx={{ mb: 4 }}>
|
27 |
+
<SearchForm
|
28 |
+
onSearch={handleSearch}
|
29 |
+
isLoading={isLoading}
|
30 |
+
/>
|
31 |
</Box>
|
32 |
|
33 |
+
{isLoading && <LoadingSpinner />}
|
34 |
{error && <ErrorMessage message={error} />}
|
35 |
|
36 |
+
{results && results.length > 0 && (
|
37 |
<Box sx={{ mb: 4 }}>
|
38 |
<SearchResults
|
39 |
results={results}
|
40 |
query={searchQuery}
|
41 |
+
isLoading={isLoading}
|
42 |
+
error={error}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
/>
|
44 |
</Box>
|
45 |
)}
|
src/styles/theme.js
CHANGED
@@ -1,5 +1,5 @@
|
|
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({
|
@@ -8,16 +8,89 @@ const getTheme = (isDark) => createTheme({
|
|
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 |
};
|
|
|
1 |
import { createTheme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
|
2 |
+
import React, { useMemo, useEffect } from 'react';
|
3 |
import useTheme from '../hooks/useTheme';
|
4 |
|
5 |
const getTheme = (isDark) => createTheme({
|
|
|
8 |
primary: {
|
9 |
main: isDark ? '#7986cb' : '#3f51b5',
|
10 |
},
|
11 |
+
secondary: {
|
12 |
+
main: isDark ? '#ff9800' : '#f50057',
|
13 |
+
},
|
14 |
background: {
|
15 |
default: isDark ? '#121212' : '#f9f9f9',
|
16 |
+
paper: isDark ? '#1e1e1e' : '#ffffff',
|
17 |
+
},
|
18 |
+
text: {
|
19 |
+
primary: isDark ? '#ffffff' : '#121212',
|
20 |
+
secondary: isDark ? '#b0bec5' : '#666666',
|
21 |
+
},
|
22 |
+
// Add spiritual palette for specialized components
|
23 |
+
spiritual: {
|
24 |
+
light: isDark ? '#303f9f' : '#e8eaf6',
|
25 |
+
dark: isDark ? '#1a237e' : '#c5cae9',
|
26 |
+
accent: isDark ? '#ffcc80' : '#7986cb',
|
27 |
+
quoteBorder: isDark ? '#5c6bc0' : '#3f51b5',
|
28 |
+
},
|
29 |
+
},
|
30 |
+
typography: {
|
31 |
+
fontFamily: '"Poppins", "Roboto", "Helvetica", "Arial", sans-serif',
|
32 |
+
h1: {
|
33 |
+
fontWeight: 700,
|
34 |
+
},
|
35 |
+
h2: {
|
36 |
+
fontWeight: 600,
|
37 |
+
},
|
38 |
+
h3: {
|
39 |
+
fontWeight: 600,
|
40 |
+
},
|
41 |
+
h4: {
|
42 |
+
fontWeight: 600,
|
43 |
+
},
|
44 |
+
h5: {
|
45 |
+
fontWeight: 500,
|
46 |
+
},
|
47 |
+
h6: {
|
48 |
+
fontWeight: 500,
|
49 |
+
},
|
50 |
+
},
|
51 |
+
shape: {
|
52 |
+
borderRadius: 8,
|
53 |
+
},
|
54 |
+
components: {
|
55 |
+
MuiButton: {
|
56 |
+
styleOverrides: {
|
57 |
+
root: {
|
58 |
+
textTransform: 'none',
|
59 |
+
borderRadius: 8,
|
60 |
+
},
|
61 |
+
},
|
62 |
+
},
|
63 |
+
MuiCard: {
|
64 |
+
styleOverrides: {
|
65 |
+
root: {
|
66 |
+
borderRadius: 12,
|
67 |
+
},
|
68 |
+
},
|
69 |
},
|
70 |
},
|
71 |
});
|
72 |
|
73 |
+
// Enhanced ThemeProvider that forces re-render on theme change
|
74 |
export const ThemeProvider = ({ children }) => {
|
75 |
const { isDark } = useTheme();
|
76 |
+
|
77 |
+
// Create a new theme whenever isDark changes
|
78 |
const theme = useMemo(() => getTheme(isDark), [isDark]);
|
79 |
+
|
80 |
+
// Force a style update when theme changes
|
81 |
+
useEffect(() => {
|
82 |
+
// This helps force Material-UI to reapply styles
|
83 |
+
document.documentElement.style.setProperty(
|
84 |
+
'--mui-palette-mode',
|
85 |
+
isDark ? 'dark' : 'light'
|
86 |
+
);
|
87 |
+
|
88 |
+
// Apply additional custom CSS variables if needed
|
89 |
+
document.documentElement.style.setProperty(
|
90 |
+
'--app-background',
|
91 |
+
isDark ? '#121212' : '#f9f9f9'
|
92 |
+
);
|
93 |
+
}, [isDark]);
|
94 |
|
95 |
return <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>;
|
96 |
};
|