Tons of updates. Rags paginated tables, site tracking

This commit is contained in:
2025-05-14 03:23:18 -05:00
parent 5d054026d8
commit 004153617b
15 changed files with 3443 additions and 4212 deletions

6773
llm-fe/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"chroma-js": "^3.1.2", "chroma-js": "^3.1.2",
"formik": "^2.4.6", "formik": "^2.4.6",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-to-jsx": "^7.7.2", "markdown-to-jsx": "^7.7.2",
@@ -65,9 +66,11 @@
"@types/lodash": "~4.17.13", "@types/lodash": "~4.17.13",
"@types/react": "^18.3.16", "@types/react": "^18.3.16",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"@types/react-google-recaptcha": "^2.1.9",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-google-recaptcha": "^3.1.0",
"typescript": "^4.9.5" "typescript": "^4.9.5"
} }
} }

View File

@@ -15,8 +15,10 @@ import FeedbackPage from './llm-fe/pages/FeedbackPage/FeedbackPage';
import { Typography } from '@mui/material'; import { Typography } from '@mui/material';
import AsyncDashboard2 from './llm-fe/pages/AsyncDashboard2/AsyncDashboard2'; import AsyncDashboard2 from './llm-fe/pages/AsyncDashboard2/AsyncDashboard2';
import FeedbackPage2 from './llm-fe/pages/FeedbackPage2/FeedbackPage2'; import FeedbackPage2 from './llm-fe/pages/FeedbackPage2/FeedbackPage2';
import DocumentStoragePage from './llm-fe/pages/DocumentStoragePage/DocumentStoragePage';
import Account2 from './llm-fe/pages/Account2/Account2'; import Account2 from './llm-fe/pages/Account2/Account2';
import AnalyticsPage from './llm-fe/pages/Analytics/Analytics'; import AnalyticsPage from './llm-fe/pages/Analytics/Analytics';
import PasswordResetConfirmation from './llm-fe/pages/PasswordResetConfirmation/PasswordReset';
const ProtectedRoutes = () => { const ProtectedRoutes = () => {
const { authenticated, needsNewPassword, loading } = useContext(AuthContext); const { authenticated, needsNewPassword, loading } = useContext(AuthContext);
@@ -45,6 +47,7 @@ class App extends Component {
<Route path={"/signin/"} Component={SignIn}/> <Route path={"/signin/"} Component={SignIn}/>
<Route path={"/password_reset/"} Component={PasswordReset}/> <Route path={"/password_reset/"} Component={PasswordReset}/>
<Route path={"/password_reset_confirmation/"} Component={PasswordResetConfirmation}/>
<Route path={'/set_password/'} Component={SetPassword}/> <Route path={'/set_password/'} Component={SetPassword}/>
@@ -52,6 +55,7 @@ class App extends Component {
<Route element={<ProtectedRoutes />}> <Route element={<ProtectedRoutes />}>
<Route path={"/"} index={true} Component={AsyncDashboard2}/> <Route path={"/"} index={true} Component={AsyncDashboard2}/>
<Route path={"/account/"} Component={Account2}/> <Route path={"/account/"} Component={Account2}/>
<Route path={"/document_storage"} Component={DocumentStoragePage}/>
<Route path={"/terms_of_service/"} Component={TermsOfService}/> <Route path={"/terms_of_service/"} Component={TermsOfService}/>
<Route path={"/feedback/"} Component={FeedbackPage2}/> <Route path={"/feedback/"} Component={FeedbackPage2}/>
<Route path={"/analytics/"} Component={AnalyticsPage} /> <Route path={"/analytics/"} Component={AnalyticsPage} />

View File

@@ -1,6 +1,7 @@
import axios from "axios"; import axios from "axios";
const Cookies = require('js-cookie');
//const baseURL = 'http://127.0.0.1:8001/api/'; //const baseURL = 'http://127.0.0.1:8011/api/';
const baseURL = 'https://chatbackend.aimloperations.com/api/'; const baseURL = 'https://chatbackend.aimloperations.com/api/';
//const baseURL = process.env.REACT_APP_BACKEND_REST_API_BASE_URL; //const baseURL = process.env.REACT_APP_BACKEND_REST_API_BASE_URL;
@@ -14,6 +15,25 @@ export const axiosInstance = axios.create({
} }
}); });
export const cleanAxiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
});
export const axiosInstanceCSRF = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
'X-CSRFToken': Cookies.get('csrftoken'), // Include CSRF token in headers
},
withCredentials: true,
}
);
axiosInstance.interceptors.request.use(config => { axiosInstance.interceptors.request.use(config => {
config.timeout = 100000; config.timeout = 100000;
return config; return config;

View File

@@ -48,6 +48,10 @@ const Header2 = ({ absolute=false, light=false, isMini=false }: Header2Props): J
navigate('/') navigate('/')
} }
const handleDocumentStorageClick = async () => {
navigate('/document_storage/')
}
const handleAccountClick = async () => { const handleAccountClick = async () => {
navigate('/account/') navigate('/account/')
} }
@@ -77,6 +81,8 @@ const Header2 = ({ absolute=false, light=false, isMini=false }: Header2Props): J
<MDBox sx={{marginLeft: "auto"}}> <MDBox sx={{marginLeft: "auto"}}>
<Button color="inherit" onClick={handleAccountClick}>Account</Button> <Button color="inherit" onClick={handleAccountClick}>Account</Button>
<Button color="inherit" onClick={handleDocumentStorageClick}>Document Storage</Button>
<Button color="inherit" onClick={handleAnalyticsClick}>Analytics</Button> <Button color="inherit" onClick={handleAnalyticsClick}>Analytics</Button>
<Button color="inherit" onClick={handleFeedbackClick} >Feedback</Button> <Button color="inherit" onClick={handleFeedbackClick} >Feedback</Button>

View File

@@ -0,0 +1,93 @@
import React, { useState } from 'react';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
TablePagination,
} from "@mui/material";;
interface PaginatedTableProps<T> {
data: T[];
columns: {
key: keyof T;
label: string;
render?: (value: any, row: T) => React.ReactNode;
}[];
rowsPerPageOptions?: number[];
}
export function PaginatedTable<T>({
data,
columns,
rowsPerPageOptions = [5, 10, 25],
}: PaginatedTableProps<T>) {
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(rowsPerPageOptions[0]);
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
// Avoid a layout jump when reaching the last page with empty rows.
const emptyRows =
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - data.length) : 0;
return (
<Paper>
<TableContainer>
<Table>
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell key={column.key.toString()}>{column.label}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{(rowsPerPage > 0
? data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
: data
).map((row, index) => (
<TableRow key={index}>
{columns.map((column) => (
<TableCell key={column.key.toString()}>
{column.render
? column.render(row[column.key], row)
: (row[column.key] as React.ReactNode)}
</TableCell>
))}
</TableRow>
))}
{emptyRows > 0 && (
<TableRow style={{ height: 53 * emptyRows }}>
<TableCell colSpan={columns.length} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={rowsPerPageOptions}
component="div"
count={data.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</Paper>
);
}
export default PaginatedTable;

View File

@@ -24,7 +24,7 @@ function WebSocketProvider({ children }) {
delete channels.current[channel] delete channels.current[channel]
} }
const sendMessage = (message, conversation_id, file, fileType) => { const sendMessage = (message, conversation_id, file, fileType, modelName) => {
if (socket && socket.readyState === WebSocket.OPEN){ if (socket && socket.readyState === WebSocket.OPEN){
if (file){ if (file){
@@ -40,6 +40,7 @@ function WebSocketProvider({ children }) {
email: account?.email, email: account?.email,
file: base64File, file: base64File,
fileType: fileType, fileType: fileType,
modelName: modelName,
} }
socket.send(JSON.stringify(data)) socket.send(JSON.stringify(data))
@@ -52,7 +53,8 @@ function WebSocketProvider({ children }) {
conversation_id: conversation_id, conversation_id: conversation_id,
email: account?.email, email: account?.email,
file: null, file: null,
fileType: null fileType: null,
modelName: modelName,
} }
socket.send(JSON.stringify(data)) socket.send(JSON.stringify(data))
@@ -69,7 +71,7 @@ function WebSocketProvider({ children }) {
if (account){ if (account){
//ws.current = new WebSocket(`ws://127.0.0.1:8001/ws/chat_again/`); //ws.current = new WebSocket(`ws://127.0.0.1:8011/ws/chat_again/`);
ws.current = new WebSocket('wss://chatbackend.aimloperations.com/ws/chat_again/') ws.current = new WebSocket('wss://chatbackend.aimloperations.com/ws/chat_again/')
//ws.current = process.env.REACT_APP_BACKEND_WS_API_BASE_URL; //ws.current = process.env.REACT_APP_BACKEND_WS_API_BASE_URL;

View File

@@ -87,6 +87,16 @@ export class Announcement {
} }
} }
export interface DocumentType {
id: number;
name: string;
date_uploaded: string;
created: string;
file: string;
active: boolean;
processed: boolean;
}
export interface FeedbackType { export interface FeedbackType {
id: number; id: number;
title: string; title: string;
@@ -95,7 +105,24 @@ export interface FeedbackType {
category: string; category: string;
} }
export class Document {
id: number = 0;
name: string = '';
file: string = '';
date_uploaded: string = '';
active: boolean = false;
processed: boolean = false;
constructor(initializer?: any){
if(!initializer) return;
if (initializer.id) this.id = initializer.id;
if (initializer.name) this.name = initializer.name;
if (initializer.file) this.file = initializer.file;
if (initializer.active) this.active = initializer.active;
if (initializer.date_uploaded) this.date_uploaded = initializer.date_uploaded;
if (initializer.processed) this.processed = initializer.processed;
}
}
export class Feedback { export class Feedback {
id: number = 0; id: number = 0;

View File

@@ -1,4 +1,4 @@
import { Card, CardContent, Divider, InputAdornment, } from "@mui/material" import { Card, CardContent, Divider, InputAdornment, MenuItem, Select, } from "@mui/material"
import DashboardLayout from "../../ui-kit/examples/LayoutContainers/DashboardLayout" import DashboardLayout from "../../ui-kit/examples/LayoutContainers/DashboardLayout"
@@ -29,6 +29,8 @@ import Footer from "../../components/Footer/Footer"
import { MessageContext } from "../../contexts/MessageContext" import { MessageContext } from "../../contexts/MessageContext"
import CustomSelect, { CustomSelectItem } from "../../components/CustomSelect/CustomSelect" import CustomSelect, { CustomSelectItem } from "../../components/CustomSelect/CustomSelect"
const MODELS = ["Turbo","RAG"]
type RenderMessageProps= { type RenderMessageProps= {
response: string response: string
index: number index: number
@@ -126,7 +128,6 @@ const AsyncDashboardInner =({}): JSX.Element => {
const handlePromptSubmit = async ({prompt, file, fileType, modelName}: PromptValues, {resetForm}: any): Promise<void> => { const handlePromptSubmit = async ({prompt, file, fileType, modelName}: PromptValues, {resetForm}: any): Promise<void> => {
// send the prompt to be saved // send the prompt to be saved
try{ try{
const tempConversations: ConversationPrompt[] = [...conversationDetails, new ConversationPrompt({message: prompt, user_created:true}), new ConversationPrompt({message: '', user_created:false})] const tempConversations: ConversationPrompt[] = [...conversationDetails, new ConversationPrompt({message: prompt, user_created:true}), new ConversationPrompt({message: '', user_created:false})]
@@ -135,7 +136,7 @@ const AsyncDashboardInner =({}): JSX.Element => {
setConversationDetails(tempConversations) setConversationDetails(tempConversations)
// TODO: add the file here // TODO: add the file here
sendMessage(prompt, selectedConversation, file, fileType) sendMessage(prompt, selectedConversation, file, fileType, modelName)
resetForm(); resetForm();
@@ -205,13 +206,15 @@ return(
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
<InputAdornment position='end'> <InputAdornment position='end'>
{/* <CustomSelect <Select
name="category" id="modelSelect"
items={models} label="Model"
label="Category" value={formik.values}
required required
onChange={(e) => {console.log(e.target.value); formik.setFieldValue('modelName', e.target.value)}}
/> */} >
{MODELS.map((model) => <MenuItem value={model} >{model}</MenuItem>)}
</Select>
<MDButton <MDButton
component="label" component="label"
startIcon={<AttachFile/>} startIcon={<AttachFile/>}

View File

@@ -57,7 +57,7 @@ const ConversationDetail = ({selectedConversation, conversationTitle,conversatio
useEffect(() => { useEffect(() => {
// now we want to stream the new response // now we want to stream the new response
const eventSource = new EventSource("http://127.0.0.1:8000/api/streamed_response") const eventSource = new EventSource("http://127.0.0.1:8011/api/streamed_response")
try{ try{
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {

View File

@@ -0,0 +1,329 @@
import { useEffect, useState } from "react";
import MDBox from "../../ui-kit/components/MDBox";
import { Box, Button, Card, CardContent, Divider, IconButton, Paper, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
import { Document, DocumentType } from "../../data";
import { AxiosResponse } from "axios";
import { axiosInstance } from "../../../axiosApi";
import Header2 from "../../components/Header2/Header2";
import PageWrapperLayout from "../../components/PageWrapperLayout/PageWrapperLayout";
import Footer from "../../components/Footer/Footer";
import MDTypography from "../../ui-kit/components/MDTypography";
import FeedbackSubmitCard from "../../components/FeedbackSubmitCard/FeedbackSubmitCard";
import { CheckCircle, CloudUpload, DeleteForever, Pending } from "@mui/icons-material";
import { Formik } from "formik";
import MDButton from "../../ui-kit/components/MDButton";
import PaginatedTable from "../../components/PaginatedTable/PaginatedTable";
type DocumentLineProps = {
document: Document,
handleDocumentUpdate: (document_id: number, value: string) => void
}
type DocumentTableCardProps = {
documents: Document[],
setDocuments: React.Dispatch<React.SetStateAction<Document[]>>
}
const DocumentStorageTableRow = ({document, handleDocumentUpdate}: DocumentLineProps): JSX.Element =>
{
return(
<TableRow key={document.id}>
<TableCell><MDTypography> {document.name}</MDTypography></TableCell>
<TableCell><MDTypography> {document.date_uploaded}</MDTypography></TableCell>
<TableCell>
{ document.processed ? (
<IconButton onClick={() => console.log('clicked')}>
<CheckCircle color="success" />
</IconButton>
):(
<IconButton onClick={() => console.log('clicked')}>
<Pending color="disabled" />
</IconButton>
)}
</TableCell>
<TableCell >
<Switch checked={document.active} disabled={true} onChange={(event) => handleDocumentUpdate(document.id, event.target.value)} />
</TableCell>
</TableRow>
)
}
const CompanyDocumentStorageTableCard =({documents, setDocuments}: DocumentTableCardProps): JSX.Element => {
const handleDocumentUpdate = async(document_id: number, value: string): Promise<void> => {
console.log(document_id, value)
// if(field === 'delete'){
// await axiosInstance.delete(`/company_users`, {
// data: {'email':email}
// })
// }else {
// await axiosInstance.post(`/company_users`, {
// 'email':email,
// 'field': field,
// 'value': value
// });
// }
// // get all of th edata again
// try{
// const {data, }: AxiosResponse<AccountType[]> = await axiosInstance.get(`/company_users`);
// setUsers(data.map((item) => new Account({
// email: item.email,
// first_name: item.first_name,
// last_name: item.last_name,
// is_company_manager: item.is_company_manager,
// has_password: item.has_usable_password,
// is_active: item.is_active,
// company: undefined
// })))
// }catch(error){
// console.log(error)
// }
}
async function getUploadedDocuments(){
try{
const {data, }: AxiosResponse<DocumentType[]> = await axiosInstance.get(`/documents/`);
setDocuments(data.map((item) => new Document({
id: item.id,
name: item.file.replace(/^.*[\\\/]/, ''),
date_uploaded: item.created.substring(0,10),
active: item.active,
processed: item.processed,
})))
}catch(error){
console.log(error)
}
}
useEffect(()=>{
getUploadedDocuments();
}, [])
return (
<Card sx={{mt:1}}>
<CardContent>
<MDTypography variant="h3">
Your documents in the company workspace
</MDTypography>
</CardContent>
<CardContent>
<PaginatedTable
data={documents}
columns={[
{key: 'name', label: 'Name'},
{key: 'date_uploaded', label: 'Date Uploaded'},
{
key: 'processed',
label: 'Processed',
render: (value) =>( value ? (
<IconButton onClick={() => console.log('clicked')}>
<CheckCircle color="success" />
</IconButton>
):(
<IconButton onClick={() => console.log('clicked')}>
<Pending color="disabled" />
</IconButton>
))},
{
key: 'active',
label: 'Active',
render: (value) => (
<Switch checked={value} disabled={true} />
)
},
]}
/>
</CardContent>
</Card>
)
}
const UserDocumentStorageTableCard = ({}): JSX.Element => {
return(
<Card sx={{mt:1}}>
<CardContent>
<MDTypography variant="h3">
Your documents in the your personal workspace
</MDTypography>
</CardContent>
<CardContent>
<MDTypography>This will become available shortly</MDTypography>
{/* <TableContainer component={Paper}>
<Table >
<TableHead sx={{ display: "table-header-group" }}>
<TableRow>
<TableCell><MDTypography>Name</MDTypography></TableCell>
<TableCell><MDTypography>Date Uploaded</MDTypography></TableCell>
<TableCell><MDTypography>Processed</MDTypography></TableCell>
<TableCell><MDTypography>Active</MDTypography></TableCell>
</TableRow>
</TableHead>
<TableBody>
<MDTypography>This will become available shortly</MDTypography>
</TableBody>
</Table>
</TableContainer> */}
</CardContent>
</Card>
)
}
type DocumentSubmitValues = {
document: File
}
const DocumentUploadCard = ({}): JSX.Element => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const initialValues = {'file': '',}
const handleDocumentUpload = async ({document}: DocumentSubmitValues): Promise<void> => {
console.log(selectedFile)
if(selectedFile){
try{
const reader = new FileReader();
reader.onload = () => {
const base64File = reader.result?.toString().split(',')[1];
axiosInstance.post('/documents/', {
file: selectedFile
},
{
headers: {
'Content-Type': 'multipart/form-data',
},
})
}
reader.readAsDataURL(selectedFile)
// TODO set the documents here
}
finally{
}
}
}
// const handleDocumentUpload = async ({email}: InviteValues, {resetForm}: any): Promise<void> => {
// try{
// await axiosInstance.post('/documents/', {
// 'file': file
// })
// getCompanyUsers()
// } catch{
// // put a message here
// }
// resetForm();
// }
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setSelectedFile(event.target.files[0]);
}
};
return(
<Card sx={{mt:1}}>
<CardContent>
<MDTypography variant="h3">
Upload a Document
</MDTypography>
</CardContent>
<Divider />
<CardContent>
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<MDButton
component="label"
variant="contained"
startIcon={<CloudUpload />}
>
Select File
<input type="file" hidden onChange={handleFileChange} />
</MDButton>
{selectedFile && (
<>
<MDTypography sx={{ ml: 2 }}>{selectedFile.name}</MDTypography>
<MDButton
variant="contained"
color="primary"
sx={{ ml: 2 }}
onClick={handleDocumentUpload}
>
Upload
</MDButton>
</>
)}
</Box>
</Box>
</CardContent>
</Card>
)
}
const DocumentStoragePageInner = ({}): JSX.Element => {
const [documents, setDocuments] = useState<Document[]>([]);
return(
<>
<CompanyDocumentStorageTableCard documents={documents} setDocuments={setDocuments}/>
<UserDocumentStorageTableCard />
<DocumentUploadCard />
</>
)
}
const DocumentStoragePage = ({}): JSX.Element => {
return (
<PageWrapperLayout>
<Header2 />
<MDBox sx={{mt:12}}>
</MDBox>
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
<DocumentStoragePageInner />
<Footer />
</MDBox>
</PageWrapperLayout>
)
}
export default DocumentStoragePage;

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import MDBox from "../../ui-kit/components/MDBox"; import MDBox from "../../ui-kit/components/MDBox";
import { CardContent, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material"; import { Card, CardContent, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
import { Feedback, FeedbackType } from "../../data"; import { Feedback, FeedbackType } from "../../data";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { axiosInstance } from "../../../axiosApi"; import { axiosInstance } from "../../../axiosApi";
@@ -9,21 +9,7 @@ import PageWrapperLayout from "../../components/PageWrapperLayout/PageWrapperLay
import Footer from "../../components/Footer/Footer"; import Footer from "../../components/Footer/Footer";
import MDTypography from "../../ui-kit/components/MDTypography"; import MDTypography from "../../ui-kit/components/MDTypography";
import FeedbackSubmitCard from "../../components/FeedbackSubmitCard/FeedbackSubmitCard"; import FeedbackSubmitCard from "../../components/FeedbackSubmitCard/FeedbackSubmitCard";
import PaginatedTable from "../../components/PaginatedTable/PaginatedTable";
type FeedbackLineProps = {
feedback: Feedback
}
const FeedbackLine = ({feedback}: FeedbackLineProps): JSX.Element => {
return(
<TableRow key={feedback.id}>
<TableCell><MDTypography>{feedback.title}</MDTypography></TableCell>
<TableCell><MDTypography>{feedback.category}</MDTypography></TableCell>
<TableCell><MDTypography>{feedback.text}</MDTypography></TableCell>
<TableCell><MDTypography>{feedback.status}</MDTypography></TableCell>
</TableRow>
)
}
type FeedbackTableCardProps = { type FeedbackTableCardProps = {
feedbacks: Feedback[], feedbacks: Feedback[],
@@ -54,25 +40,27 @@ const FeedbackTableCard =({feedbacks, setFeedbacks}: FeedbackTableCardProps): JS
getCompanyUsers(); getCompanyUsers();
}, []) }, [])
return( return(
<Card sx={{mt:1}}>
<CardContent> <CardContent>
<TableContainer component={Paper}> <MDTypography variant="h3">
<Table > Feedback
<TableHead sx={{ display: "table-header-group" }}>
<TableRow>
<TableCell><MDTypography>Title</MDTypography></TableCell>
<TableCell><MDTypography>Category</MDTypography></TableCell>
<TableCell><MDTypography>Text</MDTypography></TableCell>
<TableCell><MDTypography>Status</MDTypography></TableCell>
</TableRow>
</TableHead>
<TableBody>
{feedbacks.map((feedback) => <FeedbackLine key={feedback.id} feedback={feedback}/>)}
</TableBody>
</Table>
</TableContainer>
</MDTypography>
</CardContent> </CardContent>
<CardContent>
<PaginatedTable
data={feedbacks}
columns={[
{key: 'title', label: 'Title'},
{key: 'category', label: 'Category'},
{key: 'text', label: 'Text'},
{key: 'status', label: 'Status'},
]}
/>
</CardContent>
</Card>
) )
} }

View File

@@ -1,17 +1,38 @@
import { Form, Formik } from 'formik'; import { Form, Formik } from 'formik';
import React, { Dispatch, PropsWithChildren, SetStateAction, useState } from 'react'; import React, { useRef } from 'react';
import CustomPasswordField from '../../components/CustomPasswordField/CustomPasswordField';
import { Button } from '@mui/material'; import { Card, CardContent, Divider } from '@mui/material';
import { axiosInstance } from '../../../axiosApi'; import { axiosInstance, cleanAxiosInstance } from '../../../axiosApi';
import CustomToastMessage from '../../components/CustomToastMessage/CustomeToastMessage'; import CustomToastMessage from '../../components/CustomToastMessage/CustomeToastMessage';
import PageWrapperLayout from '../../components/PageWrapperLayout/PageWrapperLayout';
import MDBox from '../../ui-kit/components/MDBox';
import background from '../../../bg.jpeg'
import { Col, Row } from 'react-bootstrap';
import MDTypography from '../../ui-kit/components/MDTypography';
import CustomTextField from '../../components/CustomTextField/CustomTextField';
import MDButton from '../../ui-kit/components/MDButton';
import { useNavigate } from 'react-router-dom';
import ReCAPTCHA from 'react-google-recaptcha';
export type PasswordResetValues = { export type PasswordResetValues = {
password1: string; password1: string;
password2: string; password2: string;
}; };
export type EmailPasswordResetValues = {
email: string;
}
const PasswordReset = ({}): JSX.Element => { const PasswordReset = ({}): JSX.Element => {
const navigate = useNavigate();
const recaptchaRef = useRef<ReCAPTCHA>(null);
const handlePasswordReset = ({password1, password2}: PasswordResetValues): void => { const handlePasswordReset = ({password1, password2}: PasswordResetValues): void => {
try{ try{
// verify // verify
@@ -26,74 +47,166 @@ const PasswordReset = ({}): JSX.Element => {
<CustomToastMessage message={error as string} /> <CustomToastMessage message={error as string} />
} }
}
const handlePasswordResetEmail = ({email}: EmailPasswordResetValues): void => {
if(recaptchaRef.current){
const token = recaptchaRef.current.getValue();
if (!token) {
try{
cleanAxiosInstance.post('user/reset_password',
{
'email': email,
'recaptchaToken': token
}
);
// navigate to another page now
navigate('/password_reset_confirmation')
}catch(error){
console.log('error')
}
}
}
} }
return ( return (
<div className='main-content' style={{'height': '100%', minHeight: '100vh', display: 'flex', flexDirection: 'column'}}> <PageWrapperLayout>
<div className='container my-auto'> <MDBox sx={{'height': '100%', minHeight: '100vh', display: 'flex', flexDirection: 'column', backgroundImage: `url(${background})`,backgroundSize: "cover",
<div className='row'> backgroundRepeat: "no-repeat",}}>
<div className='col -lg-4 col-md-8 col-12 mx-auto'>
<div className='card z-index-0 fadeIn3 fadeInBottom'> <MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
<div className='card-header p-0 position-relative mt-n4 mx-3 z-index-2'> <Row>
<div className='bg-gradient-dark shadow-dark border-radius-lg py-3 pe-1'> <Col className='col -lg-4 col-md-8 col-12 mx-auto'>
<h4 className='text-white font-weight-bold text-center'>Password Reset</h4> <Card sx={{mt:30}} >
</div> <CardContent>
</div> <MDTypography variant="h3">
<div className='card-body text-center'> Reset Password
</MDTypography>
</CardContent>
<Divider />
<Formik <Formik
initialValues={{ initialValues={{
password1: '', email: '',
password2: '',
}} }}
onSubmit={handlePasswordReset}> onSubmit={handlePasswordResetEmail}
>
{(formik) => ( {(formik) => (
<Form> <Form>
<div className='row'> <div className='row'>
<div className='col'> <div className='col'>
<CustomPasswordField <CustomTextField
label='Password' label='Email'
name="password1" name="email"
changeHandler={(e) => formik.setFieldValue('password1', e.target.value)} /> changeHandler={(e) => formik.setFieldValue('email', e.target.value)} isMultline={false}/>
</div> </div>
</div> </div>
<div className='row'> <div className='row'>
<div className='col'> <div className='col'>
<CustomPasswordField <ReCAPTCHA
label='Confirm Password' ref={recaptchaRef}
name="password2" sitekey = "6LfENu4qAAAAAFtPejcrP3dwBDxcRPjqi7RhytJJ"
changeHandler={(e) => formik.setFieldValue('password2', e.target.value)} /> size="invisible"
/>
</div> <MDButton
</div>
<div className='row'>
<div className='col'>
<Button
type={'submit'} type={'submit'}
disabled={ formik.isSubmitting} fullWidth
// type={'submit'}
// loading={formik.isSubmitting}
// disabled={
// !formik.isValid || !formik.dirty || formik.isSubmitting
// }
> >
Reset Password <MDTypography
</Button> as="h6">
Rest Password
</MDTypography>
</MDButton>
</div> </div>
</div> </div>
</Form> </Form>
)} )}
</Formik> </Formik>
</div> </Card>
</div> </Col>
</div> </Row>
</div> </MDBox>
</div> </MDBox>
</div>
</PageWrapperLayout>
// <div className='main-content' style={{'height': '100%', minHeight: '100vh', display: 'flex', flexDirection: 'column'}}>
// <div className='container my-auto'>
// <div className='row'>
// <div className='col -lg-4 col-md-8 col-12 mx-auto'>
// <div className='card z-index-0 fadeIn3 fadeInBottom'>
// <div className='card-header p-0 position-relative mt-n4 mx-3 z-index-2'>
// <div className='bg-gradient-dark shadow-dark border-radius-lg py-3 pe-1'>
// <h4 className='text-white font-weight-bold text-center'>Password Reset</h4>
// </div>
// </div>
// <div className='card-body text-center'>
// <Formik
// initialValues={{
// password1: '',
// password2: '',
// }}
// onSubmit={handlePasswordReset}>
// {(formik) => (
// <Form>
// <div className='row'>
// <div className='col'>
// <CustomPasswordField
// label='Password'
// name="password1"
// changeHandler={(e) => formik.setFieldValue('password1', e.target.value)} />
// </div>
// </div>
// <div className='row'>
// <div className='col'>
// <CustomPasswordField
// label='Confirm Password'
// name="password2"
// changeHandler={(e) => formik.setFieldValue('password2', e.target.value)} />
// </div>
// </div>
// <div className='row'>
// <div className='col'>
// <Button
// type={'submit'}
// disabled={ formik.isSubmitting}
// // type={'submit'}
// // loading={formik.isSubmitting}
// // disabled={
// // !formik.isValid || !formik.dirty || formik.isSubmitting
// // }
// >
// Reset Password
// </Button>
// </div>
// </div>
// </Form>
// )}
// </Formik>
// </div>
// </div>
// </div>
// </div>
// </div>
// </div>
); );
}; };

View File

@@ -0,0 +1,59 @@
import { Form, Formik } from 'formik';
import React, { Dispatch, PropsWithChildren, SetStateAction, useState } from 'react';
import CustomPasswordField from '../../components/CustomPasswordField/CustomPasswordField';
import { Button, Card, CardContent, Divider } from '@mui/material';
import { axiosInstance } from '../../../axiosApi';
import CustomToastMessage from '../../components/CustomToastMessage/CustomeToastMessage';
import PageWrapperLayout from '../../components/PageWrapperLayout/PageWrapperLayout';
import MDBox from '../../ui-kit/components/MDBox';
import background from '../../../bg.jpeg'
import { Col, Row } from 'react-bootstrap';
import MDTypography from '../../ui-kit/components/MDTypography';
import { valuesIn } from 'lodash';
import CustomTextField from '../../components/CustomTextField/CustomTextField';
import MDButton from '../../ui-kit/components/MDButton';
import { useNavigate } from 'react-router-dom';
export type PasswordResetValues = {
password1: string;
password2: string;
};
export type EmailPasswordResetValues = {
email: string;
}
const PasswordResetConfirmation = ({}): JSX.Element => {
return (
<PageWrapperLayout>
<MDBox sx={{'height': '100%', minHeight: '100vh', display: 'flex', flexDirection: 'column', backgroundImage: `url(${background})`,backgroundSize: "cover",
backgroundRepeat: "no-repeat",}}>
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
<Row>
<Col className='col -lg-4 col-md-8 col-12 mx-auto'>
<Card sx={{mt:30}} >
<CardContent>
<MDTypography variant="h3">
Reset Password Confirmation
</MDTypography>
</CardContent>
<Divider />
<MDTypography>
Check your email for a link to set your password!
</MDTypography>
</Card>
</Col>
</Row>
</MDBox>
</MDBox>
</PageWrapperLayout>
);
};
export default PasswordResetConfirmation;

View File

@@ -91,6 +91,8 @@ const SignIn = ({}): JSX.Element => {
return ( return (
<PageWrapperLayout> <PageWrapperLayout>
<MDBox sx={{'height': '100%', minHeight: '100vh', display: 'flex', flexDirection: 'column', backgroundImage: `url(${background})`,backgroundSize: "cover", <MDBox sx={{'height': '100%', minHeight: '100vh', display: 'flex', flexDirection: 'column', backgroundImage: `url(${background})`,backgroundSize: "cover",
@@ -140,15 +142,34 @@ const SignIn = ({}): JSX.Element => {
</div> </div>
</div> </div>
<div className='row'> <div className='row'>
<div className='col'> <div className='col-6'>
<MDButton <MDButton
type={'submit'} type={'submit'}
fullWidth
> >
<MDTypography
as="h6">
Sign In Sign In
</MDTypography>
</MDButton> </MDButton>
</div> </div>
{/* <div className='col-6'>
<MDButton
fullWidth
onClick={() => navigate('/password_reset')}
>
<MDTypography
color="error"
as="h6"
>
Reset Password
</MDTypography>
</MDButton>
</div> */}
</div> </div>
</Form> </Form>