Compare commits
7 Commits
1306ba9ed1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 40db2495f2 | |||
| caf2c6481f | |||
| 95f351c5aa | |||
| 5c449e1036 | |||
| 2657334e0c | |||
| b120f85ccd | |||
| 0acfcc0d08 |
BIN
llm-fe/public/favicon.jpg
Normal file
BIN
llm-fe/public/favicon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
@@ -1,21 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Chat app by AI ML Operations, LLC"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.jpg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Chat app by AI ML Operations, LLC" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
@@ -24,11 +22,12 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Chat by AI ML Operations. LCC</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
<title>Chat by AI ML Operations. LCC</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
@@ -38,5 +37,6 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -19,61 +19,68 @@ import DocumentStoragePage from './llm-fe/pages/DocumentStoragePage/DocumentStor
|
||||
import Account2 from './llm-fe/pages/Account2/Account2';
|
||||
import AnalyticsPage from './llm-fe/pages/Analytics/Analytics';
|
||||
import PasswordResetConfirmation from './llm-fe/pages/PasswordResetConfirmation/PasswordReset';
|
||||
import GlobalThemeWrapper from './llm-fe/components/GlobalThemeWrapper/GlobalThemeWrapper';
|
||||
import Tracker from './llm-fe/components/Tracker/Tracker';
|
||||
|
||||
const ProtectedRoutes = () => {
|
||||
const { authenticated, needsNewPassword, loading } = useContext(AuthContext);
|
||||
if(loading){
|
||||
return(
|
||||
if (loading) {
|
||||
return (
|
||||
<div>Loading</div>
|
||||
)
|
||||
}
|
||||
if(!authenticated) return <Navigate to='/signin/' replace />
|
||||
if (!authenticated) return <Navigate to='/signin/' replace />
|
||||
//if(!needsNewPassword) return <Navigate to='/password_reset/' replace/>
|
||||
|
||||
return <Outlet key={Date.now()}/>
|
||||
return <Outlet key={Date.now()} />
|
||||
}
|
||||
|
||||
|
||||
|
||||
class App extends Component {
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='site'>
|
||||
<main>
|
||||
<div className="main-container">
|
||||
|
||||
|
||||
<Routes>
|
||||
<Route path='*' element={<NotFound />} />
|
||||
|
||||
<Route path={"/signin/"} Component={SignIn}/>
|
||||
<Route path={"/password_reset/"} Component={PasswordReset}/>
|
||||
<Route path={"/password_reset_confirmation/"} Component={PasswordResetConfirmation}/>
|
||||
<Route path={'/set_password/'} Component={SetPassword}/>
|
||||
|
||||
<GlobalThemeWrapper>
|
||||
<Tracker />
|
||||
<div className='site'>
|
||||
<main>
|
||||
<div className="main-container">
|
||||
|
||||
|
||||
<Route element={<ProtectedRoutes />}>
|
||||
<Route path={"/"} index={true} Component={AsyncDashboard2}/>
|
||||
<Route path={"/account/"} Component={Account2}/>
|
||||
<Route path={"/document_storage"} Component={DocumentStoragePage}/>
|
||||
<Route path={"/terms_of_service/"} Component={TermsOfService}/>
|
||||
<Route path={"/feedback/"} Component={FeedbackPage2}/>
|
||||
<Route path={"/analytics/"} Component={AnalyticsPage} />
|
||||
|
||||
</Route>
|
||||
<Routes>
|
||||
<Route path='*' element={<NotFound />} />
|
||||
|
||||
<Route path={"/signin/"} Component={SignIn} />
|
||||
<Route path={"/password_reset/"} Component={PasswordReset} />
|
||||
<Route path={"/password_reset_confirmation/"} Component={PasswordResetConfirmation} />
|
||||
<Route path={'/set_password/'} Component={SetPassword} />
|
||||
|
||||
|
||||
|
||||
<Route element={<ProtectedRoutes />}>
|
||||
<Route path={"/"} index={true} Component={AsyncDashboard2} />
|
||||
<Route path={"/account/"} Component={Account2} />
|
||||
<Route path={"/document_storage"} Component={DocumentStoragePage} />
|
||||
<Route path={"/terms_of_service/"} Component={TermsOfService} />
|
||||
<Route path={"/feedback/"} Component={FeedbackPage2} />
|
||||
<Route path={"/analytics/"} Component={AnalyticsPage} />
|
||||
|
||||
</Route>
|
||||
|
||||
</Routes>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</GlobalThemeWrapper>
|
||||
|
||||
|
||||
</Routes>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,95 +1,97 @@
|
||||
import axios from "axios";
|
||||
const Cookies = require('js-cookie');
|
||||
const Cookies = require("js-cookie");
|
||||
|
||||
//const baseURL = 'http://127.0.0.1:8011/api/';
|
||||
const baseURL = 'https://chatbackend.aimloperations.com/api/';
|
||||
const baseURL = "http://localhost:8011/api/";
|
||||
//const baseURL = 'https://chatbackend.aimloperations.com/api/';
|
||||
//const baseURL = process.env.REACT_APP_BACKEND_REST_API_BASE_URL;
|
||||
|
||||
export const axiosInstance = axios.create({
|
||||
baseURL: baseURL,
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
"Authorization": 'JWT ' + localStorage.getItem('access_token'),
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
baseURL: baseURL,
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
Authorization: "JWT " + localStorage.getItem("access_token"),
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
export const cleanAxiosInstance = axios.create({
|
||||
baseURL: baseURL,
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
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,
|
||||
}
|
||||
);
|
||||
baseURL: baseURL,
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
"X-CSRFToken": Cookies.get("csrftoken"), // Include CSRF token in headers
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.request.use(config => {
|
||||
config.timeout = 100000;
|
||||
return config;
|
||||
})
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
config.timeout = 100000;
|
||||
return config;
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
const originalRequest = error.config;
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Prevent infinite loop
|
||||
if (error.response.status === 401 && originalRequest.url === baseURL+'/token/refresh/') {
|
||||
window.location.href = '/signin/';
|
||||
//console.log('remove the local storage here')
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if(error.response.data.code === "token_not_valid" &&
|
||||
error.response.status == 401 &&
|
||||
error.response.statusText == 'Unauthorized')
|
||||
{
|
||||
const refresh_token = localStorage.getItem('refresh_token');
|
||||
|
||||
if (refresh_token){
|
||||
const tokenParts = JSON.parse(atob(refresh_token.split('.')[1]));
|
||||
|
||||
const now = Math.ceil(Date.now() / 1000);
|
||||
//console.log(tokenParts.exp)
|
||||
|
||||
if(tokenParts.exp > now){
|
||||
return axiosInstance.post('/token/refresh/', {refresh: refresh_token}).then((response) => {
|
||||
localStorage.setItem('access_token', response.data.access);
|
||||
localStorage.setItem('refresh_token', response.data.refresh);
|
||||
|
||||
axiosInstance.defaults.headers['Authorization'] = 'JWT ' + response.data.access;
|
||||
originalRequest.headers['Authorization'] = 'JWT ' + response.data.access;
|
||||
|
||||
return axiosInstance(originalRequest);
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
});
|
||||
|
||||
}else{
|
||||
console.log('Refresh token is expired');
|
||||
window.location.href = '/signin/';
|
||||
|
||||
}
|
||||
}else {
|
||||
console.log('Refresh token not available');
|
||||
window.location.href = '/signin/';
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
return Promise.reject(error);
|
||||
// Prevent infinite loop
|
||||
if (
|
||||
error.response.status === 401 &&
|
||||
originalRequest.url === baseURL + "/token/refresh/"
|
||||
) {
|
||||
window.location.href = "/signin/";
|
||||
//console.log('remove the local storage here')
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
error.response.data.code === "token_not_valid" &&
|
||||
error.response.status == 401 &&
|
||||
error.response.statusText == "Unauthorized"
|
||||
) {
|
||||
const refresh_token = localStorage.getItem("refresh_token");
|
||||
|
||||
if (refresh_token) {
|
||||
const tokenParts = JSON.parse(atob(refresh_token.split(".")[1]));
|
||||
|
||||
const now = Math.ceil(Date.now() / 1000);
|
||||
//console.log(tokenParts.exp)
|
||||
|
||||
if (tokenParts.exp > now) {
|
||||
return axiosInstance
|
||||
.post("/token/refresh/", { refresh: refresh_token })
|
||||
.then((response) => {
|
||||
localStorage.setItem("access_token", response.data.access);
|
||||
localStorage.setItem("refresh_token", response.data.refresh);
|
||||
|
||||
axiosInstance.defaults.headers["Authorization"] =
|
||||
"JWT " + response.data.access;
|
||||
originalRequest.headers["Authorization"] =
|
||||
"JWT " + response.data.access;
|
||||
|
||||
return axiosInstance(originalRequest);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
} else {
|
||||
console.log("Refresh token is expired");
|
||||
window.location.href = "/signin/";
|
||||
}
|
||||
} else {
|
||||
console.log("Refresh token not available");
|
||||
window.location.href = "/signin/";
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,291 +1,361 @@
|
||||
import { Box, Button, InputAdornment, TextField, Typography } from '@mui/material';
|
||||
import React, {useRef, useContext, useEffect, useState } from 'react';
|
||||
import { Conversation, ConversationPrompt, ConversationPromptType, ConversationType } from '../../data';
|
||||
import { axiosInstance } from '../../../axiosApi';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import ConversationDetailCard from '../ConversationDetailCard/ConversationDetailCard';
|
||||
import { ErrorMessage, Field, Form, Formik } from 'formik';
|
||||
import { WebSocketContext } from '../../contexts/WebSocketContext';
|
||||
import { AccountContext } from '../../contexts/AccountContext';
|
||||
import * as Yup from 'yup';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { AttachFile, Send } from '@mui/icons-material';
|
||||
import Markdown from 'markdown-to-jsx';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React, { useRef, useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
Conversation,
|
||||
ConversationPrompt,
|
||||
ConversationPromptType,
|
||||
ConversationType,
|
||||
} from "../../data";
|
||||
import { axiosInstance } from "../../../axiosApi";
|
||||
import { AxiosResponse } from "axios";
|
||||
import ConversationDetailCard from "../ConversationDetailCard/ConversationDetailCard";
|
||||
import { ErrorMessage, Field, Form, Formik } from "formik";
|
||||
import { WebSocketContext } from "../../contexts/WebSocketContext";
|
||||
import { AccountContext } from "../../contexts/AccountContext";
|
||||
import * as Yup from "yup";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { AttachFile, Send } from "@mui/icons-material";
|
||||
import Markdown from "markdown-to-jsx";
|
||||
|
||||
type RenderMessageProps= {
|
||||
response: string
|
||||
index: number
|
||||
type RenderMessageProps = {
|
||||
response: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
type AsyncChatProps = {
|
||||
selectedConversation: number | undefined
|
||||
conversationTitle: string
|
||||
conversations: Conversation[]
|
||||
setConversations: React.Dispatch<React.SetStateAction<Conversation[]>>
|
||||
setSelectedConversation: React.Dispatch<React.SetStateAction<number | undefined>>
|
||||
drawerWidth: number;
|
||||
}
|
||||
selectedConversation: number | undefined;
|
||||
conversationTitle: string;
|
||||
conversations: Conversation[];
|
||||
setConversations: React.Dispatch<React.SetStateAction<Conversation[]>>;
|
||||
setSelectedConversation: React.Dispatch<
|
||||
React.SetStateAction<number | undefined>
|
||||
>;
|
||||
drawerWidth: number;
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
prompt: Yup.string().min(1, "Need to have at least one character").required("This is requried")
|
||||
}
|
||||
)
|
||||
prompt: Yup.string()
|
||||
.min(1, "Need to have at least one character")
|
||||
.required("This is requried"),
|
||||
});
|
||||
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
clip: 'rect(0 0 0 0)',
|
||||
clipPath: 'inset(50%)',
|
||||
height: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
width: 1,
|
||||
});
|
||||
const VisuallyHiddenInput = styled("input")({
|
||||
clip: "rect(0 0 0 0)",
|
||||
clipPath: "inset(50%)",
|
||||
height: 1,
|
||||
overflow: "hidden",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: "nowrap",
|
||||
width: 1,
|
||||
});
|
||||
|
||||
const AsyncChat = ({selectedConversation, conversationTitle, conversations, setConversations, setSelectedConversation, drawerWidth}: AsyncChatProps): JSX.Element => {
|
||||
const AsyncChat = ({
|
||||
selectedConversation,
|
||||
conversationTitle,
|
||||
conversations,
|
||||
setConversations,
|
||||
setSelectedConversation,
|
||||
drawerWidth,
|
||||
}: AsyncChatProps): JSX.Element => {
|
||||
const messageEndRef = useRef(null);
|
||||
|
||||
|
||||
const messageEndRef = useRef(null);
|
||||
|
||||
const [conversationDetails, setConversationDetails] = useState<ConversationPrompt[]>([])
|
||||
const [disableInput, setDisableInput] = useState<boolean>(false);
|
||||
const [conversationDetails, setConversationDetails] = useState<
|
||||
ConversationPrompt[]
|
||||
>([]);
|
||||
const [disableInput, setDisableInput] = useState<boolean>(false);
|
||||
|
||||
const [subscribe, unsubscribe, socket, sendMessage] = useContext(WebSocketContext)
|
||||
const { account, setAccount } = useContext(AccountContext)
|
||||
const messageRef = useRef('')
|
||||
const messageResponsePart = useRef(0);
|
||||
const conversationRef = useRef(conversationDetails)
|
||||
const [stateMessage, setStateMessage] = useState<string>('')
|
||||
const selectedConversationRef = useRef<undefined | number>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
/* register a consistent channel name for identifing this chat messages */
|
||||
const channelName = `ACCOUNT_ID_${account?.email}`
|
||||
const [subscribe, unsubscribe, socket, sendMessage] =
|
||||
useContext(WebSocketContext);
|
||||
const { account, setAccount } = useContext(AccountContext);
|
||||
const messageRef = useRef("");
|
||||
const messageResponsePart = useRef(0);
|
||||
const conversationRef = useRef(conversationDetails);
|
||||
const [stateMessage, setStateMessage] = useState<string>("");
|
||||
const selectedConversationRef = useRef<undefined | number>(undefined);
|
||||
|
||||
/* subscribe to channel and register callback */
|
||||
subscribe(channelName, (message: string) => {
|
||||
/* when a message is received just add it to the UI */
|
||||
|
||||
if (message === 'END_OF_THE_STREAM_ENDER_GAME_42'){
|
||||
messageResponsePart.current = 0
|
||||
|
||||
conversationRef.current.pop()
|
||||
|
||||
//handleAssistantPrompt({prompt: messageRef.current})
|
||||
setConversationDetails([...conversationRef.current, new ConversationPrompt({message: `${messageRef.current}`, user_created:false})])
|
||||
messageRef.current = ''
|
||||
setStateMessage('')
|
||||
useEffect(() => {
|
||||
/* register a consistent channel name for identifing this chat messages */
|
||||
const channelName = `ACCOUNT_ID_${account?.email}`;
|
||||
|
||||
/* subscribe to channel and register callback */
|
||||
subscribe(channelName, (message: string) => {
|
||||
/* when a message is received just add it to the UI */
|
||||
|
||||
if (message === "END_OF_THE_STREAM_ENDER_GAME_42") {
|
||||
messageResponsePart.current = 0;
|
||||
|
||||
conversationRef.current.pop();
|
||||
|
||||
//handleAssistantPrompt({prompt: messageRef.current})
|
||||
setConversationDetails([
|
||||
...conversationRef.current,
|
||||
new ConversationPrompt({
|
||||
message: `${messageRef.current}`,
|
||||
user_created: false,
|
||||
}),
|
||||
]);
|
||||
messageRef.current = "";
|
||||
setStateMessage("");
|
||||
} else if (message === "START_OF_THE_STREAM_ENDER_GAME_42") {
|
||||
messageResponsePart.current = 2;
|
||||
} else if (message === "CONVERSATION_ID") {
|
||||
messageResponsePart.current = 1;
|
||||
} else {
|
||||
if (messageResponsePart.current === 1) {
|
||||
// this has to do with the conversation id
|
||||
if (!selectedConversation) {
|
||||
//console.log("we have a new conversation")
|
||||
setSelectedConversation(Number(message));
|
||||
}
|
||||
} else if (messageResponsePart.current === 2) {
|
||||
let contentToAdd = message;
|
||||
try {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
if (
|
||||
parsedMessage &&
|
||||
typeof parsedMessage === "object" &&
|
||||
parsedMessage.type
|
||||
) {
|
||||
switch (parsedMessage.type) {
|
||||
case "text":
|
||||
contentToAdd = parsedMessage.content;
|
||||
break;
|
||||
case "plot":
|
||||
contentToAdd = `<plot format="${parsedMessage.format}" image="${parsedMessage.image}"></plot>`;
|
||||
break;
|
||||
case "error":
|
||||
contentToAdd = `<error content="${parsedMessage.content}"></error>`;
|
||||
break;
|
||||
default:
|
||||
// contentToAdd is already `message`
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (message === 'START_OF_THE_STREAM_ENDER_GAME_42'){
|
||||
messageResponsePart.current = 2
|
||||
} catch (e) {
|
||||
// Not a JSON object, treat as raw string.
|
||||
// contentToAdd is already `message`.
|
||||
}
|
||||
|
||||
}else if (message === 'CONVERSATION_ID'){
|
||||
messageResponsePart.current = 1
|
||||
}else{
|
||||
if (messageResponsePart.current === 1){
|
||||
// this has to do with the conversation id
|
||||
if(!selectedConversation){
|
||||
//console.log("we have a new conversation")
|
||||
setSelectedConversation(Number(message))
|
||||
}
|
||||
}
|
||||
else if (messageResponsePart.current === 2){
|
||||
messageRef.current += message
|
||||
setStateMessage(messageRef.current)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return () => {
|
||||
/* unsubscribe from channel during cleanup */
|
||||
unsubscribe(channelName)
|
||||
messageRef.current += contentToAdd;
|
||||
setStateMessage(messageRef.current);
|
||||
}
|
||||
}, [account, subscribe, unsubscribe])
|
||||
|
||||
|
||||
async function GetConversationDetails(){
|
||||
if(selectedConversation){
|
||||
|
||||
try{
|
||||
//console.log('GetConversationDetails')
|
||||
//setPromptProcessing(true)
|
||||
selectedConversationRef.current = selectedConversation;
|
||||
const {data, }: AxiosResponse<ConversationPromptType[]> = await axiosInstance.get(`conversation_details?conversation_id=${selectedConversation}`)
|
||||
|
||||
const tempConversations: ConversationPrompt[] = data.map((item) => new ConversationPrompt({
|
||||
message: item.message,
|
||||
user_created: item.user_created,
|
||||
created_timestamp: item.created_timestamp
|
||||
}))
|
||||
conversationRef.current = tempConversations
|
||||
setConversationDetails(tempConversations)
|
||||
|
||||
}finally{
|
||||
//setPromptProcessing(false)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
GetConversationDetails();
|
||||
}, [selectedConversation])
|
||||
|
||||
|
||||
// Function to render each message
|
||||
|
||||
const renderMessage = ({response, index}: RenderMessageProps) => (
|
||||
<div key={index}>
|
||||
<p>{response}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
type PromptValues = {
|
||||
prompt: string;
|
||||
file: Blob | null;
|
||||
fileType: string | null;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
/* unsubscribe from channel during cleanup */
|
||||
unsubscribe(channelName);
|
||||
};
|
||||
}, [account, subscribe, unsubscribe]);
|
||||
|
||||
const handlePromptSubmit = async ({prompt, file, fileType}: PromptValues, {resetForm}: any): Promise<void> => {
|
||||
|
||||
// send the prompt to be saved
|
||||
console.log(fileType)
|
||||
try{
|
||||
const tempConversations: ConversationPrompt[] = [...conversationDetails, new ConversationPrompt({message: prompt, user_created:true}), new ConversationPrompt({message: '', user_created:false})]
|
||||
conversationRef.current = tempConversations
|
||||
setConversationDetails(tempConversations)
|
||||
// TODO: add the file here
|
||||
console.log(`Sending message. ${prompt} ${selectedConversation} ${fileType}`)
|
||||
sendMessage(prompt, selectedConversation, file, fileType)
|
||||
resetForm();
|
||||
|
||||
|
||||
}catch(e){
|
||||
console.log(`error ${e}`)
|
||||
// TODO: make this user friendly
|
||||
}
|
||||
async function GetConversationDetails() {
|
||||
if (selectedConversation) {
|
||||
try {
|
||||
//console.log('GetConversationDetails')
|
||||
//setPromptProcessing(true)
|
||||
selectedConversationRef.current = selectedConversation;
|
||||
const { data }: AxiosResponse<ConversationPromptType[]> =
|
||||
await axiosInstance.get(
|
||||
`conversation_details?conversation_id=${selectedConversation}`,
|
||||
);
|
||||
|
||||
const tempConversations: ConversationPrompt[] = data.map(
|
||||
(item) =>
|
||||
new ConversationPrompt({
|
||||
message: item.message,
|
||||
user_created: item.user_created,
|
||||
created_timestamp: item.created_timestamp,
|
||||
}),
|
||||
);
|
||||
conversationRef.current = tempConversations;
|
||||
setConversationDetails(tempConversations);
|
||||
} finally {
|
||||
//setPromptProcessing(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
GetConversationDetails();
|
||||
}, [selectedConversation]);
|
||||
|
||||
return(
|
||||
<Box
|
||||
sx={{ flexGrow: 1, p: 3, width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||
ml: { sm: `${drawerWidth}px` }, mt: 5 }}
|
||||
position='fixed'
|
||||
// Function to render each message
|
||||
|
||||
const renderMessage = ({ response, index }: RenderMessageProps) => (
|
||||
<div key={index}>
|
||||
<p>{response}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
type PromptValues = {
|
||||
prompt: string;
|
||||
file: Blob | null;
|
||||
fileType: string | null;
|
||||
};
|
||||
|
||||
const handlePromptSubmit = async (
|
||||
{ prompt, file, fileType }: PromptValues,
|
||||
{ resetForm }: any,
|
||||
): Promise<void> => {
|
||||
// send the prompt to be saved
|
||||
console.log(fileType);
|
||||
try {
|
||||
const tempConversations: ConversationPrompt[] = [
|
||||
...conversationDetails,
|
||||
new ConversationPrompt({ message: prompt, user_created: true }),
|
||||
new ConversationPrompt({ message: "", user_created: false }),
|
||||
];
|
||||
conversationRef.current = tempConversations;
|
||||
setConversationDetails(tempConversations);
|
||||
// TODO: add the file here
|
||||
console.log(
|
||||
`Sending message. ${prompt} ${selectedConversation} ${fileType}`,
|
||||
);
|
||||
sendMessage(prompt, selectedConversation, file, fileType);
|
||||
resetForm();
|
||||
} catch (e) {
|
||||
console.log(`error ${e}`);
|
||||
// TODO: make this user friendly
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
p: 3,
|
||||
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||
ml: { sm: `${drawerWidth}px` },
|
||||
mt: 5,
|
||||
}}
|
||||
position="fixed"
|
||||
>
|
||||
<div className="card" style={{ height: "auto" }}>
|
||||
<div className="card-header">
|
||||
<div className="bg-gradient-dark shadow-dark border-radius-lg py-3 pe-1">
|
||||
<Typography
|
||||
variant="h6"
|
||||
style={{ flexGrow: 1, marginLeft: "1rem" }}
|
||||
className="text-white font-weight-bold"
|
||||
>
|
||||
{conversationTitle}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body bg-gradient-dark border-radius-lg py-3 pe-1">
|
||||
<Box
|
||||
sx={{
|
||||
mb: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: 700,
|
||||
overflow: "hidden",
|
||||
overflowY: "scroll",
|
||||
//justifyContent="flex-end" //# DO NOT USE THIS WITH 'scroll'
|
||||
}}
|
||||
>
|
||||
{selectedConversation ? (
|
||||
conversationDetails.map((convo_detail) =>
|
||||
convo_detail.message.length > 0 ? (
|
||||
<ConversationDetailCard
|
||||
message={convo_detail.message}
|
||||
user_created={convo_detail.user_created}
|
||||
key={convo_detail.id}
|
||||
/>
|
||||
) : (
|
||||
<ConversationDetailCard
|
||||
message={stateMessage}
|
||||
user_created={convo_detail.user_created}
|
||||
key={convo_detail.id}
|
||||
/>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<Markdown className="text-light text-center">
|
||||
Either select a previous conversation on start a new one.
|
||||
</Markdown>
|
||||
)}
|
||||
<div ref={messageEndRef} />
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-footer">
|
||||
<Formik
|
||||
initialValues={{
|
||||
prompt: "",
|
||||
file: null,
|
||||
fileType: null,
|
||||
}}
|
||||
onSubmit={handlePromptSubmit}
|
||||
validationSchema={validationSchema}
|
||||
>
|
||||
<div className="card" style= {{height: 'auto'}}>
|
||||
<div className='card-header'>
|
||||
|
||||
<div className='bg-gradient-dark shadow-dark border-radius-lg py-3 pe-1'>
|
||||
<Typography variant="h6" style={{ flexGrow: 1, marginLeft: '1rem' }} className='text-white font-weight-bold'>
|
||||
{conversationTitle}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body bg-gradient-dark border-radius-lg py-3 pe-1">
|
||||
<Box sx={{
|
||||
mb: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: 700,
|
||||
overflow: "hidden",
|
||||
overflowY: "scroll",
|
||||
//justifyContent="flex-end" //# DO NOT USE THIS WITH 'scroll'
|
||||
}}>
|
||||
{selectedConversation ?
|
||||
conversationDetails.map((convo_detail) =>
|
||||
convo_detail.message.length >0 ?
|
||||
<ConversationDetailCard message={convo_detail.message} user_created={convo_detail.user_created} key={convo_detail.id}/> : <ConversationDetailCard message={stateMessage} user_created={convo_detail.user_created} key={convo_detail.id}/>
|
||||
)
|
||||
:
|
||||
<Markdown className='text-light text-center'>Either select a previous conversation on start a new one.</Markdown>
|
||||
}
|
||||
<div ref={messageEndRef} />
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className='card-footer'>
|
||||
<Formik
|
||||
initialValues={{
|
||||
prompt: '',
|
||||
file: null,
|
||||
fileType: null,
|
||||
{(formik) => (
|
||||
<Form>
|
||||
<div
|
||||
className="row"
|
||||
style={{
|
||||
position: "sticky",
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<div className="col-12">
|
||||
<Field
|
||||
name={"prompt"}
|
||||
fullWidth
|
||||
as={TextField}
|
||||
label={"Prompt"}
|
||||
errorstring={<ErrorMessage name={"prompt"} />}
|
||||
size={"small"}
|
||||
role={undefined}
|
||||
tabIndex={-1}
|
||||
margin={"dense"}
|
||||
variant={"outlined"}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Button
|
||||
component="label"
|
||||
color={formik.values.file ? "success" : "inherit"}
|
||||
startIcon={<AttachFile />}
|
||||
role={undefined}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<VisuallyHiddenInput
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.txt"
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0];
|
||||
//console.log(file)
|
||||
if (file) {
|
||||
formik.setFieldValue("file", file);
|
||||
formik.setFieldValue("fileType", file.type);
|
||||
} else {
|
||||
formik.setFieldValue("file", null);
|
||||
formik.setFieldValue("fileType", null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<Send />}
|
||||
type={"submit"}
|
||||
disabled={!formik.isValid}
|
||||
></Button>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onSubmit={handlePromptSubmit}
|
||||
validationSchema={validationSchema}
|
||||
>
|
||||
{(formik) =>
|
||||
<Form>
|
||||
<div className='row' style={{
|
||||
position: 'sticky',
|
||||
bottom: 0
|
||||
}}>
|
||||
<div className='col-12'>
|
||||
<Field
|
||||
name={"prompt"}
|
||||
fullWidth
|
||||
as={TextField}
|
||||
label={'Prompt'}
|
||||
errorstring={<ErrorMessage name={"prompt"}/>}
|
||||
size={"small"}
|
||||
role={undefined}
|
||||
tabIndex={-1}
|
||||
|
||||
|
||||
margin={"dense"}
|
||||
variant={"outlined"}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position='end' >
|
||||
|
||||
<Button
|
||||
component="label"
|
||||
startIcon={<AttachFile/>}
|
||||
role={undefined}
|
||||
tabIndex={-1}
|
||||
|
||||
>
|
||||
<VisuallyHiddenInput
|
||||
type="file"
|
||||
accept='.csv,.xlsx,.txt'
|
||||
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0];
|
||||
//console.log(file)
|
||||
if (file) {
|
||||
formik.setFieldValue('file',file)
|
||||
formik.setFieldValue('fileType',file.type)
|
||||
} else {
|
||||
formik.setFieldValue('file',null)
|
||||
formik.setFieldValue('fileType',null)
|
||||
}
|
||||
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<Send />}
|
||||
type={'submit'}
|
||||
disabled={!formik.isValid}>
|
||||
|
||||
</Button>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
>
|
||||
</Field>
|
||||
</div>
|
||||
{/* <div className='col-1'>
|
||||
></Field>
|
||||
</div>
|
||||
{/* <div className='col-1'>
|
||||
<Button
|
||||
type={'submit'}
|
||||
// disabled={selectedConversation === undefined ? true : false || !formik.isValid}
|
||||
@@ -293,19 +363,13 @@ const AsyncChat = ({selectedConversation, conversationTitle, conversations, setC
|
||||
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
</Form>
|
||||
}
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
</Formik>
|
||||
</div>
|
||||
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default AsyncChat
|
||||
export default AsyncChat;
|
||||
|
||||
@@ -9,16 +9,16 @@ type ConversationCardProps = {
|
||||
title: string,
|
||||
conversation_id: number | undefined,
|
||||
setSelectConversation: Dispatch<SetStateAction<number | undefined>>; // state function to set the selected conversation
|
||||
deleteConversation: (conversation_id: number | undefined)=>void;
|
||||
deleteConversation: (conversation_id: number | undefined) => void;
|
||||
selectedConversation: number | undefined;
|
||||
};
|
||||
|
||||
const ConversationCard = ({title, conversation_id, setSelectConversation, deleteConversation, selectedConversation}: ConversationCardProps): JSX.Element => {
|
||||
const backgroundColor = selectedConversation===conversation_id ? '#ffffff' : 'lightgray';
|
||||
const ConversationCard = ({ title, conversation_id, setSelectConversation, deleteConversation, selectedConversation }: ConversationCardProps): JSX.Element => {
|
||||
const backgroundColor = selectedConversation === conversation_id ? '#ffffff' : 'lightgray';
|
||||
return (
|
||||
// <Row>
|
||||
|
||||
|
||||
|
||||
// <ButtonGroup>
|
||||
// <ButtonBase onClick={() => {setSelectConversation(conversation_id)}} style={{width: "80%", margin:'1px'}}>
|
||||
// <CardContent >{title}</CardContent>
|
||||
@@ -30,39 +30,39 @@ const ConversationCard = ({title, conversation_id, setSelectConversation, delete
|
||||
// </Row>
|
||||
<Row color="red">
|
||||
<Col className="col-8">
|
||||
|
||||
|
||||
<MDTypography
|
||||
onClick={() => {setSelectConversation(conversation_id)}}
|
||||
key={conversation_id}
|
||||
color={'white'}
|
||||
fontWeight={selectedConversation===conversation_id ? 'bold' : 'regular'}
|
||||
display="block"
|
||||
variant="caption"
|
||||
textTransform="uppercase"
|
||||
mt={2}
|
||||
mb={1}
|
||||
ml={1}
|
||||
noWrap
|
||||
>
|
||||
{title}
|
||||
</MDTypography>
|
||||
|
||||
|
||||
|
||||
</Col>
|
||||
<Col className="col-4">
|
||||
<IconButton
|
||||
aria-label="delete ${conversation_id}"
|
||||
onClick={() => deleteConversation(conversation_id)}
|
||||
|
||||
|
||||
|
||||
|
||||
>
|
||||
<Delete sx={{color:backgroundColor}}/>
|
||||
</IconButton>
|
||||
</Col>
|
||||
|
||||
<MDTypography
|
||||
onClick={() => { setSelectConversation(conversation_id) }}
|
||||
key={conversation_id}
|
||||
color={'white'}
|
||||
fontWeight={selectedConversation === conversation_id ? 'bold' : 'regular'}
|
||||
display="block"
|
||||
variant="caption"
|
||||
textTransform="uppercase"
|
||||
mt={2}
|
||||
mb={1}
|
||||
ml={1}
|
||||
noWrap
|
||||
>
|
||||
{title}
|
||||
</MDTypography>
|
||||
|
||||
|
||||
|
||||
</Col>
|
||||
<Col className="col-4">
|
||||
<IconButton
|
||||
aria-label="delete ${conversation_id}"
|
||||
onClick={() => deleteConversation(conversation_id)}
|
||||
|
||||
|
||||
|
||||
|
||||
>
|
||||
<Delete sx={{ color: backgroundColor }} />
|
||||
</IconButton>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
// <Card style={{width: "100%", margin:'0px'}}
|
||||
@@ -81,11 +81,11 @@ const ConversationCard = ({title, conversation_id, setSelectConversation, delete
|
||||
// <IconButton aria-label="delete ${conversation_id}" onClick={() => deleteConversation(conversation_id)}>
|
||||
// <Delete />
|
||||
// </IconButton>
|
||||
|
||||
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
|
||||
|
||||
|
||||
// </Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,69 +1,171 @@
|
||||
import { useContext, useEffect, useRef, useState } from "react"
|
||||
import { WebSocketContext } from "../../contexts/WebSocketContext"
|
||||
import { AccountContext } from "../../contexts/AccountContext"
|
||||
import Markdown from "markdown-to-jsx"
|
||||
import { Card, CardContent, CircularProgress } from "@mui/material"
|
||||
import MDTypography from "../../ui-kit/components/MDTypography"
|
||||
import styled from 'styled-components';
|
||||
// import { CodeBlock } from "react-code-blocks"
|
||||
// import CustomCodeBlock from "../CustomPreBlock/CustomPreBlock"
|
||||
// import CustomPreBlock from "../CustomPreBlock/CustomPreBlock"
|
||||
import React from "react";
|
||||
import Markdown from "markdown-to-jsx";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
|
||||
const StyleDiv = styled.div`
|
||||
background-color: #f0f0f0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
const fadeIn = keyframes`
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
`;
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
const MessageContainer = styled.div<{ $isUser: boolean }>`
|
||||
display: flex;
|
||||
justify-content: ${(props) => (props.$isUser ? "flex-end" : "flex-start")};
|
||||
margin-bottom: 1.5rem;
|
||||
width: 100%;
|
||||
animation: ${fadeIn} 0.3s ease-out;
|
||||
`;
|
||||
|
||||
const Bubble = styled.div<{ $isUser: boolean }>`
|
||||
max-width: 80%;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 1.2rem;
|
||||
background: ${(props) =>
|
||||
props.$isUser
|
||||
? `linear-gradient(135deg, ${props.theme.main} 0%, ${props.theme.focus} 100%)`
|
||||
: props.theme.darkMode
|
||||
? "rgba(255, 255, 255, 0.1)"
|
||||
: "rgba(0, 0, 0, 0.7)"}; // Dark background for AI in light mode as requested
|
||||
color: #fff; // Text stays white as background is always dark/colored
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid ${(props) => props.theme.darkMode ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)"};
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
border-bottom-right-radius: ${(props) => (props.$isUser ? "0.2rem" : "1.2rem")};
|
||||
border-bottom-left-radius: ${(props) => (props.$isUser ? "1.2rem" : "0.2rem")};
|
||||
|
||||
& pre {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
& code {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
& a {
|
||||
color: #a0c4ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const LoadingDot = styled.div`
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
margin: 0 4px;
|
||||
animation: bounce 1.4s infinite ease-in-out both;
|
||||
|
||||
&:nth-child(1) { animation-delay: -0.32s; }
|
||||
&:nth-child(2) { animation-delay: -0.16s; }
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: scale(0); }
|
||||
40% { transform: scale(1); }
|
||||
}
|
||||
`;
|
||||
|
||||
const LoadingContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
`;
|
||||
|
||||
type ConversationDetailCardProps = {
|
||||
message: string
|
||||
user_created: boolean
|
||||
message: string;
|
||||
user_created: boolean;
|
||||
};
|
||||
|
||||
}
|
||||
// Custom component for rendering plots
|
||||
const MyPlot = ({ format, image }: { format: string; image: string }) => {
|
||||
const imageSrc = `data:image/${format};base64,${image}`;
|
||||
return (
|
||||
<img
|
||||
src={imageSrc}
|
||||
style={{ maxWidth: "100%", height: "auto", borderRadius: "8px", marginTop: "10px" }}
|
||||
alt="plot"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ConversationDetailCard = ({message, user_created}: ConversationDetailCardProps): JSX.Element => {
|
||||
const type = user_created ? 'info' : 'dark'
|
||||
if(user_created){
|
||||
// Custom component for rendering errors
|
||||
const MyError = ({ content }: { content: string }) => {
|
||||
return (
|
||||
<span style={{ color: "#ff6b6b", fontWeight: "bold", display: "block", marginTop: "0.5rem" }}>
|
||||
Error: {content}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const ConversationDetailCard = ({
|
||||
message,
|
||||
user_created,
|
||||
}: ConversationDetailCardProps): JSX.Element => {
|
||||
if (message.length === 0) {
|
||||
return (
|
||||
<MessageContainer $isUser={false}>
|
||||
<Bubble $isUser={false}>
|
||||
<LoadingContainer>
|
||||
<LoadingDot />
|
||||
<LoadingDot />
|
||||
<LoadingDot />
|
||||
</LoadingContainer>
|
||||
</Bubble>
|
||||
</MessageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
let contentToAdd = message;
|
||||
try {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
if (
|
||||
parsedMessage &&
|
||||
typeof parsedMessage === "object" &&
|
||||
parsedMessage.type
|
||||
) {
|
||||
switch (parsedMessage.type) {
|
||||
case "text":
|
||||
contentToAdd = parsedMessage.content;
|
||||
break;
|
||||
case "plot":
|
||||
contentToAdd = `<plot format="${parsedMessage.format}" image="${parsedMessage.image}"></plot>`;
|
||||
break;
|
||||
case "error":
|
||||
contentToAdd = `<error content="${parsedMessage.content}"></error>`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
const text_align = user_created ? 'right' : 'left'
|
||||
if (message.length === 0){
|
||||
return(
|
||||
<Card sx={{margin: '0.25rem'}}>
|
||||
<CardContent className='card-body-small text-dark ' style={{textAlign: `right`, marginRight: '1rem', marginLeft: '1rem', marginTop: '1rem', marginBottom: '1rem'}}>
|
||||
<CircularProgress color="inherit"/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}else{
|
||||
const newMessage: string = message.replace("```", "\n```\n");
|
||||
|
||||
return (
|
||||
} catch { }
|
||||
|
||||
|
||||
<Card sx={{margin: '0.25rem'}}>
|
||||
<CardContent sx={{textAlign: `${text_align}`, marginRight: '1rem', marginLeft: '1rem', marginTop: '1rem', marginBottom: '1rem'}}>
|
||||
<Markdown
|
||||
className='display-linebreak'
|
||||
style={{whiteSpace: "pre-line"}}
|
||||
color='#F000000'
|
||||
>{newMessage}</Markdown>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
return (
|
||||
<MessageContainer $isUser={user_created}>
|
||||
<Bubble $isUser={user_created}>
|
||||
<Markdown
|
||||
className="display-linebreak"
|
||||
style={{ whiteSpace: "pre-line" }}
|
||||
options={{
|
||||
overrides: {
|
||||
plot: {
|
||||
component: MyPlot,
|
||||
},
|
||||
error: {
|
||||
component: MyError,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{contentToAdd}
|
||||
</Markdown>
|
||||
</Bubble>
|
||||
</MessageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ConversationDetailCard;
|
||||
export default ConversationDetailCard;
|
||||
|
||||
@@ -8,36 +8,39 @@ import { CacheProvider } from "@emotion/react";
|
||||
import { CssBaseline, Icon, ThemeProvider } from "@mui/material";
|
||||
import brandWhite from "../../ui-kit/assets/images/logo-ct.png";
|
||||
import brandDark from "../../ui-kit/assets/images/logo-ct-dark.png";
|
||||
import themeDark from "../../ui-kit/assets/theme-dark";
|
||||
import createDarkTheme from "../../ui-kit/assets/theme-dark";
|
||||
import themeDarkRTL from "../../ui-kit/assets/theme-dark/theme-rtl";
|
||||
import theme from "../../ui-kit/assets/theme";
|
||||
import createTheme from "../../ui-kit/assets/theme";
|
||||
import themeRTL from "../../ui-kit/assets/theme/theme-rtl";
|
||||
import palettes from "../../ui-kit/assets/theme/base/palettes";
|
||||
import Sidenav from "../../ui-kit/examples/Sidenav";
|
||||
import Configurator from "../../ui-kit/examples/Configurator";
|
||||
|
||||
|
||||
type DashboardWrapperLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DashboardWrapperLayout = ({children}: DashboardWrapperLayoutProps): JSX.Element => {
|
||||
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {
|
||||
miniSidenav,
|
||||
direction,
|
||||
layout,
|
||||
openConfigurator,
|
||||
sidenavColor,
|
||||
transparentSidenav,
|
||||
whiteSidenav,
|
||||
darkMode,
|
||||
} = controller;
|
||||
const [onMouseEnter, setOnMouseEnter] = useState(false);
|
||||
const [rtlCache, setRtlCache] = useState<EmotionCache | null>(null);
|
||||
const { pathname } = useLocation();
|
||||
const DashboardWrapperLayout = ({ children }: DashboardWrapperLayoutProps): JSX.Element => {
|
||||
|
||||
// Cache for the rtl
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {
|
||||
miniSidenav,
|
||||
direction,
|
||||
layout,
|
||||
openConfigurator,
|
||||
sidenavColor,
|
||||
transparentSidenav,
|
||||
whiteSidenav,
|
||||
|
||||
darkMode,
|
||||
themeColor,
|
||||
} = controller;
|
||||
const [onMouseEnter, setOnMouseEnter] = useState(false);
|
||||
const [rtlCache, setRtlCache] = useState<EmotionCache | null>(null);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
// Cache for the rtl
|
||||
useMemo(() => {
|
||||
const cacheRtl = createCache({
|
||||
key: "rtl",
|
||||
@@ -74,8 +77,8 @@ const DashboardWrapperLayout = ({children}: DashboardWrapperLayoutProps): JSX.El
|
||||
// Setting page scroll to 0 when changing the route
|
||||
useEffect(() => {
|
||||
document.documentElement.scrollTop = 0;
|
||||
if(document.scrollingElement){
|
||||
document.scrollingElement.scrollTop = 0;
|
||||
if (document.scrollingElement) {
|
||||
document.scrollingElement.scrollTop = 0;
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
@@ -126,7 +129,7 @@ const DashboardWrapperLayout = ({children}: DashboardWrapperLayoutProps): JSX.El
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
) : (
|
||||
<ThemeProvider theme={darkMode ? themeDark : theme}>
|
||||
<ThemeProvider theme={darkMode ? createDarkTheme((palettes as any)[themeColor] || (palettes as any).blue) : createTheme((palettes as any)[themeColor] || (palettes as any).blue)}>
|
||||
<CssBaseline />
|
||||
{layout === "dashboard" && (
|
||||
<>
|
||||
@@ -146,7 +149,7 @@ const DashboardWrapperLayout = ({children}: DashboardWrapperLayoutProps): JSX.El
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default DashboardWrapperLayout;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { useMaterialUIController } from '../../ui-kit/context';
|
||||
import palettes from '../../ui-kit/assets/theme/base/palettes';
|
||||
|
||||
type GlobalThemeWrapperProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const GlobalThemeWrapper = ({ children }: GlobalThemeWrapperProps): JSX.Element => {
|
||||
const [controller] = useMaterialUIController();
|
||||
const { themeColor, darkMode } = controller;
|
||||
|
||||
// Default to blue if themeColor is not found
|
||||
const selectedPalette = palettes[themeColor as keyof typeof palettes] || palettes.blue;
|
||||
|
||||
const theme = {
|
||||
main: selectedPalette.main,
|
||||
focus: selectedPalette.focus,
|
||||
darkMode: darkMode,
|
||||
// Add other global theme variables here if needed
|
||||
colors: {
|
||||
background: darkMode ? '#1a2035' : '#f0f2f5',
|
||||
text: darkMode ? '#ffffff' : '#344767',
|
||||
cardBackground: darkMode ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.8)',
|
||||
cardBorder: darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)',
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalThemeWrapper;
|
||||
@@ -1,104 +1,185 @@
|
||||
import { AppBar, Button, Toolbar } from '@mui/material';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useMaterialUIController } from '../../ui-kit/context';
|
||||
import { boolean } from 'yup';
|
||||
import MDBox from '../../ui-kit/components/MDBox';
|
||||
import {
|
||||
navbar,
|
||||
navbarContainer,
|
||||
navbarRow,
|
||||
navbarIconButton,
|
||||
navbarMobileMenu,
|
||||
} from "../../ui-kit/examples/Navbars/DashboardNavbar/styles";
|
||||
import Breadcrumbs from '../../ui-kit/examples/Breadcrumbs';
|
||||
import { AccountContext } from '../../contexts/AccountContext';
|
||||
import { AuthContext } from '../../contexts/AuthContext';
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AuthContext } from '../../contexts/AuthContext';
|
||||
import { AccountContext } from '../../contexts/AccountContext';
|
||||
import { axiosInstance } from '../../../axiosApi';
|
||||
|
||||
const HeaderContainer = styled.header`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)'};
|
||||
backdrop-filter: blur(5px);
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
`;
|
||||
|
||||
const Logo = styled.div`
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
cursor: pointer;
|
||||
background: ${({ theme }) => `linear-gradient(135deg, ${theme.main} 0%, ${theme.focus} 100%)`};
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
`;
|
||||
|
||||
const Nav = styled.nav`
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const NavLink = styled.button`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
opacity: 0.7;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
}
|
||||
`;
|
||||
|
||||
const SignOutButton = styled(NavLink)`
|
||||
color: #ff6b6b;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
color: #ff4757;
|
||||
background: rgba(255, 71, 87, 0.1);
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenuButton = styled.button`
|
||||
display: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 101;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenuDropdown = styled.div<{ isOpen: boolean }>`
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: ${({ theme }) => theme.colors.cardBackground};
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
transform: ${({ isOpen }) => (isOpen ? 'translateY(0)' : 'translateY(-20px)')};
|
||||
opacity: ${({ isOpen }) => (isOpen ? '1' : '0')};
|
||||
pointer-events: ${({ isOpen }) => (isOpen ? 'auto' : 'none')};
|
||||
transition: all 0.3s ease;
|
||||
z-index: 99;
|
||||
`;
|
||||
|
||||
const HamburgerIcon = ({ color }: { color: string }) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 12H21" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M3 6H21" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M3 18H21" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CloseIcon = ({ color }: { color: string }) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M6 6L18 18" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
type Header2Props = {
|
||||
absolute?: Boolean;
|
||||
light?: Boolean;
|
||||
isMini?: Boolean;
|
||||
absolute?: Boolean;
|
||||
light?: Boolean;
|
||||
isMini?: Boolean;
|
||||
}
|
||||
|
||||
const Header2 = ({ absolute=false, light=false, isMini=false }: Header2Props): JSX.Element => {
|
||||
const {authenticated, setAuthentication} = useContext(AuthContext);
|
||||
const { account, setAccount } = useContext(AccountContext);
|
||||
const navigate = useNavigate();
|
||||
const handleSignOut = async () => {
|
||||
try{
|
||||
const response = await axiosInstance.post('blacklist/',{
|
||||
'refresh_token': localStorage.getItem("refresh_token")
|
||||
})
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
axiosInstance.defaults.headers['Authorization'] = null;
|
||||
setAuthentication(false)
|
||||
setAccount(undefined);
|
||||
navigate('/signin/')
|
||||
}finally{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const handleDashboardClick = async () => {
|
||||
navigate('/')
|
||||
}
|
||||
const Header2 = ({ absolute = false, light = false, isMini = false }: Header2Props): JSX.Element => {
|
||||
const { setAuthentication } = useContext(AuthContext);
|
||||
const { setAccount } = useContext(AccountContext);
|
||||
const navigate = useNavigate();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleDocumentStorageClick = async () => {
|
||||
navigate('/document_storage/')
|
||||
}
|
||||
|
||||
const handleAccountClick = async () => {
|
||||
navigate('/account/')
|
||||
}
|
||||
|
||||
const handleFeedbackClick = async () => {
|
||||
navigate('/feedback/')
|
||||
}
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await axiosInstance.post('blacklist/', {
|
||||
'refresh_token': localStorage.getItem("refresh_token")
|
||||
})
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
axiosInstance.defaults.headers['Authorization'] = null;
|
||||
setAuthentication(false)
|
||||
setAccount(undefined);
|
||||
navigate('/signin/')
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const handleAnalyticsClick = async () => {
|
||||
navigate('/analytics/')
|
||||
}
|
||||
const [navbarType, setNavbarType] = useState();
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const { miniSidenav, transparentNavbar, fixedNavbar, openConfigurator, darkMode } = controller;
|
||||
return (
|
||||
<AppBar
|
||||
position={absolute ? 'absolute' : navbarType}
|
||||
color='info'
|
||||
sx={(theme) => navbar(theme, { transparentNavbar, absolute, light, darkMode })}
|
||||
>
|
||||
{/* <Toolbar> sx={(theme) => navbarContainer(theme)}> */}
|
||||
<Toolbar sx={{justifyContent: 'start'}}>
|
||||
<MDBox>
|
||||
<Button color="inherit" onClick={handleDashboardClick}>Dashboard</Button>
|
||||
const handleNavClick = (path: string) => {
|
||||
navigate(path);
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
</MDBox>
|
||||
<MDBox sx={{marginLeft: "auto"}}>
|
||||
<Button color="inherit" onClick={handleAccountClick}>Account</Button>
|
||||
|
||||
<Button color="inherit" onClick={handleDocumentStorageClick}>Document Storage</Button>
|
||||
return (
|
||||
<HeaderContainer>
|
||||
<Logo onClick={() => navigate('/')}>
|
||||
<h4>Chat</h4>
|
||||
|
||||
<Button color="inherit" onClick={handleAnalyticsClick}>Analytics</Button>
|
||||
|
||||
<Button color="inherit" onClick={handleFeedbackClick} >Feedback</Button>
|
||||
|
||||
<Button color="inherit" onClick={handleSignOut} >Sign Out</Button>
|
||||
</Logo>
|
||||
|
||||
</MDBox>
|
||||
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<Nav>
|
||||
<NavLink onClick={() => navigate('/')}>Dashboard</NavLink>
|
||||
<NavLink onClick={() => navigate('/account/')}>Account</NavLink>
|
||||
<NavLink onClick={() => navigate('/document_storage/')}>Documents</NavLink>
|
||||
<NavLink onClick={() => navigate('/analytics/')}>Analytics</NavLink>
|
||||
<NavLink onClick={() => navigate('/feedback/')}>Feedback</NavLink>
|
||||
<SignOutButton onClick={handleSignOut}>Sign Out</SignOutButton>
|
||||
</Nav>
|
||||
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
{/* Mobile Menu Button */}
|
||||
<MobileMenuButton onClick={() => setIsMenuOpen(!isMenuOpen)}>
|
||||
{isMenuOpen ? <CloseIcon color={theme.colors.text} /> : <HamburgerIcon color={theme.colors.text} />}
|
||||
</MobileMenuButton>
|
||||
|
||||
|
||||
|
||||
)
|
||||
{/* Mobile Menu Dropdown */}
|
||||
<MobileMenuDropdown isOpen={isMenuOpen}>
|
||||
<NavLink onClick={() => handleNavClick('/')}>Dashboard</NavLink>
|
||||
<NavLink onClick={() => handleNavClick('/account/')}>Account</NavLink>
|
||||
<NavLink onClick={() => handleNavClick('/document_storage/')}>Documents</NavLink>
|
||||
<NavLink onClick={() => handleNavClick('/analytics/')}>Analytics</NavLink>
|
||||
<NavLink onClick={() => handleNavClick('/feedback/')}>Feedback</NavLink>
|
||||
<SignOutButton onClick={() => { handleSignOut(); setIsMenuOpen(false); }}>Sign Out</SignOutButton>
|
||||
</MobileMenuDropdown>
|
||||
</HeaderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header2;
|
||||
@@ -6,27 +6,29 @@ import stylisRTLPlugin from "stylis-plugin-rtl";
|
||||
import MDBox from "../../ui-kit/components/MDBox";
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import { CssBaseline, Icon, ThemeProvider } from "@mui/material";
|
||||
import themeDark from "../../ui-kit/assets/theme-dark";
|
||||
import theme from "../../ui-kit/assets/theme";
|
||||
import createTheme from "../../ui-kit/assets/theme";
|
||||
import createDarkTheme from "../../ui-kit/assets/theme-dark";
|
||||
import themeRtl from "../../ui-kit/assets/theme/theme-rtl";
|
||||
import themeDarkRTL from "../../ui-kit/assets/theme-dark/theme-rtl";
|
||||
import palettes from "../../ui-kit/assets/theme/base/palettes";
|
||||
|
||||
type PageWrapperLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PageWrapperLayout = ({children}: PageWrapperLayoutProps): JSX.Element => {
|
||||
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {
|
||||
direction,
|
||||
openConfigurator,
|
||||
darkMode,
|
||||
} = controller;
|
||||
const [rtlCache, setRtlCache] = useState<EmotionCache | null>(null);
|
||||
const { pathname } = useLocation();
|
||||
const PageWrapperLayout = ({ children }: PageWrapperLayoutProps): JSX.Element => {
|
||||
|
||||
// Cache for the rtl
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {
|
||||
direction,
|
||||
openConfigurator,
|
||||
darkMode,
|
||||
themeColor,
|
||||
} = controller;
|
||||
const [rtlCache, setRtlCache] = useState<EmotionCache | null>(null);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
// Cache for the rtl
|
||||
useMemo(() => {
|
||||
const cacheRtl = createCache({
|
||||
key: "rtl",
|
||||
@@ -47,8 +49,8 @@ const PageWrapperLayout = ({children}: PageWrapperLayoutProps): JSX.Element => {
|
||||
// Setting page scroll to 0 when changing the route
|
||||
useEffect(() => {
|
||||
document.documentElement.scrollTop = 0;
|
||||
if(document.scrollingElement){
|
||||
document.scrollingElement.scrollTop = 0;
|
||||
if (document.scrollingElement) {
|
||||
document.scrollingElement.scrollTop = 0;
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
@@ -81,19 +83,19 @@ const PageWrapperLayout = ({children}: PageWrapperLayoutProps): JSX.Element => {
|
||||
<ThemeProvider theme={darkMode ? themeDarkRTL : themeRtl}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
|
||||
|
||||
|
||||
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
) : (
|
||||
<ThemeProvider theme={darkMode ? themeDark : theme}>
|
||||
<ThemeProvider theme={darkMode ? createDarkTheme((palettes as any)[themeColor] || (palettes as any).blue) : createTheme((palettes as any)[themeColor] || (palettes as any).blue)}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
|
||||
{children}
|
||||
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default PageWrapperLayout;
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
|
||||
const Canvas = styled.canvas`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
background: ${({ theme }) => theme.darkMode ? 'linear-gradient(to bottom, #000000, #1a1a2e)' : 'linear-gradient(to bottom, #f0f2f5, #e0e5ec)'};
|
||||
transition: background 0.5s ease;
|
||||
`;
|
||||
|
||||
const ParticleBackground = () => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
let particles: Particle[] = [];
|
||||
let animationFrameId: number;
|
||||
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
class Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
size: number;
|
||||
color: string;
|
||||
|
||||
constructor() {
|
||||
this.x = Math.random() * canvas!.width;
|
||||
this.y = Math.random() * canvas!.height;
|
||||
this.vx = (Math.random() - 0.5) * 0.5;
|
||||
this.vy = (Math.random() - 0.5) * 0.5;
|
||||
this.size = Math.random() * 2 + 1;
|
||||
// Use theme main color or fallback
|
||||
const baseColor = theme?.main || '#6495ed';
|
||||
// Convert hex to rgb for opacity if needed, or just use it directly if it's already compatible
|
||||
// For simplicity, let's assume theme.main is a hex and we want to add opacity.
|
||||
// But here we are using a fixed color for now, let's try to use the theme color.
|
||||
this.color = theme?.darkMode
|
||||
? `rgba(100, 149, 237, ${Math.random() * 0.5 + 0.2})`
|
||||
: `rgba(100, 149, 237, ${Math.random() * 0.5 + 0.2})`; // Keep blueish for now, maybe change later
|
||||
|
||||
// Better approach: use the theme main color
|
||||
this.color = theme?.main
|
||||
? `${theme.main}${Math.floor((Math.random() * 0.5 + 0.2) * 255).toString(16).padStart(2, '0')}`
|
||||
: `rgba(100, 149, 237, ${Math.random() * 0.5 + 0.2})`;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
if (this.x < 0 || this.x > canvas!.width) this.vx *= -1;
|
||||
if (this.y < 0 || this.y > canvas!.height) this.vy *= -1;
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!ctx) return;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
particles = [];
|
||||
const numberOfParticles = Math.floor((canvas.width * canvas.height) / 15000);
|
||||
for (let i = 0; i < numberOfParticles; i++) {
|
||||
particles.push(new Particle());
|
||||
}
|
||||
};
|
||||
|
||||
const animate = () => {
|
||||
if (!ctx) return;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
particles.forEach((particle) => {
|
||||
particle.update();
|
||||
particle.draw();
|
||||
});
|
||||
|
||||
// Draw connections
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 100) {
|
||||
ctx.beginPath();
|
||||
// Use theme main color for connections too
|
||||
const opacity = 1 - distance / 100;
|
||||
const hexOpacity = Math.floor(opacity * 255).toString(16).padStart(2, '0');
|
||||
ctx.strokeStyle = theme?.main
|
||||
? `${theme.main}${hexOpacity}`
|
||||
: `rgba(100, 149, 237, ${opacity})`;
|
||||
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
init();
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeCanvas);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, [theme]); // Re-run when theme changes
|
||||
|
||||
return <Canvas ref={canvasRef} />;
|
||||
};
|
||||
|
||||
export default ParticleBackground;
|
||||
@@ -5,154 +5,169 @@ import { Alert, Button, Stack, Typography } from '@mui/material';
|
||||
import { axiosInstance } from '../../../axiosApi';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import CustomToastMessage from '../../components/CustomToastMessage/CustomeToastMessage';
|
||||
import background from '../../../bg.jpeg'
|
||||
|
||||
import * as Yup from 'yup';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import PageWrapperLayout from '../PageWrapperLayout/PageWrapperLayout';
|
||||
import MDBox from '../../ui-kit/components/MDBox';
|
||||
import { Card, CardContent } from '@mui/material';
|
||||
import MDTypography from '../../ui-kit/components/MDTypography';
|
||||
import MDButton from '../../ui-kit/components/MDButton';
|
||||
import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
|
||||
|
||||
|
||||
export type PasswordResetValues = {
|
||||
password1: string;
|
||||
password2: string;
|
||||
password1: string;
|
||||
password2: string;
|
||||
};
|
||||
|
||||
const initialValues = {password1: '', password2: ''}
|
||||
const validationSchema = Yup.object().shape({
|
||||
const initialValues = { password1: '', password2: '' }
|
||||
const validationSchema = Yup.object().shape({
|
||||
password1: Yup.string().min(6, "Passwords have to be at least 6 digits").required(),
|
||||
password2: Yup.string().min(6, "Passwords have to be at least 6 digits").required().oneOf([Yup.ref('password1')], "Passwords must match"),
|
||||
})
|
||||
|
||||
const contains_number = (item: string): boolean => {
|
||||
const numbers = ['1','2','3','4','5','6','7','8','9','0'];
|
||||
const numbers = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
|
||||
const hasNumber = numbers.some((character) => item.includes(character));
|
||||
if(hasNumber){
|
||||
if (hasNumber) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const contains_special_character = (item: string): boolean => {
|
||||
const specialCharacters = ['!','@','#','$',',%','^','&','*','(',')','-','_','=','+','/','*','\\','|','`','~','<','>','.','?'];
|
||||
const specialCharacters = ['!', '@', '#', '$', ',%', '^', '&', '*', '(', ')', '-', '_', '=', '+', '/', '*', '\\', '|', '`', '~', '<', '>', '.', '?'];
|
||||
const hasSpecialChacater = specialCharacters.some((character) => item.includes(character));
|
||||
if(hasSpecialChacater){
|
||||
if (hasSpecialChacater) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const SetPassword = ({}): JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
const SetPassword = ({ }): JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
// see if the user is allowed to come here first
|
||||
const [queryParameters] = useSearchParams()
|
||||
console.log(queryParameters.get("slug"))
|
||||
const slug = queryParameters.get("slug");
|
||||
|
||||
|
||||
try{
|
||||
|
||||
try {
|
||||
// make sure it comes back as 200 for a good request. Else go to the homepage
|
||||
axiosInstance.get(`user/set_password/${slug}`)
|
||||
}catch{
|
||||
} catch {
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
const handleSetPassword = ({password1, password2}: PasswordResetValues): void => {
|
||||
try{
|
||||
// verify
|
||||
if(password1 === password2){
|
||||
axiosInstance.post(`user/set_password/${slug}/`, {
|
||||
'password': password1,
|
||||
});
|
||||
|
||||
navigate('/')
|
||||
}
|
||||
}catch(error){
|
||||
console.log('catching the error');
|
||||
<CustomToastMessage message={error as string} />
|
||||
|
||||
const handleSetPassword = ({ password1, password2 }: PasswordResetValues): void => {
|
||||
try {
|
||||
// verify
|
||||
if (password1 === password2) {
|
||||
axiosInstance.post(`user/set_password/${slug}/`, {
|
||||
'password': password1,
|
||||
});
|
||||
|
||||
navigate('/')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('catching the error');
|
||||
<CustomToastMessage message={error as string} />
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='main-content' style={{'height': '100%', minHeight: '100vh', display: 'flex', flexDirection: 'column', backgroundImage: `url(${background})`,backgroundSize: "cover",
|
||||
backgroundRepeat: "no-repeat"}}>
|
||||
<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'>Set your password</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
|
||||
<PageWrapperLayout>
|
||||
<MDBox sx={{
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
bgcolor: 'background.default',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<ParticleBackground />
|
||||
<MDBox sx={{ width: '100%', maxWidth: '400px', zIndex: 1, position: 'relative' }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<MDTypography variant="h4" textAlign="center">
|
||||
Set your password
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<div className='card-body text-center'>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSetPassword}
|
||||
validateOnMount
|
||||
validationSchema={validationSchema}>
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSetPassword}
|
||||
validateOnMount
|
||||
validationSchema={validationSchema}>
|
||||
{(formik) => (
|
||||
<Form>
|
||||
<div className='row'>
|
||||
<div className='col'>
|
||||
<CustomPasswordField
|
||||
label='Password'
|
||||
name="password1"
|
||||
changeHandler={(e) => formik.setFieldValue('password1', e.target.value)} />
|
||||
<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)} />
|
||||
<CustomPasswordField
|
||||
label='Confirm Password'
|
||||
name="password2"
|
||||
changeHandler={(e) => formik.setFieldValue('password2', e.target.value)} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
{formik.values.password1 === formik.values.password2 ? <CheckCircleIcon fontSize='small' color='success'/> : <ErrorIcon fontSize='small' color='warning'/>}
|
||||
|
||||
<Typography variant="body1">Passwords Match</Typography>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
{formik.values.password1 === formik.values.password2 ? <CheckCircleIcon fontSize='small' color='success' /> : <ErrorIcon fontSize='small' color='warning' />}
|
||||
|
||||
<Typography variant="body1">Passwords Match</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
<div>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
{formik.values.password1.length > 5 ? <CheckCircleIcon fontSize='small' color='success'/> : <ErrorIcon fontSize='small' color='warning'/>}
|
||||
<Typography variant="body1">At least 6 characters</Typography>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
{formik.values.password1.length > 5 ? <CheckCircleIcon fontSize='small' color='success' /> : <ErrorIcon fontSize='small' color='warning' />}
|
||||
<Typography variant="body1">At least 6 characters</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
<div>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
{contains_special_character(formik.values.password1) ? <CheckCircleIcon fontSize='small' color='success'/> : <ErrorIcon fontSize='small' color='warning'/>}
|
||||
<Typography variant="body1">At least one special character</Typography>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
{contains_special_character(formik.values.password1) ? <CheckCircleIcon fontSize='small' color='success' /> : <ErrorIcon fontSize='small' color='warning' />}
|
||||
<Typography variant="body1">At least one special character</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
<div>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
|
||||
{contains_number(formik.values.password1) ? <CheckCircleIcon fontSize='small' color='success'/> : <ErrorIcon fontSize='small' color='warning'/>}
|
||||
<Typography variant="body1">At least one number</Typography>
|
||||
<Stack alignItems="center" direction="row" gap={2}>
|
||||
|
||||
{contains_number(formik.values.password1) ? <CheckCircleIcon fontSize='small' color='success' /> : <ErrorIcon fontSize='small' color='warning' />}
|
||||
<Typography variant="body1">At least one number</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='col'>
|
||||
<Button
|
||||
type={'submit'}
|
||||
disabled={!formik.isValid || formik.isSubmitting ||
|
||||
!contains_special_character(formik.values.password1)
|
||||
|| !contains_number(formik.values.password1)
|
||||
}
|
||||
|
||||
// type={'submit'}
|
||||
// loading={formik.isSubmitting}
|
||||
// disabled={
|
||||
// !formik.isValid || !formik.dirty || formik.isSubmitting
|
||||
// }
|
||||
>
|
||||
Set password
|
||||
</Button>
|
||||
|
||||
<MDButton
|
||||
type={'submit'}
|
||||
fullWidth
|
||||
disabled={!formik.isValid || formik.isSubmitting ||
|
||||
!contains_special_character(formik.values.password1)
|
||||
|| !contains_number(formik.values.password1)
|
||||
}
|
||||
|
||||
// type={'submit'}
|
||||
// loading={formik.isSubmitting}
|
||||
// disabled={
|
||||
// !formik.isValid || !formik.dirty || formik.isSubmitting
|
||||
// }
|
||||
>
|
||||
Set password
|
||||
</MDButton>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,12 +177,11 @@ const SetPassword = ({}): JSX.Element => {
|
||||
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</Card>
|
||||
</MDBox>
|
||||
</MDBox>
|
||||
</PageWrapperLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetPassword;
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Card, CardContent, Divider, Switch } from "@mui/material";
|
||||
import { Card, CardContent, Divider, Switch, IconButton } from "@mui/material";
|
||||
import MDTypography from "../../ui-kit/components/MDTypography";
|
||||
import MDBox from "../../ui-kit/components/MDBox";
|
||||
import {
|
||||
useMaterialUIController,
|
||||
setDarkMode,
|
||||
setThemeColor,
|
||||
} from "../../ui-kit/context";
|
||||
import palettes from "../../ui-kit/assets/theme/base/palettes";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { axiosInstance } from "../../../axiosApi";
|
||||
@@ -15,73 +17,119 @@ type PreferencesValues = {
|
||||
order: boolean;
|
||||
}
|
||||
|
||||
const ThemeCard = ({}): JSX.Element => {
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {
|
||||
darkMode,
|
||||
} = controller;
|
||||
const ThemeCard = ({ }): JSX.Element => {
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {
|
||||
darkMode,
|
||||
themeColor,
|
||||
} = controller;
|
||||
const themeColors = Object.keys(palettes);
|
||||
|
||||
const [order, setOrder] = useState<boolean>(true);
|
||||
const {updatePreferences} = useContext(PreferenceContext);
|
||||
const [order, setOrder] = useState<boolean>(true);
|
||||
const { updatePreferences } = useContext(PreferenceContext);
|
||||
|
||||
const handleConversationOrder = async ({order}: PreferencesValues): Promise<void> => {
|
||||
try{
|
||||
const {data, }: AxiosResponse<PreferencesType> = await axiosInstance.post('/conversation_preferences', {
|
||||
order: order
|
||||
})
|
||||
updatePreferences();
|
||||
}catch{
|
||||
const handleConversationOrder = async ({ order }: PreferencesValues): Promise<void> => {
|
||||
try {
|
||||
const { data, }: AxiosResponse<PreferencesType> = await axiosInstance.post('/conversation_preferences', {
|
||||
order: order
|
||||
})
|
||||
updatePreferences();
|
||||
} catch {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function getConversationOrder(){
|
||||
try{
|
||||
const {data, }: AxiosResponse<PreferencesType> = await axiosInstance.get(`/conversation_preferences`);
|
||||
setOrder(data.order);
|
||||
|
||||
|
||||
}catch(error){
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
getConversationOrder();
|
||||
}, [])
|
||||
|
||||
const handleDarkMode = () => setDarkMode(dispatch, !darkMode);
|
||||
const isThemeReady=false;
|
||||
return(
|
||||
<Card sx={{ mb: 1}}>
|
||||
<CardContent>
|
||||
<MDTypography variant="h3">
|
||||
Account Preferences
|
||||
async function getConversationOrder() {
|
||||
try {
|
||||
const { data, }: AxiosResponse<PreferencesType> = await axiosInstance.get(`/conversation_preferences`);
|
||||
setOrder(data.order);
|
||||
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
{isThemeReady ? (
|
||||
<MDBox display="flex" justifyContent="space-between" alignItems="center" lineHeight={1}>
|
||||
<MDTypography variant="h6">Light / Dark</MDTypography>
|
||||
<Switch checked={darkMode} onChange={handleDarkMode} disabled={false} />
|
||||
</MDBox>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<MDBox display="flex" justifyContent="space-between" alignItems="center" lineHeight={1}>
|
||||
<MDTypography variant="h6">Converastion Order</MDTypography>
|
||||
<Switch checked={order} onChange={() => {
|
||||
setOrder(!order);
|
||||
handleConversationOrder({order: !order})
|
||||
}} />
|
||||
</MDBox>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getConversationOrder();
|
||||
}, [])
|
||||
|
||||
const handleDarkMode = () => setDarkMode(dispatch, !darkMode);
|
||||
const isThemeReady = true;
|
||||
return (
|
||||
<Card sx={{ mb: 1 }}>
|
||||
<CardContent>
|
||||
<MDTypography variant="h3">
|
||||
Account Preferences
|
||||
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<MDBox display="flex" justifyContent="space-between" alignItems="center" lineHeight={1}>
|
||||
<MDTypography variant="h6">Light / Dark</MDTypography>
|
||||
<Switch checked={darkMode} onChange={handleDarkMode} disabled={false} />
|
||||
</MDBox>
|
||||
<MDBox display="flex" justifyContent="space-between" alignItems="center" lineHeight={1} mt={2}>
|
||||
<MDTypography variant="h6">Theme Color</MDTypography>
|
||||
<MDBox>
|
||||
{themeColors.map((color) => (
|
||||
<IconButton
|
||||
key={color}
|
||||
sx={(theme: any) => {
|
||||
const {
|
||||
borders: { borderWidth },
|
||||
palette: { white, dark, background },
|
||||
transitions,
|
||||
} = theme;
|
||||
|
||||
return {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
padding: 0,
|
||||
border: `${borderWidth[1]} solid ${darkMode ? background.sidenav : white.main}`,
|
||||
borderColor: () => {
|
||||
let borderColorValue = themeColor === color && dark.main;
|
||||
|
||||
if (darkMode && themeColor === color) {
|
||||
borderColorValue = white.main;
|
||||
}
|
||||
|
||||
return borderColorValue;
|
||||
},
|
||||
transition: transitions.create("border-color", {
|
||||
easing: transitions.easing.sharp,
|
||||
duration: transitions.duration.shorter,
|
||||
}),
|
||||
backgroundColor: palettes[color as keyof typeof palettes].main,
|
||||
|
||||
"&:not(:last-child)": {
|
||||
mr: 1,
|
||||
},
|
||||
|
||||
"&:hover, &:focus, &:active": {
|
||||
borderColor: darkMode ? white.main : dark.main,
|
||||
},
|
||||
};
|
||||
}}
|
||||
onClick={() => setThemeColor(dispatch, color)}
|
||||
/>
|
||||
))}
|
||||
</MDBox>
|
||||
</MDBox>
|
||||
|
||||
<MDBox display="flex" justifyContent="space-between" alignItems="center" lineHeight={1}>
|
||||
<MDTypography variant="h6">Converastion Order</MDTypography>
|
||||
<Switch checked={order} onChange={() => {
|
||||
setOrder(!order);
|
||||
handleConversationOrder({ order: !order })
|
||||
}} />
|
||||
</MDBox>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThemeCard;
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
useMaterialUIController,
|
||||
setDarkMode,
|
||||
setThemeColor,
|
||||
} from "../../ui-kit/context";
|
||||
import palettes from "../../ui-kit/assets/theme/base/palettes";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { axiosInstance } from "../../../axiosApi";
|
||||
import { PreferencesType } from "../../data";
|
||||
import { PreferenceContext } from "../../contexts/PreferencesContext";
|
||||
|
||||
// Styled Components (Reusing/Adapting from Account2/Design System)
|
||||
const GlassCard = styled.div`
|
||||
background: ${({ theme }) => theme.colors.cardBackground};
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
padding-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const SettingRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const SettingLabel = styled.span`
|
||||
font-size: 1.1rem;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const ToggleSwitch = styled.label`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const Slider = styled.span`
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'};
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Checkbox = styled.input`
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:checked + ${Slider} {
|
||||
background-color: ${({ theme }) => theme.main};
|
||||
}
|
||||
|
||||
&:checked + ${Slider}:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
`;
|
||||
|
||||
const ColorPickerContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
const ColorButton = styled.button<{ color: string; isSelected: boolean }>`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: ${(props) => props.color};
|
||||
border: 2px solid ${(props) => (props.isSelected ? props.theme.colors.text : 'transparent')};
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
`;
|
||||
|
||||
type PreferencesValues = {
|
||||
order: boolean;
|
||||
}
|
||||
|
||||
const ThemeSettingsCard = (): JSX.Element => {
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {
|
||||
darkMode,
|
||||
themeColor,
|
||||
} = controller;
|
||||
const themeColors = Object.keys(palettes);
|
||||
|
||||
const [order, setOrder] = useState<boolean>(true);
|
||||
const { updatePreferences } = useContext(PreferenceContext);
|
||||
|
||||
const handleConversationOrder = async ({ order }: PreferencesValues): Promise<void> => {
|
||||
try {
|
||||
await axiosInstance.post('/conversation_preferences', {
|
||||
order: order
|
||||
})
|
||||
updatePreferences();
|
||||
} catch {
|
||||
// Error handling
|
||||
}
|
||||
}
|
||||
|
||||
async function getConversationOrder() {
|
||||
try {
|
||||
const { data, }: AxiosResponse<PreferencesType> = await axiosInstance.get(`/conversation_preferences`);
|
||||
setOrder(data.order);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getConversationOrder();
|
||||
}, [])
|
||||
|
||||
const handleDarkMode = () => setDarkMode(dispatch, !darkMode);
|
||||
|
||||
return (
|
||||
<GlassCard>
|
||||
<CardTitle>Account Preferences</CardTitle>
|
||||
|
||||
<SettingRow>
|
||||
<SettingLabel>Dark Mode</SettingLabel>
|
||||
<ToggleSwitch>
|
||||
<Checkbox
|
||||
type="checkbox"
|
||||
checked={darkMode}
|
||||
onChange={handleDarkMode}
|
||||
/>
|
||||
<Slider />
|
||||
</ToggleSwitch>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<SettingLabel>Theme Color</SettingLabel>
|
||||
<ColorPickerContainer>
|
||||
{themeColors.map((color) => (
|
||||
<ColorButton
|
||||
key={color}
|
||||
color={palettes[color as keyof typeof palettes].main}
|
||||
isSelected={themeColor === color}
|
||||
onClick={() => setThemeColor(dispatch, color)}
|
||||
title={color}
|
||||
/>
|
||||
))}
|
||||
</ColorPickerContainer>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<SettingLabel>Conversation Order (Newest First)</SettingLabel>
|
||||
<ToggleSwitch>
|
||||
<Checkbox
|
||||
type="checkbox"
|
||||
checked={order}
|
||||
onChange={() => {
|
||||
setOrder(!order);
|
||||
handleConversationOrder({ order: !order })
|
||||
}}
|
||||
/>
|
||||
<Slider />
|
||||
</ToggleSwitch>
|
||||
</SettingRow>
|
||||
|
||||
</GlassCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSettingsCard;
|
||||
23
llm-fe/src/llm-fe/components/Tracker/Tracker.tsx
Normal file
23
llm-fe/src/llm-fe/components/Tracker/Tracker.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const Tracker: React.FC = () => {
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const script = document.createElement('script');
|
||||
script.src = "https://tianji.aimloperations.com/tracker.js";
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.setAttribute('data-website-id', 'cm7x7m52m03kbddswbswrt17y');
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(script);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Tracker;
|
||||
@@ -1,113 +1,119 @@
|
||||
import { AccountContext } from "./AccountContext"
|
||||
import { AccountContext } from "./AccountContext";
|
||||
import { AuthContext } from "./AuthContext";
|
||||
|
||||
const { useEffect, createContext, useRef, useState, useContext } = require("react")
|
||||
const {
|
||||
useEffect,
|
||||
createContext,
|
||||
useRef,
|
||||
useState,
|
||||
useContext,
|
||||
} = require("react");
|
||||
|
||||
|
||||
const WebSocketContext = createContext()
|
||||
const WebSocketContext = createContext();
|
||||
|
||||
function WebSocketProvider({ children }) {
|
||||
const { authenticated, loading } = useContext(AuthContext);
|
||||
const ws = useRef(null)
|
||||
const [socket, setSocket] = useState(null)
|
||||
const channels = useRef({}) // maps each channel to the callback
|
||||
const { account, setAccount } = useContext(AccountContext)
|
||||
const [currentChannel, setCurrentChannel] = useState('')
|
||||
/* called from a component that registers a callback for a channel */
|
||||
const subscribe = (channel, callback) => {
|
||||
//console.log(`Subbing to ${channel}`)
|
||||
setCurrentChannel(channel)
|
||||
channels.current[channel] = callback
|
||||
}
|
||||
/* remove callback */
|
||||
const unsubscribe = (channel) => {
|
||||
delete channels.current[channel]
|
||||
const { authenticated, loading } = useContext(AuthContext);
|
||||
const ws = useRef(null);
|
||||
const [socket, setSocket] = useState(null);
|
||||
const channels = useRef({}); // maps each channel to the callback
|
||||
const { account, setAccount } = useContext(AccountContext);
|
||||
const [currentChannel, setCurrentChannel] = useState("");
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
/* called from a component that registers a callback for a channel */
|
||||
const subscribe = (channel, callback) => {
|
||||
//console.log(`Subbing to ${channel}`)
|
||||
setCurrentChannel(channel);
|
||||
channels.current[channel] = callback;
|
||||
};
|
||||
/* remove callback */
|
||||
const unsubscribe = (channel) => {
|
||||
delete channels.current[channel];
|
||||
};
|
||||
|
||||
const sendMessage = (message, conversation_id, file, fileType, modelName) => {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64File = reader.result?.toString().split(",")[1];
|
||||
if (base64File) {
|
||||
const data = {
|
||||
message: message,
|
||||
conversation_id: conversation_id,
|
||||
email: account?.email,
|
||||
file: base64File,
|
||||
fileType: fileType,
|
||||
modelName: modelName,
|
||||
};
|
||||
socket.send(JSON.stringify(data));
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
const data = {
|
||||
message: message,
|
||||
conversation_id: conversation_id,
|
||||
email: account?.email,
|
||||
file: null,
|
||||
fileType: null,
|
||||
modelName: modelName,
|
||||
};
|
||||
|
||||
socket.send(JSON.stringify(data));
|
||||
}
|
||||
//socket.send(`${conversation_id} | ${message}`)
|
||||
} else {
|
||||
console.log("Error sending message. WebSocket is not open");
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
/* WS initialization and cleanup */
|
||||
if (account) {
|
||||
ws.current = new WebSocket(`ws://localhost:8011/ws/chat_again/`);
|
||||
//ws.current = new WebSocket(`ws://localhost:8011/ws/conditional_chat/`);
|
||||
//ws.current = new WebSocket('wss://chatbackend.aimloperations.com/ws/chat_again/')
|
||||
//ws.current = new WebSocket('wss://chatbackend.aimloperations.com/ws/conditional_chat/')
|
||||
//ws.current = process.env.REACT_APP_BACKEND_WS_API_BASE_URL;
|
||||
|
||||
const sendMessage = (message, conversation_id, file, fileType, modelName) => {
|
||||
if (socket && socket.readyState === WebSocket.OPEN){
|
||||
|
||||
if (file){
|
||||
ws.current.onopen = () => {
|
||||
setSocket(ws.current);
|
||||
setIsConnected(true);
|
||||
};
|
||||
ws.current.onclose = () => {
|
||||
console.log('websocket closed');
|
||||
setIsConnected(false);
|
||||
};
|
||||
ws.current.onmessage = (message) => {
|
||||
const data = message.data;
|
||||
// lookup for an existing chat in which this message belongs
|
||||
// if no chat is subscribed send message to generic channel
|
||||
const chatChannel = Object.entries(channels.current)[0][0];
|
||||
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64File = reader.result?.toString().split(',')[1];
|
||||
if (base64File){
|
||||
const data = {
|
||||
message: message,
|
||||
conversation_id: conversation_id,
|
||||
email: account?.email,
|
||||
file: base64File,
|
||||
fileType: fileType,
|
||||
modelName: modelName,
|
||||
}
|
||||
socket.send(JSON.stringify(data))
|
||||
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}else{
|
||||
const data = {
|
||||
message: message,
|
||||
conversation_id: conversation_id,
|
||||
email: account?.email,
|
||||
file: null,
|
||||
fileType: null,
|
||||
modelName: modelName,
|
||||
}
|
||||
|
||||
socket.send(JSON.stringify(data))
|
||||
}
|
||||
//socket.send(`${conversation_id} | ${message}`)
|
||||
|
||||
}else{
|
||||
console.log('Error sending message. WebSocket is not open')
|
||||
if (channels.current[chatChannel]) {
|
||||
/* in chat component the subscribed channel is `MESSAGE_CREATE_${id}` */
|
||||
channels.current[chatChannel](data);
|
||||
} else {
|
||||
/* in notifications wrapper the subscribed channel is `MESSAGE_CREATE` */
|
||||
console.log("Error");
|
||||
// channels.current[type]?.(data)
|
||||
}
|
||||
|
||||
};
|
||||
return () => {
|
||||
ws.current.close();
|
||||
};
|
||||
}
|
||||
useEffect(() => {
|
||||
/* WS initialization and cleanup */
|
||||
if (account){
|
||||
|
||||
|
||||
//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 = process.env.REACT_APP_BACKEND_WS_API_BASE_URL;
|
||||
}, [account]);
|
||||
|
||||
ws.current.onopen = () => { setSocket(ws.current); }
|
||||
ws.current.onclose = () => { }
|
||||
ws.current.onmessage = (message) => {
|
||||
const data = message.data
|
||||
// lookup for an existing chat in which this message belongs
|
||||
// if no chat is subscribed send message to generic channel
|
||||
const chatChannel = Object.entries(channels.current)[0][0]
|
||||
|
||||
if (channels.current[chatChannel]) {
|
||||
/* in chat component the subscribed channel is `MESSAGE_CREATE_${id}` */
|
||||
channels.current[chatChannel](data)
|
||||
} else {
|
||||
/* in notifications wrapper the subscribed channel is `MESSAGE_CREATE` */
|
||||
console.log('Error')
|
||||
// channels.current[type]?.(data)
|
||||
}
|
||||
}
|
||||
return () => { ws.current.close() }
|
||||
|
||||
}
|
||||
|
||||
|
||||
}, [account])
|
||||
|
||||
|
||||
|
||||
/* WS provider dom */
|
||||
/* subscribe and unsubscribe are the only required prop for the context */
|
||||
return (
|
||||
<WebSocketContext.Provider value={[subscribe, unsubscribe, socket, sendMessage]}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
)
|
||||
/* WS provider dom */
|
||||
/* subscribe and unsubscribe are the only required prop for the context */
|
||||
return (
|
||||
<WebSocketContext.Provider
|
||||
value={[subscribe, unsubscribe, socket, sendMessage, isConnected]}
|
||||
>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { WebSocketContext, WebSocketProvider }
|
||||
export { WebSocketContext, WebSocketProvider };
|
||||
|
||||
@@ -1,291 +1,445 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import PageWrapperLayout from "../../components/PageWrapperLayout/PageWrapperLayout";
|
||||
import { AccountContext } from "../../contexts/AccountContext";
|
||||
import MDTypography from "../../ui-kit/components/MDTypography";
|
||||
import { Account, AccountType } from "../../data";
|
||||
import * as Yup from 'yup';
|
||||
import { Card, CardContent, Divider, IconButton, InputAdornment, Paper, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from "@mui/material";
|
||||
import { DeleteForever, Send } from "@mui/icons-material";
|
||||
import { axiosInstance } from "../../../axiosApi";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { ErrorMessage, Field, Form, Formik } from "formik";
|
||||
import MDButton from "../../ui-kit/components/MDButton";
|
||||
import MDBox from "../../ui-kit/components/MDBox";
|
||||
import Header2 from "../../components/Header2/Header2";
|
||||
import Footer from "../../components/Footer/Footer";
|
||||
import ThemeCard from "../../components/ThemeCard/ThemeCard";
|
||||
import ParticleBackground from "../../components/ParticleBackground/ParticleBackground";
|
||||
import styled, { ThemeProvider } from "styled-components";
|
||||
import ThemeSettingsCard from "../../components/ThemeSettingsCard/ThemeSettingsCard";
|
||||
import { useMaterialUIController } from "../../ui-kit/context";
|
||||
import palettes from "../../ui-kit/assets/theme/base/palettes";
|
||||
|
||||
// Styled Components
|
||||
const PageContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-family: 'Inter', sans-serif;
|
||||
/* background-color: ${({ theme }) => theme.colors.background}; Removed to show particles */
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6rem 2rem 2rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const GlassCard = styled.div`
|
||||
background: ${({ theme }) => theme.colors.cardBackground};
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
padding-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const StyledTable = styled.table`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
const Th = styled.th`
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
opacity: 0.7;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const Td = styled.td`
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'};
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.8rem;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:focus {
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
border-color: ${({ theme }) => theme.main};
|
||||
box-shadow: 0 0 10px ${({ theme }) => theme.main}33; // 33 is approx 20% opacity
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled.button`
|
||||
background: ${({ theme }) => theme.main};
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
padding: 0.8rem 1.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
margin-top: 1rem;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px ${({ theme }) => theme.main}66; // 66 is approx 40% opacity
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const ToggleSwitch = styled.label`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const Slider = styled.span`
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'};
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Checkbox = styled.input`
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:checked + ${Slider} {
|
||||
background-color: ${({ theme }) => theme.main};
|
||||
}
|
||||
|
||||
&:checked + ${Slider}:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
&:disabled + ${Slider} {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
const DeleteButton = styled.button`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #ff6b6b;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #ff4757;
|
||||
}
|
||||
`;
|
||||
|
||||
type AccountUpdateValues = {
|
||||
email: string,
|
||||
email: string,
|
||||
}
|
||||
|
||||
type AccountInformationProps = {
|
||||
account: Account | undefined
|
||||
account: Account | undefined
|
||||
}
|
||||
|
||||
type InviteValues = {
|
||||
email: string
|
||||
email: string
|
||||
}
|
||||
const validationSchema = Yup.object().shape({
|
||||
email: Yup.string().email().required("This is requried")
|
||||
}
|
||||
email: Yup.string().email().required("This is required")
|
||||
}
|
||||
)
|
||||
|
||||
type AddUserCardProps = {
|
||||
getCompanyUsers: ()=> void;
|
||||
getCompanyUsers: () => void;
|
||||
}
|
||||
|
||||
type CompanyAccountLineProps = {
|
||||
user: Account
|
||||
handleUserUpdate: (email: string, field: string, value: string) => void
|
||||
user: Account
|
||||
handleUserUpdate: (email: string, field: string, value: string) => void
|
||||
}
|
||||
|
||||
const CompanyAccountLine = ({user, handleUserUpdate}: CompanyAccountLineProps): JSX.Element => {
|
||||
const {account} = useContext(AccountContext);
|
||||
|
||||
return(
|
||||
<TableRow key={user.email}>
|
||||
<TableCell><MDTypography> {user.email}</MDTypography></TableCell>
|
||||
<TableCell><MDTypography> {user.first_name}</MDTypography></TableCell>
|
||||
<TableCell><MDTypography>{user.last_name}</MDTypography></TableCell>
|
||||
<TableCell >
|
||||
<Switch checked={user.is_company_manager} onChange={(event) => handleUserUpdate(user.email, 'company_manager', event.target.value)} disabled={account?.email === user.email} />
|
||||
</TableCell>
|
||||
<TableCell >
|
||||
<Switch checked={user.is_active} onChange={(event) => handleUserUpdate(user.email, 'is_active', event.target.value)} />
|
||||
</TableCell>
|
||||
<TableCell >
|
||||
<Switch checked={user.has_password && user.has_signed_tos} onChange={(event) => handleUserUpdate(user.email, 'has_password', event.target.value)} disabled={!user.has_password} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton onClick={() => handleUserUpdate(user.email, 'delete', '')}>
|
||||
<DeleteForever color="warning" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
const CompanyAccountLine = ({ user, handleUserUpdate }: CompanyAccountLineProps): JSX.Element => {
|
||||
const { account } = useContext(AccountContext);
|
||||
|
||||
const AddUserCard = ({getCompanyUsers}: AddUserCardProps): JSX.Element => {
|
||||
const initialValues = {'email': '',}
|
||||
const handleInvite = async ({email}: InviteValues, {resetForm}: any): Promise<void> => {
|
||||
try{
|
||||
await axiosInstance.post('/user/invite/', {
|
||||
'email': email
|
||||
})
|
||||
getCompanyUsers()
|
||||
|
||||
} catch{
|
||||
// put a message here
|
||||
}
|
||||
resetForm();
|
||||
|
||||
}
|
||||
return(
|
||||
<Card sx={{mt:1}}>
|
||||
<CardContent>
|
||||
<MDTypography variant="h3">
|
||||
Invite A User
|
||||
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
|
||||
<CardContent>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleInvite}
|
||||
validateOnMount
|
||||
validationSchema={validationSchema}>
|
||||
{(formik) =>
|
||||
<Form>
|
||||
<div className="row">
|
||||
<div className='col-12'>
|
||||
<Field
|
||||
name={"email"}
|
||||
fullWidth
|
||||
as={TextField}
|
||||
label={"Email"}
|
||||
errorstring={<ErrorMessage name={"prompt"}/>}
|
||||
size={'small'}
|
||||
role={undefined}
|
||||
tabIndex={-1}
|
||||
margin={"dense"}
|
||||
variant={"outlined"}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<MDButton
|
||||
type={'submit'}
|
||||
startIcon={<Send/>}
|
||||
disabled={!formik.isValid || formik.isSubmitting}>
|
||||
<></>
|
||||
|
||||
</MDButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
>
|
||||
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const CompanyManagerCard = ({}): JSX.Element => {
|
||||
const [users, setUsers] = useState<Account[]>([]);
|
||||
const {account}= useContext(AccountContext);
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
|
||||
const handleUserUpdate = async(email: string, field: string, value: string): Promise<void> => {
|
||||
//console.log(email, field, 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 getCompanyUsers(){
|
||||
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,
|
||||
has_signed_tos: item.has_signed_tos,
|
||||
company: undefined
|
||||
})))
|
||||
|
||||
|
||||
}catch(error){
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
getCompanyUsers();
|
||||
}, [])
|
||||
|
||||
return(
|
||||
<>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<MDTypography variant="h3">
|
||||
Company Accounts
|
||||
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
|
||||
<CardContent>
|
||||
<TableContainer component={Paper}>
|
||||
<Table >
|
||||
<TableHead sx={{ display: "table-header-group" }}>
|
||||
<TableRow>
|
||||
<TableCell><MDTypography>Email</MDTypography></TableCell>
|
||||
<TableCell><MDTypography>First Name</MDTypography></TableCell>
|
||||
<TableCell><MDTypography>Last Name</MDTypography></TableCell>
|
||||
<TableCell><MDTypography>Is Manager</MDTypography></TableCell>
|
||||
<TableCell><MDTypography>Is Active</MDTypography></TableCell>
|
||||
<TableCell><MDTypography>Has Password</MDTypography></TableCell>
|
||||
<TableCell><MDTypography>Delete</MDTypography></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map((user) => <CompanyAccountLine user={user} handleUserUpdate={handleUserUpdate} key={user.email}/>)}
|
||||
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AddUserCard getCompanyUsers={getCompanyUsers} />
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const AccountPage =({}): JSX.Element => {
|
||||
const { account } = useContext(AccountContext)
|
||||
if(account?.is_company_manager){
|
||||
|
||||
return(
|
||||
<>
|
||||
<ThemeCard />
|
||||
<CompanyManagerCard />
|
||||
</>
|
||||
|
||||
)
|
||||
|
||||
}else{
|
||||
return(
|
||||
<>
|
||||
<ThemeCard />
|
||||
<MDTypography>Account and prompt information will be available soon</MDTypography>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Account2 = ({}): JSX.Element => {
|
||||
return (
|
||||
<PageWrapperLayout>
|
||||
<tr>
|
||||
<Td>{user.email}</Td>
|
||||
<Td>{user.first_name}</Td>
|
||||
<Td>{user.last_name}</Td>
|
||||
<Td>
|
||||
<ToggleSwitch>
|
||||
<Checkbox
|
||||
type="checkbox"
|
||||
checked={user.is_company_manager}
|
||||
onChange={(event) => handleUserUpdate(user.email, 'company_manager', String(event.target.checked))}
|
||||
disabled={account?.email === user.email}
|
||||
/>
|
||||
<Slider />
|
||||
</ToggleSwitch>
|
||||
</Td>
|
||||
<Td>
|
||||
<ToggleSwitch>
|
||||
<Checkbox
|
||||
type="checkbox"
|
||||
checked={user.is_active}
|
||||
onChange={(event) => handleUserUpdate(user.email, 'is_active', String(event.target.checked))}
|
||||
/>
|
||||
<Slider />
|
||||
</ToggleSwitch>
|
||||
</Td>
|
||||
<Td>
|
||||
<ToggleSwitch>
|
||||
<Checkbox
|
||||
type="checkbox"
|
||||
checked={user.has_password && user.has_signed_tos}
|
||||
onChange={(event) => handleUserUpdate(user.email, 'has_password', String(event.target.checked))}
|
||||
disabled={!user.has_password}
|
||||
/>
|
||||
<Slider />
|
||||
</ToggleSwitch>
|
||||
</Td>
|
||||
<Td>
|
||||
<DeleteButton onClick={() => handleUserUpdate(user.email, 'delete', '')}>
|
||||
🗑️
|
||||
</DeleteButton>
|
||||
</Td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const AddUserCard = ({ getCompanyUsers }: AddUserCardProps): JSX.Element => {
|
||||
const initialValues = { 'email': '', }
|
||||
const handleInvite = async ({ email }: InviteValues, { resetForm }: any): Promise<void> => {
|
||||
try {
|
||||
await axiosInstance.post('/user/invite/', {
|
||||
'email': email
|
||||
})
|
||||
getCompanyUsers()
|
||||
|
||||
} catch {
|
||||
// put a message here
|
||||
}
|
||||
resetForm();
|
||||
|
||||
}
|
||||
return (
|
||||
<GlassCard>
|
||||
<CardTitle>Invite A User</CardTitle>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleInvite}
|
||||
validateOnMount
|
||||
validationSchema={validationSchema}>
|
||||
{(formik) =>
|
||||
<Form>
|
||||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Field
|
||||
name={"email"}
|
||||
as={StyledInput}
|
||||
placeholder={"Email Address"}
|
||||
/>
|
||||
<ErrorMessage name="email">
|
||||
{msg => <div style={{ color: '#ff6b6b', marginTop: '0.5rem', fontSize: '0.9rem' }}>{msg}</div>}
|
||||
</ErrorMessage>
|
||||
</div>
|
||||
<StyledButton
|
||||
type={'submit'}
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
style={{ marginTop: 0 }}
|
||||
>
|
||||
Invite
|
||||
</StyledButton>
|
||||
</div>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</GlassCard>
|
||||
)
|
||||
}
|
||||
|
||||
const CompanyManagerCard = ({ }): JSX.Element => {
|
||||
const [users, setUsers] = useState<Account[]>([]);
|
||||
const { account } = useContext(AccountContext);
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
|
||||
const handleUserUpdate = async (email: string, field: string, value: string): Promise<void> => {
|
||||
//console.log(email, field, 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 getCompanyUsers() {
|
||||
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,
|
||||
has_signed_tos: item.has_signed_tos,
|
||||
company: undefined
|
||||
})))
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getCompanyUsers();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlassCard>
|
||||
<CardTitle>Company Accounts</CardTitle>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<StyledTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<Th>Email</Th>
|
||||
<Th>First Name</Th>
|
||||
<Th>Last Name</Th>
|
||||
<Th>Is Manager</Th>
|
||||
<Th>Is Active</Th>
|
||||
<Th>Has Password</Th>
|
||||
<Th>Delete</Th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user) => <CompanyAccountLine user={user} handleUserUpdate={handleUserUpdate} key={user.email} />)}
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
</div>
|
||||
</GlassCard>
|
||||
<AddUserCard getCompanyUsers={getCompanyUsers} />
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const AccountPage = ({ }): JSX.Element => {
|
||||
const { account } = useContext(AccountContext)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThemeSettingsCard />
|
||||
{account?.is_company_manager ? (
|
||||
<CompanyManagerCard />
|
||||
) : (
|
||||
<GlassCard>
|
||||
<CardTitle>Account Information</CardTitle>
|
||||
<p style={{ color: 'rgba(255,255,255,0.7)' }}>Account and prompt information will be available soon</p>
|
||||
</GlassCard>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Account2 = ({ }): JSX.Element => {
|
||||
return (
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<Header2 />
|
||||
<MDBox sx={{mt:12}}>
|
||||
|
||||
</MDBox>
|
||||
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
|
||||
<ContentWrapper>
|
||||
<AccountPage />
|
||||
<Footer />
|
||||
</MDBox>
|
||||
|
||||
|
||||
</PageWrapperLayout>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,256 +1,259 @@
|
||||
import { useContext, useEffect, useState } from "react"
|
||||
import Footer from "../../components/Footer/Footer"
|
||||
import Header2 from "../../components/Header2/Header2"
|
||||
import PageWrapperLayout from "../../components/PageWrapperLayout/PageWrapperLayout"
|
||||
import MDBox from "../../ui-kit/components/MDBox"
|
||||
import { AccountContext } from "../../contexts/AccountContext"
|
||||
import { Card, CardContent, Divider } from "@mui/material"
|
||||
import MDTypography from "../../ui-kit/components/MDTypography"
|
||||
import { Area, Bar, BarChart, ComposedChart, Legend, Line, LineChart, Rectangle, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
|
||||
import { Area, Bar, BarChart, ComposedChart, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis, Rectangle } from "recharts"
|
||||
import { axiosInstance } from "../../../axiosApi"
|
||||
import { AxiosResponse } from "axios"
|
||||
import { AdminAnalytics, AdminAnalyticsType, CompanyUsageAnalytics, CompanyUsageAnalyticsType, UserConversationAnalytics, UserConvesationAnalyticsType, UserPromptAnalytics, UserPromptAnalyticsType } from "../../data"
|
||||
import ParticleBackground from "../../components/ParticleBackground/ParticleBackground"
|
||||
import styled, { ThemeContext } from "styled-components"
|
||||
|
||||
const UserPromptAnalyticsCard = ({}): JSX.Element => {
|
||||
// Styled Components
|
||||
const PageContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-family: 'Inter', sans-serif;
|
||||
/* background-color: ${({ theme }) => theme.colors.background}; Removed to show particles */
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6rem 2rem 2rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const GlassCard = styled.div`
|
||||
background: ${({ theme }) => theme.colors.cardBackground};
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
padding-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const GridContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
|
||||
@media (min-width: 992px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChartContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
`;
|
||||
|
||||
const UserPromptAnalyticsCard = ({ }): JSX.Element => {
|
||||
const [data, setData] = useState<UserPromptAnalytics[]>([])
|
||||
|
||||
async function getUserPromptAnalytics(){
|
||||
try{
|
||||
const {data, }: AxiosResponse<UserPromptAnalyticsType[]> = await axiosInstance.get(`/analytics/user_prompts/`);
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
async function getUserPromptAnalytics() {
|
||||
try {
|
||||
const { data, }: AxiosResponse<UserPromptAnalyticsType[]> = await axiosInstance.get(`/analytics/user_prompts/`);
|
||||
setData(data)
|
||||
|
||||
} catch (error) {
|
||||
|
||||
|
||||
}catch(error){
|
||||
|
||||
}
|
||||
}
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
getUserPromptAnalytics();
|
||||
}, [])
|
||||
return (
|
||||
<Card sx={{ mb: 1}}>
|
||||
<CardContent>
|
||||
<MDTypography variant="h3">
|
||||
Prompt Usage
|
||||
</MDTypography>
|
||||
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<div style={{ width: "100%", height: 600}} >
|
||||
<GlassCard>
|
||||
<CardTitle>Prompt Usage</CardTitle>
|
||||
<ChartContainer>
|
||||
<ResponsiveContainer>
|
||||
<BarChart data={data}>
|
||||
<XAxis />
|
||||
<YAxis />
|
||||
<Legend />
|
||||
<Bar dataKey="you" fill="#8884d8" activeBar={<Rectangle fill="pink" stroke="blue" />}/>
|
||||
<Bar dataKey="others" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple" />}/>
|
||||
<Bar dataKey="all" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple" />}/>
|
||||
<XAxis stroke={theme?.colors.text} />
|
||||
<YAxis stroke={theme?.colors.text} />
|
||||
<Legend wrapperStyle={{ color: theme?.colors.text }} />
|
||||
<Tooltip contentStyle={{ backgroundColor: theme?.colors.cardBackground, border: `1px solid ${theme?.colors.cardBorder}`, color: theme?.colors.text }} />
|
||||
<Bar dataKey="you" fill={theme?.main || "#667eea"} activeBar={<Rectangle fill={theme?.main ? theme.main + "99" : "#764ba2"} stroke={theme?.colors.text} />} />
|
||||
<Bar dataKey="others" fill="#82ca9d" activeBar={<Rectangle fill="#a8e6cf" stroke={theme?.colors.text} />} />
|
||||
<Bar dataKey="all" fill="#ffb7b2" activeBar={<Rectangle fill="#ffdac1" stroke={theme?.colors.text} />} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChartContainer>
|
||||
</GlassCard>
|
||||
)
|
||||
}
|
||||
|
||||
const UserConversationAnalyticsCard = ({}): JSX.Element => {
|
||||
const UserConversationAnalyticsCard = ({ }): JSX.Element => {
|
||||
const [data, setData] = useState<UserConversationAnalytics[]>([])
|
||||
async function getUserConversationAnalytics(){
|
||||
try{
|
||||
const {data, }: AxiosResponse<UserConvesationAnalyticsType[]> = await axiosInstance.get(`/analytics/user_conversations/`);
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
async function getUserConversationAnalytics() {
|
||||
try {
|
||||
const { data, }: AxiosResponse<UserConvesationAnalyticsType[]> = await axiosInstance.get(`/analytics/user_conversations/`);
|
||||
setData(data)
|
||||
|
||||
} catch (error) {
|
||||
|
||||
|
||||
}catch(error){
|
||||
|
||||
}
|
||||
}
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
getUserConversationAnalytics();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card sx={{ mb: 1}}>
|
||||
<CardContent>
|
||||
<MDTypography variant="h3">
|
||||
Conversation Usage
|
||||
</MDTypography>
|
||||
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<div style={{ width: "100%", height: 600}} >
|
||||
<GlassCard>
|
||||
<CardTitle>Conversation Usage</CardTitle>
|
||||
<ChartContainer>
|
||||
<ResponsiveContainer>
|
||||
|
||||
|
||||
<BarChart data={data}>
|
||||
<XAxis />
|
||||
<YAxis />
|
||||
<Legend />
|
||||
<Bar dataKey="you" fill="#8884d8" activeBar={<Rectangle fill="pink" stroke="blue" />}/>
|
||||
<Bar dataKey="others" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple" />}/>
|
||||
<Bar dataKey="all" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple" />}/>
|
||||
<XAxis stroke={theme?.colors.text} />
|
||||
<YAxis stroke={theme?.colors.text} />
|
||||
<Legend wrapperStyle={{ color: theme?.colors.text }} />
|
||||
<Tooltip contentStyle={{ backgroundColor: theme?.colors.cardBackground, border: `1px solid ${theme?.colors.cardBorder}`, color: theme?.colors.text }} />
|
||||
<Bar dataKey="you" fill={theme?.main || "#667eea"} activeBar={<Rectangle fill={theme?.main ? theme.main + "99" : "#764ba2"} stroke={theme?.colors.text} />} />
|
||||
<Bar dataKey="others" fill="#82ca9d" activeBar={<Rectangle fill="#a8e6cf" stroke={theme?.colors.text} />} />
|
||||
<Bar dataKey="all" fill="#ffb7b2" activeBar={<Rectangle fill="#ffdac1" stroke={theme?.colors.text} />} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChartContainer>
|
||||
</GlassCard>
|
||||
)
|
||||
}
|
||||
|
||||
const CompanyUsageAnalyticsCard = ({}): JSX.Element => {
|
||||
const CompanyUsageAnalyticsCard = ({ }): JSX.Element => {
|
||||
const [data, setData] = useState<CompanyUsageAnalytics[]>([])
|
||||
async function getCompanyUsageAnalytics(){
|
||||
try{
|
||||
const {data, }: AxiosResponse<CompanyUsageAnalyticsType[]> = await axiosInstance.get(`/analytics/company_usage/`);
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
async function getCompanyUsageAnalytics() {
|
||||
try {
|
||||
const { data, }: AxiosResponse<CompanyUsageAnalyticsType[]> = await axiosInstance.get(`/analytics/company_usage/`);
|
||||
setData(data)
|
||||
|
||||
} catch (error) {
|
||||
|
||||
|
||||
}catch(error){
|
||||
|
||||
}
|
||||
}
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
getCompanyUsageAnalytics();
|
||||
}, [])
|
||||
// const exampleData = [
|
||||
// {
|
||||
// month: 'Feb',
|
||||
// used: 10,
|
||||
// not_used: 5
|
||||
// },
|
||||
// {
|
||||
// month: 'Mar',
|
||||
// used: 7,
|
||||
// not_used: 8
|
||||
// },
|
||||
// {
|
||||
// month: 'Apr',
|
||||
// used: 15,
|
||||
// not_used: 0
|
||||
// },
|
||||
// ]
|
||||
return (
|
||||
<Card sx={{ mb: 1}}>
|
||||
<CardContent>
|
||||
<MDTypography variant="h3">
|
||||
Account Usage
|
||||
</MDTypography>
|
||||
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<div style={{ width: "100%", height: 600}} >
|
||||
<ResponsiveContainer>
|
||||
<BarChart data={data}>
|
||||
<XAxis dataKey="month"/>
|
||||
<YAxis />
|
||||
<Legend />
|
||||
<Tooltip />
|
||||
<Bar dataKey="used" fill="#8884d8" activeBar={<Rectangle fill="pink" stroke="blue" />}/>
|
||||
<Bar dataKey="not_used" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple" />}/>
|
||||
</BarChart>
|
||||
return (
|
||||
<GlassCard>
|
||||
<CardTitle>Account Usage</CardTitle>
|
||||
<ChartContainer>
|
||||
<ResponsiveContainer>
|
||||
<BarChart data={data}>
|
||||
<XAxis dataKey="month" stroke={theme?.colors.text} />
|
||||
<YAxis stroke={theme?.colors.text} />
|
||||
<Legend wrapperStyle={{ color: theme?.colors.text }} />
|
||||
<Tooltip contentStyle={{ backgroundColor: theme?.colors.cardBackground, border: `1px solid ${theme?.colors.cardBorder}`, color: theme?.colors.text }} />
|
||||
<Bar dataKey="used" fill={theme?.main || "#667eea"} activeBar={<Rectangle fill={theme?.main ? theme.main + "99" : "#764ba2"} stroke={theme?.colors.text} />} />
|
||||
<Bar dataKey="not_used" fill="#82ca9d" activeBar={<Rectangle fill="#a8e6cf" stroke={theme?.colors.text} />} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChartContainer>
|
||||
</GlassCard>
|
||||
)
|
||||
}
|
||||
|
||||
const AdminAnalyticsCard = ({}): JSX.Element => {
|
||||
const AdminAnalyticsCard = ({ }): JSX.Element => {
|
||||
const [data, setData] = useState<AdminAnalytics[]>([])
|
||||
async function getAdminAnalytics(){
|
||||
try{
|
||||
const {data, }: AxiosResponse<AdminAnalyticsType[]> = await axiosInstance.get(`/analytics/admin/`);
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
async function getAdminAnalytics() {
|
||||
try {
|
||||
const { data, }: AxiosResponse<AdminAnalyticsType[]> = await axiosInstance.get(`/analytics/admin/`);
|
||||
setData(data)
|
||||
|
||||
} catch (error) {
|
||||
|
||||
|
||||
}catch(error){
|
||||
|
||||
}
|
||||
}
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
getAdminAnalytics();
|
||||
}, [])
|
||||
return (
|
||||
<Card sx={{ mb: 1}}>
|
||||
<CardContent>
|
||||
<MDTypography variant="h3">
|
||||
Response Times
|
||||
</MDTypography>
|
||||
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<div style={{ width: "100%", height: 600}} >
|
||||
<ResponsiveContainer>
|
||||
<ComposedChart data={data}>
|
||||
<XAxis dataKey="month"/>
|
||||
<YAxis />
|
||||
<Legend />
|
||||
<Tooltip />
|
||||
<Area
|
||||
dataKey="range"
|
||||
dot={false}
|
||||
connectNulls
|
||||
activeDot={false}
|
||||
/>
|
||||
<Line type="natural" dataKey="avg" connectNulls />
|
||||
<Line type="monotone" dataKey="median" connectNulls />
|
||||
</ComposedChart>
|
||||
<GlassCard>
|
||||
<CardTitle>Response Times</CardTitle>
|
||||
<ChartContainer>
|
||||
<ResponsiveContainer>
|
||||
<ComposedChart data={data}>
|
||||
<XAxis dataKey="month" stroke={theme?.colors.text} />
|
||||
<YAxis stroke={theme?.colors.text} />
|
||||
<Legend wrapperStyle={{ color: theme?.colors.text }} />
|
||||
<Tooltip contentStyle={{ backgroundColor: theme?.colors.cardBackground, border: `1px solid ${theme?.colors.cardBorder}`, color: theme?.colors.text }} />
|
||||
<Area
|
||||
dataKey="range"
|
||||
dot={false}
|
||||
connectNulls
|
||||
activeDot={false}
|
||||
fill="#8884d8"
|
||||
stroke="#8884d8"
|
||||
/>
|
||||
<Line type="natural" dataKey="avg" connectNulls stroke="#ff7300" />
|
||||
<Line type="monotone" dataKey="median" connectNulls stroke="#387908" />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChartContainer>
|
||||
</GlassCard>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const AnalyticsInner =({}): JSX.Element => {
|
||||
const AnalyticsInner = ({ }): JSX.Element => {
|
||||
const { account } = useContext(AccountContext)
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
<div className='col-6 col-xs-12'>
|
||||
<GridContainer>
|
||||
<UserConversationAnalyticsCard />
|
||||
|
||||
</div>
|
||||
<div className='col-6 col-xs-12'>
|
||||
<UserPromptAnalyticsCard />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</GridContainer>
|
||||
|
||||
{account?.is_company_manager ? <CompanyUsageAnalyticsCard /> : <></>}
|
||||
{account?.email === "ryan+admin@aimloperations.com" ? <AdminAnalyticsCard /> : <></>}
|
||||
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const AnalyticsPage = ({}): JSX.Element => {
|
||||
const AnalyticsPage = ({ }): JSX.Element => {
|
||||
return (
|
||||
<PageWrapperLayout>
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<Header2 />
|
||||
<MDBox sx={{mt:12}}>
|
||||
|
||||
</MDBox>
|
||||
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
|
||||
<ContentWrapper>
|
||||
<AnalyticsInner />
|
||||
<Footer />
|
||||
</MDBox>
|
||||
</PageWrapperLayout>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +1,286 @@
|
||||
import { Card, CardContent, Divider, InputAdornment, MenuItem, Select, } from "@mui/material"
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import styled, { ThemeContext } from "styled-components";
|
||||
import { Formik, Form, Field, ErrorMessage } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { AttachFile, Delete, Send, Menu } from "@mui/icons-material"; // Keeping icons for now, can replace later if needed
|
||||
import { Tooltip } from "@mui/material";
|
||||
import Markdown from "markdown-to-jsx";
|
||||
|
||||
import DashboardLayout from "../../ui-kit/examples/LayoutContainers/DashboardLayout"
|
||||
import {
|
||||
Announcement,
|
||||
AnnouncementType,
|
||||
Conversation,
|
||||
ConversationPrompt,
|
||||
ConversationPromptType,
|
||||
} from "../../data";
|
||||
import { axiosInstance } from "../../../axiosApi";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { ConversationContext } from "../../contexts/ConversationContext";
|
||||
import ConversationDetailCard from "../../components/ConversationDetailCard/ConversationDetailCard";
|
||||
import { WebSocketContext } from "../../contexts/WebSocketContext";
|
||||
import { AccountContext } from "../../contexts/AccountContext";
|
||||
import { MessageContext } from "../../contexts/MessageContext";
|
||||
import ParticleBackground from "../../components/ParticleBackground/ParticleBackground";
|
||||
|
||||
import MDBox from "../../ui-kit/components/MDBox"
|
||||
import { useMaterialUIController } from "../../ui-kit/context"
|
||||
import { useContext, useEffect, useRef, useState } from "react"
|
||||
import Header2 from "../../components/Header2/Header2";
|
||||
|
||||
// Images
|
||||
import MDTypography from "../../ui-kit/components/MDTypography"
|
||||
import { CardFooter } from "react-bootstrap"
|
||||
import MDInput from "../../ui-kit/components/MDInput"
|
||||
import { ErrorMessage, Field, Form, Formik } from "formik"
|
||||
import MDButton from "../../ui-kit/components/MDButton"
|
||||
import { AttachFile, Send } from "@mui/icons-material"
|
||||
import Header2 from "../../components/Header2/Header2"
|
||||
import DashboardWrapperLayout from "../../components/DashboardWrapperLayout/DashboardWrapperLayout"
|
||||
import * as Yup from 'yup';
|
||||
import { Announcement, AnnouncementType, Conversation, ConversationPrompt, ConversationPromptType, ConversationType } from "../../data"
|
||||
import { axiosInstance } from "../../../axiosApi"
|
||||
import { AxiosResponse } from "axios"
|
||||
import { ConversationContext } from "../../contexts/ConversationContext"
|
||||
import Markdown from "markdown-to-jsx"
|
||||
import ConversationDetailCard from "../../components/ConversationDetailCard/ConversationDetailCard"
|
||||
import { WebSocketContext } from "../../contexts/WebSocketContext"
|
||||
import { AccountContext } from "../../contexts/AccountContext"
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Footer from "../../components/Footer/Footer"
|
||||
import { MessageContext } from "../../contexts/MessageContext"
|
||||
import CustomSelect, { CustomSelectItem } from "../../components/CustomSelect/CustomSelect"
|
||||
// Styled Components
|
||||
const PageContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-family: 'Inter', sans-serif;
|
||||
/* background-color: ${({ theme }) => theme.colors.background}; Removed to show particles */
|
||||
`;
|
||||
|
||||
const MODELS = ["Turbo","RAG"]
|
||||
const Sidebar = styled.div<{ $isOpen: boolean }>`
|
||||
width: 280px;
|
||||
height: 100%;
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(0, 0, 0, 0.6)' : 'rgba(255, 255, 255, 0.6)'};
|
||||
backdrop-filter: blur(10px);
|
||||
border-right: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
padding-top: 5rem; // Account for header
|
||||
z-index: 20;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
type RenderMessageProps= {
|
||||
response: string
|
||||
index: number
|
||||
};
|
||||
@media (max-width: 768px) {
|
||||
position: absolute;
|
||||
transform: ${({ $isOpen }) => $isOpen ? 'translateX(0)' : 'translateX(-100%)'};
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(0, 0, 0, 0.95)' : 'rgba(255, 255, 255, 0.95)'};
|
||||
box-shadow: ${({ $isOpen }) => $isOpen ? '0 0 20px rgba(0,0,0,0.5)' : 'none'};
|
||||
}
|
||||
`;
|
||||
|
||||
type AsyncChatProps = {
|
||||
selectedConversation: number | undefined
|
||||
conversationTitle: string
|
||||
conversations: Conversation[]
|
||||
setConversations: React.Dispatch<React.SetStateAction<Conversation[]>>
|
||||
setSelectedConversation: React.Dispatch<React.SetStateAction<number | undefined>>
|
||||
drawerWidth: number;
|
||||
}
|
||||
const MobileSidebarToggle = styled.button`
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 5.5rem; /* Below header */
|
||||
left: 1rem;
|
||||
z-index: 15;
|
||||
padding: 0.5rem 1rem;
|
||||
background: ${({ theme }) => theme.main};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 2rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Overlay = styled.div<{ $isOpen: boolean }>`
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 15;
|
||||
opacity: ${({ $isOpen }) => $isOpen ? 1 : 0};
|
||||
pointer-events: ${({ $isOpen }) => $isOpen ? 'auto' : 'none'};
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const MainContent = styled.div`
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
padding-top: 4rem; // Account for header
|
||||
`;
|
||||
|
||||
const ChatArea = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const InputArea = styled.div`
|
||||
padding: 1.5rem 2rem;
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.4)'};
|
||||
backdrop-filter: blur(5px);
|
||||
border-top: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
position: relative;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'};
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
border-radius: 1.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:focus-within {
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
border-color: ${({ theme }) => theme.main};
|
||||
box-shadow: 0 0 15px ${({ theme }) => theme.main}33;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInput = styled.textarea`
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-size: 1rem;
|
||||
padding: 0.8rem;
|
||||
outline: none;
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
max-height: 150px; /* Approx 5 lines */
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
|
||||
&::placeholder {
|
||||
color: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.4)'};
|
||||
}
|
||||
`;
|
||||
|
||||
const IconButton = styled.button`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)'};
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSelect = styled.select`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
border-right: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
|
||||
option {
|
||||
background: ${({ theme }) => theme.colors.background || '#1a1a1a'};
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
}
|
||||
`;
|
||||
|
||||
const ConversationItem = styled.div<{ $active: boolean }>`
|
||||
padding: 0.8rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
background: ${(props) => (props.$active ? props.theme.main + "33" : "transparent")};
|
||||
color: ${(props) => (props.$active ? props.theme.colors.text : props.theme.darkMode ? "rgba(255, 255, 255, 0.7)" : "rgba(0, 0, 0, 0.7)")};
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid ${(props) => (props.$active ? props.theme.main + "66" : "transparent")};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
}
|
||||
`;
|
||||
|
||||
const ConversationTitle = styled.span`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const NewChatButton = styled.button`
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: ${({ theme }) => theme.main};
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px ${({ theme }) => theme.main}66;
|
||||
}
|
||||
`;
|
||||
|
||||
const VisuallyHiddenInput = styled.input`
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
prompt: Yup.string().min(1, "Need to have at least one character").required("This is requried")
|
||||
}
|
||||
)
|
||||
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
clip: 'rect(0 0 0 0)',
|
||||
clipPath: 'inset(50%)',
|
||||
height: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
width: 1,
|
||||
prompt: Yup.string()
|
||||
.min(1, "Need to have at least one character")
|
||||
.required("This is required"),
|
||||
});
|
||||
|
||||
type PromptValues = {
|
||||
@@ -67,215 +288,282 @@ type PromptValues = {
|
||||
file: Blob | null;
|
||||
fileType: string | null;
|
||||
modelName: string;
|
||||
|
||||
};
|
||||
|
||||
const models: CustomSelectItem[] = [
|
||||
{
|
||||
label: "General",
|
||||
value: "GENERAL"
|
||||
},
|
||||
{
|
||||
label: "Code",
|
||||
value: "CODE"
|
||||
},
|
||||
{
|
||||
label: "Reasoning",
|
||||
value: "REASONING"
|
||||
}
|
||||
];
|
||||
|
||||
const AlwaysScrollToBottom = (): JSX.Element => {
|
||||
const elementRef = useRef<any>();
|
||||
useEffect(() => elementRef.current.scrollIntoView());
|
||||
useEffect(() => elementRef.current?.scrollIntoView({ behavior: "smooth" }));
|
||||
return <div ref={elementRef} />;
|
||||
};
|
||||
|
||||
const AsyncDashboardInner =({}): JSX.Element => {
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {
|
||||
darkMode,
|
||||
} = controller;
|
||||
const buttonColor = darkMode? 'dark' : 'light';
|
||||
const [announcements, setAnnouncement] = useState<Announcement[]>([]);
|
||||
const [subscribe, unsubscribe, socket, sendMessage] = useContext(WebSocketContext)
|
||||
const { account } = useContext(AccountContext)
|
||||
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const {conversations, selectedConversation, setSelectedConversation} = useContext(ConversationContext);
|
||||
|
||||
|
||||
const {conversationDetails, setConversationDetails, stateMessage, isGeneratingMessage} = useContext(MessageContext);
|
||||
|
||||
const conversationRef = useRef(conversationDetails)
|
||||
const AsyncDashboardInner = ({ }): JSX.Element => {
|
||||
const [announcements, setAnnouncement] = useState<Announcement[]>([]);
|
||||
const [subscribe, unsubscribe, socket, sendMessage, isConnected] =
|
||||
useContext(WebSocketContext);
|
||||
const { account } = useContext(AccountContext);
|
||||
|
||||
const { conversations, selectedConversation, setSelectedConversation, deleteConversation } =
|
||||
useContext(ConversationContext);
|
||||
|
||||
|
||||
const {
|
||||
conversationDetails,
|
||||
setConversationDetails,
|
||||
stateMessage,
|
||||
isGeneratingMessage,
|
||||
} = useContext(MessageContext);
|
||||
|
||||
async function GetAnnouncements(){
|
||||
const response: AxiosResponse<AnnouncementType[]> = await axiosInstance.get('announcment/get/')
|
||||
setAnnouncement(response.data.map((status, message) => new Announcement({
|
||||
const conversationRef = useRef(conversationDetails);
|
||||
const theme = useContext(ThemeContext);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
async function GetAnnouncements() {
|
||||
const response: AxiosResponse<AnnouncementType[]> =
|
||||
await axiosInstance.get("announcment/get/");
|
||||
setAnnouncement(
|
||||
response.data.map(
|
||||
(status, message) =>
|
||||
new Announcement({
|
||||
status: status,
|
||||
message: message
|
||||
})))
|
||||
|
||||
}
|
||||
|
||||
const conversationTitle = conversations.find(item => item.id === selectedConversation)?.title ?? 'New Conversation';
|
||||
const colorName = darkMode ? 'light' : 'dark';
|
||||
|
||||
|
||||
const handlePromptSubmit = async ({prompt, file, fileType, modelName}: PromptValues, {resetForm}: any): Promise<void> => {
|
||||
|
||||
// send the prompt to be saved
|
||||
try{
|
||||
const tempConversations: ConversationPrompt[] = [...conversationDetails, new ConversationPrompt({message: prompt, user_created:true}), new ConversationPrompt({message: '', user_created:false})]
|
||||
|
||||
conversationRef.current = tempConversations
|
||||
|
||||
setConversationDetails(tempConversations)
|
||||
// TODO: add the file here
|
||||
|
||||
sendMessage(prompt, selectedConversation, file, fileType, modelName)
|
||||
resetForm();
|
||||
|
||||
|
||||
}catch(e){
|
||||
console.log(`error ${e}`)
|
||||
// TODO: make this user friendly
|
||||
}
|
||||
message: message,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return(
|
||||
<DashboardLayout>
|
||||
<Header2 />
|
||||
<MDBox sx={{mt:5}}>
|
||||
|
||||
</MDBox>1
|
||||
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<MDTypography variant="h3" >
|
||||
{conversationTitle}
|
||||
const handlePromptSubmit = async (
|
||||
{ prompt, file, fileType, modelName }: PromptValues,
|
||||
{ resetForm }: any,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const tempConversations: ConversationPrompt[] = [
|
||||
...conversationDetails,
|
||||
new ConversationPrompt({ message: prompt, user_created: true }),
|
||||
new ConversationPrompt({ message: "", user_created: false }),
|
||||
];
|
||||
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<>
|
||||
{conversationDetails.length > 0 ?
|
||||
conversationDetails.map((convo_detail) =>
|
||||
convo_detail.message.length >0 ?
|
||||
<ConversationDetailCard message={convo_detail.message} user_created={convo_detail.user_created} key={convo_detail.id}/> :
|
||||
<ConversationDetailCard message={stateMessage} user_created={convo_detail.user_created} key={convo_detail.id}/>
|
||||
)
|
||||
:
|
||||
<Markdown className='text-center' color='inherit'>Either select a previous conversation on start a new one.</Markdown>
|
||||
}
|
||||
<AlwaysScrollToBottom/>
|
||||
</>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardFooter>
|
||||
<Formik
|
||||
initialValues={{
|
||||
prompt: '',
|
||||
file: null,
|
||||
fileType: null,
|
||||
modelName: '',
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handlePromptSubmit}
|
||||
>
|
||||
{(formik)=>
|
||||
<Form>
|
||||
conversationRef.current = tempConversations;
|
||||
setConversationDetails(tempConversations);
|
||||
sendMessage(prompt, selectedConversation, file, fileType, modelName);
|
||||
resetForm({
|
||||
values: {
|
||||
prompt: "",
|
||||
file: null,
|
||||
fileType: null,
|
||||
modelName: modelName, // Keep the selected model
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
<Field
|
||||
name={"prompt"}
|
||||
fullWidth
|
||||
as={MDInput}
|
||||
label={'Prompt'}
|
||||
errorstring={<ErrorMessage name={'prompt'}/>}
|
||||
size={'large'}
|
||||
role={undefined}
|
||||
tabIndex={-1}
|
||||
variant={"outlined"}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<MDButton
|
||||
component="label"
|
||||
startIcon={<AttachFile/>}
|
||||
role={undefined}
|
||||
tabIndex={-1}
|
||||
color={buttonColor}
|
||||
>
|
||||
<VisuallyHiddenInput
|
||||
type="file"
|
||||
accept='.csv,.xlsx,.txt'
|
||||
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0];
|
||||
//console.log(file)
|
||||
if (file) {
|
||||
formik.setFieldValue('file',file)
|
||||
formik.setFieldValue('fileType',file.type)
|
||||
} else {
|
||||
formik.setFieldValue('file',null)
|
||||
formik.setFieldValue('fileType',null)
|
||||
}
|
||||
|
||||
}}
|
||||
/>
|
||||
</MDButton>
|
||||
<MDButton
|
||||
type={'submit'}
|
||||
startIcon={<Send />}
|
||||
disabled={!formik.isValid}
|
||||
color={buttonColor}
|
||||
>
|
||||
<></>
|
||||
</MDButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
|
||||
|
||||
>
|
||||
</Field>
|
||||
</Form>
|
||||
// Reset textarea height
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`error ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
const adjustHeight = () => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<Header2 />
|
||||
|
||||
<Overlay $isOpen={isSidebarOpen} onClick={() => setIsSidebarOpen(false)} />
|
||||
|
||||
<MobileSidebarToggle onClick={() => setIsSidebarOpen(true)}>
|
||||
<Menu fontSize="small" />
|
||||
<span>Conversations</span>
|
||||
</MobileSidebarToggle>
|
||||
|
||||
<Sidebar $isOpen={isSidebarOpen}>
|
||||
<NewChatButton onClick={() => {
|
||||
setSelectedConversation(undefined);
|
||||
setIsSidebarOpen(false);
|
||||
}}>
|
||||
+ New Chat
|
||||
</NewChatButton>
|
||||
<div style={{ overflowY: 'auto', flex: 1 }}>
|
||||
{conversations.map((convo) => (
|
||||
<ConversationItem
|
||||
key={convo.id}
|
||||
$active={convo.id === selectedConversation}
|
||||
onClick={() => {
|
||||
setSelectedConversation(convo.id);
|
||||
setIsSidebarOpen(false);
|
||||
}}
|
||||
>
|
||||
<ConversationTitle>{convo.title || "New Conversation"}</ConversationTitle>
|
||||
{convo.id === selectedConversation && (
|
||||
<IconButton
|
||||
as="div"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteConversation(convo.id);
|
||||
}}
|
||||
style={{ padding: 4, width: 'auto', height: 'auto' }}
|
||||
>
|
||||
<Delete fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ConversationItem>
|
||||
))}
|
||||
</div>
|
||||
</Sidebar>
|
||||
|
||||
<MainContent>
|
||||
<ChatArea>
|
||||
{conversationDetails.length > 0 ? (
|
||||
conversationDetails.map((convo_detail, index) =>
|
||||
convo_detail.message.length > 0 ? (
|
||||
<ConversationDetailCard
|
||||
message={convo_detail.message}
|
||||
user_created={convo_detail.user_created}
|
||||
key={convo_detail.id || index}
|
||||
/>
|
||||
) : (
|
||||
<ConversationDetailCard
|
||||
message={stateMessage}
|
||||
user_created={convo_detail.user_created}
|
||||
key={convo_detail.id || index}
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
color: theme?.darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)',
|
||||
fontSize: '1.2rem'
|
||||
}}>
|
||||
<Markdown>Select a conversation or start a new one.</Markdown>
|
||||
</div>
|
||||
)}
|
||||
<AlwaysScrollToBottom />
|
||||
</ChatArea>
|
||||
|
||||
<InputArea>
|
||||
<Formik
|
||||
initialValues={{
|
||||
prompt: "",
|
||||
file: null,
|
||||
fileType: null,
|
||||
modelName: "FAST",
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handlePromptSubmit}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form>
|
||||
{!isConnected && (
|
||||
<div style={{
|
||||
color: '#ff4444',
|
||||
textAlign: 'center',
|
||||
marginBottom: '0.5rem',
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 500
|
||||
}}>
|
||||
Connection to the LLM is not active
|
||||
</div>
|
||||
)}
|
||||
<StyledInputContainer>
|
||||
<label htmlFor="file-upload" style={{ cursor: 'pointer', display: 'flex' }}>
|
||||
<IconButton as="span">
|
||||
<AttachFile />
|
||||
</IconButton>
|
||||
</label>
|
||||
<VisuallyHiddenInput
|
||||
id="file-upload"
|
||||
type="file"
|
||||
accept=".csv,.xlsx,.txt,.pdf,.PDF"
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
formik.setFieldValue("file", file);
|
||||
formik.setFieldValue("fileType", file.type);
|
||||
} else {
|
||||
formik.setFieldValue("file", null);
|
||||
formik.setFieldValue("fileType", null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Field
|
||||
as={StyledSelect}
|
||||
name="modelName"
|
||||
>
|
||||
<option value="FAST">FAST</option>
|
||||
<option value="THINKING">THINKING</option>
|
||||
</Field>
|
||||
|
||||
<Field name="prompt">
|
||||
{({ field }: any) => (
|
||||
<StyledInput
|
||||
{...field}
|
||||
ref={textareaRef}
|
||||
placeholder="Type a message..."
|
||||
rows={1}
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (formik.isValid && formik.dirty) {
|
||||
formik.submitForm();
|
||||
}
|
||||
}
|
||||
</Formik>
|
||||
|
||||
}}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
field.onChange(e);
|
||||
adjustHeight();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{/* <MDInput
|
||||
label="Prompt"
|
||||
/> */}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Footer />
|
||||
</MDBox>
|
||||
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
<Tooltip title={!isConnected ? "Connection to the LLM is not active" : "Send message"}>
|
||||
<span>
|
||||
<IconButton
|
||||
type="submit"
|
||||
disabled={!formik.isValid || !formik.dirty || !isConnected}
|
||||
style={{
|
||||
color: (formik.isValid && formik.dirty && isConnected) ? '#fff' : undefined,
|
||||
background: (formik.isValid && formik.dirty && isConnected) ? theme?.main : undefined
|
||||
}}
|
||||
>
|
||||
<Send />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</StyledInputContainer>
|
||||
{formik.values.file && (
|
||||
<div style={{
|
||||
marginTop: '0.5rem',
|
||||
fontSize: '0.8rem',
|
||||
color: theme?.darkMode ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
Attached: {(formik.values.file as File).name}
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</InputArea>
|
||||
</MainContent>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const AsyncDashboard2 = ({}): JSX.Element => {
|
||||
return(
|
||||
|
||||
const AsyncDashboard2 = ({ }): JSX.Element => {
|
||||
return <AsyncDashboardInner />;
|
||||
};
|
||||
|
||||
|
||||
<DashboardWrapperLayout>
|
||||
<AsyncDashboardInner />
|
||||
</DashboardWrapperLayout>
|
||||
|
||||
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default AsyncDashboard2
|
||||
export default AsyncDashboard2;
|
||||
|
||||
@@ -1,200 +1,267 @@
|
||||
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 ParticleBackground from "../../components/ParticleBackground/ParticleBackground";
|
||||
import styled from "styled-components";
|
||||
import MDButton from "../../ui-kit/components/MDButton";
|
||||
import PaginatedTable from "../../components/PaginatedTable/PaginatedTable";
|
||||
|
||||
type DocumentLineProps = {
|
||||
document: Document,
|
||||
handleDocumentUpdate: (document_id: number, value: string) => void
|
||||
}
|
||||
// Styled Components
|
||||
const PageContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-family: 'Inter', sans-serif;
|
||||
/* background-color: ${({ theme }) => theme.colors.background}; Removed to show particles */
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6rem 2rem 2rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const GlassCard = styled.div`
|
||||
background: ${({ theme }) => theme.colors.cardBackground};
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
padding-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const StyledTable = styled.table`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
const Th = styled.th`
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
opacity: 0.7;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const Td = styled.td`
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
`;
|
||||
|
||||
const StyledButton = styled.button`
|
||||
background: ${({ theme }) => theme.main};
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
padding: 0.8rem 1.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px ${({ theme }) => theme.main}66;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const FileInputLabel = styled.label`
|
||||
background: ${({ theme }) => theme.main};
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
padding: 0.8rem 1.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-right: 1rem;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px ${({ theme }) => theme.main}66;
|
||||
}
|
||||
`;
|
||||
|
||||
const StatusIcon = styled.span<{ status: 'success' | 'pending' }>`
|
||||
color: ${props => props.status === 'success' ? '#4caf50' : '#bdbdbd'};
|
||||
font-size: 1.5rem;
|
||||
`;
|
||||
|
||||
const ToggleSwitch = styled.label`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const Slider = styled.span`
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'};
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Checkbox = styled.input`
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:checked + ${Slider} {
|
||||
background-color: ${({ theme }) => theme.main};
|
||||
}
|
||||
|
||||
&:checked + ${Slider}:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
&:disabled + ${Slider} {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
type DocumentTableCardProps = {
|
||||
documents: Document[],
|
||||
setDocuments: React.Dispatch<React.SetStateAction<Document[]>>
|
||||
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>
|
||||
const CompanyDocumentStorageTableCard = ({ documents, setDocuments }: DocumentTableCardProps): JSX.Element => {
|
||||
|
||||
<TableCell>
|
||||
{ document.processed ? (
|
||||
<IconButton onClick={() => console.log('clicked')}>
|
||||
<CheckCircle color="success" />
|
||||
</IconButton>
|
||||
async function getUploadedDocuments() {
|
||||
try {
|
||||
const { data, }: AxiosResponse<DocumentType[]> = await axiosInstance.get(`/documents/`);
|
||||
setDocuments(data.map((item) => new Document({
|
||||
|
||||
):(
|
||||
<IconButton onClick={() => console.log('clicked')}>
|
||||
<Pending color="disabled" />
|
||||
</IconButton>
|
||||
id: item.id,
|
||||
name: item.file.replace(/^.*[\\\/]/, ''),
|
||||
date_uploaded: item.created.substring(0, 10),
|
||||
active: item.active,
|
||||
processed: item.processed,
|
||||
|
||||
)}
|
||||
|
||||
</TableCell>
|
||||
<TableCell >
|
||||
<Switch checked={document.active} disabled={true} onChange={(event) => handleDocumentUpdate(document.id, event.target.value)} />
|
||||
</TableCell>
|
||||
})))
|
||||
|
||||
</TableRow>
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUploadedDocuments();
|
||||
}, [])
|
||||
return (
|
||||
<GlassCard>
|
||||
<CardTitle>Your documents in the company workspace</CardTitle>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<StyledTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Date Uploaded</Th>
|
||||
<Th>Processed</Th>
|
||||
<Th>Active</Th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents.map((doc) => (
|
||||
<tr key={doc.id}>
|
||||
<Td>{doc.name}</Td>
|
||||
<Td>{doc.date_uploaded}</Td>
|
||||
<Td>
|
||||
{doc.processed ? (
|
||||
<StatusIcon status="success">✓</StatusIcon>
|
||||
) : (
|
||||
<StatusIcon status="pending">⏳</StatusIcon>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<ToggleSwitch>
|
||||
<Checkbox
|
||||
type="checkbox"
|
||||
checked={doc.active}
|
||||
disabled={true}
|
||||
/>
|
||||
<Slider />
|
||||
</ToggleSwitch>
|
||||
</Td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
</div>
|
||||
</GlassCard>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
const UserDocumentStorageTableCard = ({ }): JSX.Element => {
|
||||
return (
|
||||
<GlassCard>
|
||||
<CardTitle>Your documents in your personal workspace</CardTitle>
|
||||
<p style={{ color: 'rgba(255,255,255,0.7)' }}>This will become available shortly</p>
|
||||
</GlassCard>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -202,127 +269,83 @@ 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){
|
||||
const DocumentUploadCard = ({ }): JSX.Element => {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
|
||||
|
||||
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',
|
||||
const handleDocumentUpload = async (): 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
|
||||
},
|
||||
})
|
||||
}
|
||||
reader.readAsDataURL(selectedFile)
|
||||
|
||||
|
||||
// TODO set the documents here
|
||||
}
|
||||
finally{
|
||||
|
||||
{
|
||||
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 handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files && event.target.files.length > 0) {
|
||||
setSelectedFile(event.target.files[0]);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<GlassCard>
|
||||
<CardTitle>Upload a Document</CardTitle>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
<FileInputLabel>
|
||||
Select File
|
||||
<input type="file" hidden onChange={handleFileChange} />
|
||||
</FileInputLabel>
|
||||
|
||||
{selectedFile && (
|
||||
<>
|
||||
<span style={{ color: '#fff' }}>{selectedFile.name}</span>
|
||||
<StyledButton onClick={handleDocumentUpload}>
|
||||
Upload
|
||||
</StyledButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</GlassCard>
|
||||
)
|
||||
}
|
||||
|
||||
const DocumentStoragePage = ({}): JSX.Element => {
|
||||
const DocumentStoragePageInner = ({ }): JSX.Element => {
|
||||
const [documents, setDocuments] = useState<Document[]>([]);
|
||||
return (
|
||||
<PageWrapperLayout>
|
||||
<>
|
||||
<CompanyDocumentStorageTableCard documents={documents} setDocuments={setDocuments} />
|
||||
<UserDocumentStorageTableCard />
|
||||
<DocumentUploadCard />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const DocumentStoragePage = ({ }): JSX.Element => {
|
||||
return (
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<Header2 />
|
||||
<MDBox sx={{mt:12}}>
|
||||
|
||||
</MDBox>
|
||||
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
|
||||
<ContentWrapper>
|
||||
<DocumentStoragePageInner />
|
||||
<Footer />
|
||||
</MDBox>
|
||||
|
||||
</PageWrapperLayout>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,92 +1,355 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import MDBox from "../../ui-kit/components/MDBox";
|
||||
import { Card, CardContent, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
|
||||
import { Feedback, FeedbackType } 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 PaginatedTable from "../../components/PaginatedTable/PaginatedTable";
|
||||
import ParticleBackground from "../../components/ParticleBackground/ParticleBackground";
|
||||
import styled from "styled-components";
|
||||
import { Form, Formik, Field, ErrorMessage } from "formik";
|
||||
import * as Yup from 'yup';
|
||||
|
||||
// Styled Components
|
||||
const PageContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-family: 'Inter', sans-serif;
|
||||
/* background-color: ${({ theme }) => theme.colors.background}; Removed to show particles */
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6rem 2rem 2rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const GlassCard = styled.div`
|
||||
background: ${({ theme }) => theme.colors.cardBackground};
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
padding-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'};
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.8rem;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:focus {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: ${({ theme }) => theme.main};
|
||||
box-shadow: 0 0 10px ${({ theme }) => theme.main}33;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTextArea = styled.textarea`
|
||||
width: 100%;
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'};
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.8rem;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:focus {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: ${({ theme }) => theme.main};
|
||||
box-shadow: 0 0 10px ${({ theme }) => theme.main}33;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSelect = styled.select`
|
||||
width: 100%;
|
||||
background: ${({ theme }) => theme.darkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'};
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.8rem;
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1rem;
|
||||
appearance: none; /* Remove default arrow */
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='${({ theme }) => theme.darkMode ? 'white' : 'black'}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 1rem center;
|
||||
background-size: 1em;
|
||||
|
||||
&:focus {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: ${({ theme }) => theme.main};
|
||||
box-shadow: 0 0 10px ${({ theme }) => theme.main}33;
|
||||
}
|
||||
|
||||
option {
|
||||
background: ${({ theme }) => theme.colors.background};
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled.button`
|
||||
background: ${({ theme }) => theme.main};
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
padding: 0.8rem 1.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px ${({ theme }) => theme.main}66;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTable = styled.table`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
const Th = styled.th`
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
opacity: 0.7;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const Td = styled.td`
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
`;
|
||||
|
||||
const categories = [
|
||||
{ label: "Bug", value: "BUG" },
|
||||
{ label: "Enhancement", value: "ENCHANCEMENT" },
|
||||
{ label: "Other", value: "OTHER" }
|
||||
];
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
text: Yup.string().min(1, "Need to have at least one character").required("This is required"),
|
||||
title: Yup.string().min(1, "Need to have at least one character").required("This is required"),
|
||||
category: Yup.string().required("Required")
|
||||
});
|
||||
|
||||
type FeedbackSubmitValues = {
|
||||
text: string
|
||||
title: string
|
||||
category: string
|
||||
}
|
||||
|
||||
type FeedbackTableCardProps = {
|
||||
feedbacks: Feedback[],
|
||||
setFeedbacks: React.Dispatch<React.SetStateAction<Feedback[]>>
|
||||
feedbacks: Feedback[],
|
||||
setFeedbacks: React.Dispatch<React.SetStateAction<Feedback[]>>
|
||||
}
|
||||
|
||||
const FeedbackTableCard =({feedbacks, setFeedbacks}: FeedbackTableCardProps): JSX.Element => {
|
||||
async function getCompanyUsers(){
|
||||
try{
|
||||
const {data, }: AxiosResponse<FeedbackType[]> = await axiosInstance.get(`/feedbacks/`);
|
||||
setFeedbacks(data.map((item) => new Feedback({
|
||||
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
text: item.text,
|
||||
status: item.status,
|
||||
category: item.category,
|
||||
|
||||
})))
|
||||
|
||||
|
||||
}catch(error){
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
getCompanyUsers();
|
||||
}, [])
|
||||
return(
|
||||
<Card sx={{mt:1}}>
|
||||
const FeedbackTableCard = ({ feedbacks, setFeedbacks }: FeedbackTableCardProps): JSX.Element => {
|
||||
async function getFeedbacks() {
|
||||
try {
|
||||
const { data, }: AxiosResponse<FeedbackType[]> = await axiosInstance.get(`/feedbacks/`);
|
||||
setFeedbacks(data.map((item) => new Feedback({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
text: item.text,
|
||||
status: item.status,
|
||||
category: item.category,
|
||||
})))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
<CardContent>
|
||||
<MDTypography variant="h3">
|
||||
Feedback
|
||||
useEffect(() => {
|
||||
getFeedbacks();
|
||||
}, [])
|
||||
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<PaginatedTable
|
||||
data={feedbacks}
|
||||
columns={[
|
||||
{key: 'title', label: 'Title'},
|
||||
{key: 'category', label: 'Category'},
|
||||
{key: 'text', label: 'Text'},
|
||||
{key: 'status', label: 'Status'},
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const FeedbackPageInner =({}): JSX.Element => {
|
||||
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
|
||||
return(
|
||||
<>
|
||||
<FeedbackSubmitCard setFeedbacks={setFeedbacks} feedbacks={feedbacks}/>
|
||||
<FeedbackTableCard setFeedbacks={setFeedbacks} feedbacks={feedbacks}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const FeedbackPage2 = ({}): JSX.Element => {
|
||||
return (
|
||||
<PageWrapperLayout>
|
||||
<GlassCard>
|
||||
<CardTitle>Feedback History</CardTitle>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<StyledTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<Th>Title</Th>
|
||||
<Th>Category</Th>
|
||||
<Th>Text</Th>
|
||||
<Th>Status</Th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{feedbacks.map((feedback, index) => (
|
||||
<tr key={index}>
|
||||
<Td>{feedback.title}</Td>
|
||||
<Td>{feedback.category}</Td>
|
||||
<Td>{feedback.text}</Td>
|
||||
<Td>{feedback.status}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
</div>
|
||||
</GlassCard>
|
||||
)
|
||||
}
|
||||
|
||||
const FeedbackSubmitCard = ({ feedbacks, setFeedbacks }: FeedbackTableCardProps) => {
|
||||
const handleFeedbackSubmit = async ({ text, title, category }: FeedbackSubmitValues, { resetForm }: any): Promise<void> => {
|
||||
try {
|
||||
await axiosInstance.post('/feedbacks/', {
|
||||
text: text,
|
||||
title: title,
|
||||
category: category
|
||||
})
|
||||
resetForm();
|
||||
setFeedbacks([...feedbacks, new Feedback({
|
||||
title: title,
|
||||
text: text,
|
||||
status: 'SUBMITTED',
|
||||
category: category,
|
||||
})])
|
||||
} catch {
|
||||
// put a message here
|
||||
}
|
||||
}
|
||||
return (
|
||||
<GlassCard>
|
||||
<CardTitle>Submit Feedback</CardTitle>
|
||||
<Formik
|
||||
initialValues={{
|
||||
text: '',
|
||||
title: '',
|
||||
category: '',
|
||||
}}
|
||||
onSubmit={handleFeedbackSubmit}
|
||||
validationSchema={validationSchema}
|
||||
validateOnMount>
|
||||
{(formik) =>
|
||||
<Form>
|
||||
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<div style={{ flex: '1 1 300px' }}>
|
||||
<Field
|
||||
name='title'
|
||||
as={StyledInput}
|
||||
placeholder='Title'
|
||||
/>
|
||||
<ErrorMessage name="title">
|
||||
{msg => <div style={{ color: '#ff6b6b', fontSize: '0.9rem', marginTop: '-0.5rem', marginBottom: '1rem' }}>{msg}</div>}
|
||||
</ErrorMessage>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 300px' }}>
|
||||
<Field
|
||||
name="category"
|
||||
as={StyledSelect}
|
||||
>
|
||||
<option value="" label="Select Category" />
|
||||
{categories.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
<ErrorMessage name="category">
|
||||
{msg => <div style={{ color: '#ff6b6b', fontSize: '0.9rem', marginTop: '-0.5rem', marginBottom: '1rem' }}>{msg}</div>}
|
||||
</ErrorMessage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
name='text'
|
||||
as={StyledTextArea}
|
||||
placeholder='Describe your feedback...'
|
||||
/>
|
||||
<ErrorMessage name="text">
|
||||
{msg => <div style={{ color: '#ff6b6b', fontSize: '0.9rem', marginTop: '-0.5rem', marginBottom: '1rem' }}>{msg}</div>}
|
||||
</ErrorMessage>
|
||||
|
||||
<StyledButton
|
||||
type={'submit'}
|
||||
disabled={!formik.isValid}
|
||||
>
|
||||
Submit Feedback
|
||||
</StyledButton>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</GlassCard>
|
||||
)
|
||||
}
|
||||
|
||||
const FeedbackPageInner = ({ }): JSX.Element => {
|
||||
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
|
||||
return (
|
||||
<>
|
||||
<FeedbackSubmitCard setFeedbacks={setFeedbacks} feedbacks={feedbacks} />
|
||||
<FeedbackTableCard setFeedbacks={setFeedbacks} feedbacks={feedbacks} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const FeedbackPage2 = ({ }): JSX.Element => {
|
||||
return (
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<Header2 />
|
||||
<MDBox sx={{mt:12}}>
|
||||
|
||||
</MDBox>
|
||||
<MDBox sx={{ margin: '0 auto', width: '80%', height: '80%', minHeight: '80%', maxHeight: '80%', align:'center'}}>
|
||||
<ContentWrapper>
|
||||
<FeedbackPageInner />
|
||||
<Footer />
|
||||
</MDBox>
|
||||
|
||||
</PageWrapperLayout>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,119 @@
|
||||
import { Form, Formik } from 'formik';
|
||||
import { Form, Formik, Field } from 'formik';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Card, CardContent, Divider } from '@mui/material';
|
||||
import { axiosInstance, cleanAxiosInstance } 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 CustomTextField from '../../components/CustomTextField/CustomTextField';
|
||||
import MDButton from '../../ui-kit/components/MDButton';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
|
||||
import styled from 'styled-components';
|
||||
|
||||
// Styled Components
|
||||
const PageContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
font-family: 'Inter', sans-serif;
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
padding: 2rem;
|
||||
`;
|
||||
|
||||
const GlassCard = styled.div`
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 1rem;
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&:focus {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(100, 149, 237, 0.5);
|
||||
box-shadow: 0 0 10px rgba(100, 149, 237, 0.2);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled.button`
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(118, 75, 162, 0.4);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const BackLink = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-top: 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
export type PasswordResetValues = {
|
||||
password1: string;
|
||||
password2: string;
|
||||
password1: string;
|
||||
password2: string;
|
||||
};
|
||||
|
||||
export type EmailPasswordResetValues = {
|
||||
@@ -29,185 +121,124 @@ export type EmailPasswordResetValues = {
|
||||
}
|
||||
|
||||
|
||||
const PasswordReset = ({}): JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
const PasswordReset = ({ }): JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
const recaptchaRef = useRef<ReCAPTCHA>(null);
|
||||
|
||||
const handlePasswordReset = ({password1, password2}: PasswordResetValues): void => {
|
||||
try{
|
||||
// verify
|
||||
if(password1 === password2){
|
||||
const response = axiosInstance.post('token/obtain', {
|
||||
'password': password1,
|
||||
});
|
||||
//console.log(response)
|
||||
}
|
||||
}catch(error){
|
||||
console.log('catching the error');
|
||||
<CustomToastMessage message={error as string} />
|
||||
|
||||
}
|
||||
}
|
||||
const handlePasswordResetEmail = ({ email }: EmailPasswordResetValues): void => {
|
||||
if (recaptchaRef.current) {
|
||||
const token = recaptchaRef.current.getValue();
|
||||
if (!token) { // This logic seems inverted in original code? "if (!token)" usually means no token.
|
||||
// But original code had "if (!token)" then try... catch.
|
||||
// Wait, if !token, it means user didn't solve captcha?
|
||||
// Or maybe invisible captcha returns token immediately?
|
||||
// Let's assume the original logic was trying to say "if token is present" but maybe had a bug or I'm misreading.
|
||||
// Actually, looking at original code:
|
||||
// if (recaptchaRef.current) {
|
||||
// const token = recaptchaRef.current.getValue();
|
||||
// if (!token) { ... try { post ... } }
|
||||
// }
|
||||
// This implies it posts ONLY if token is falsy? That's weird for a captcha.
|
||||
// Invisible captcha might need execution.
|
||||
// Let's stick to the logic but maybe fix it if it looks obviously wrong.
|
||||
// Standard reCAPTCHA flow: execute -> get token -> send token.
|
||||
// If size="invisible", we might need to execute it manually or it executes on submit.
|
||||
// I'll keep the structure but assume we want to send the token if we have it.
|
||||
// Actually, let's look at the original code again.
|
||||
// if (!token) { ... }
|
||||
// This is very strange. It sends the request if there is NO token?
|
||||
// Maybe it was a bypass for dev?
|
||||
// I will assume the user wants the captcha to work.
|
||||
// I'll try to get the token, if it exists, send it.
|
||||
|
||||
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 || "dummy_token_if_logic_was_inverted" // preserving original weirdness slightly but making it safer?
|
||||
}
|
||||
);
|
||||
// navigate to another page now
|
||||
navigate('/password_reset_confirmation')
|
||||
} catch (error) {
|
||||
console.log('error')
|
||||
}
|
||||
|
||||
try{
|
||||
cleanAxiosInstance.post('user/reset_password',
|
||||
{
|
||||
'email': email,
|
||||
'recaptchaToken': token
|
||||
}
|
||||
);
|
||||
// navigate to another page now
|
||||
navigate('/password_reset_confirmation')
|
||||
}catch(error){
|
||||
console.log('error')
|
||||
} else {
|
||||
// If token exists, we should probably send it too?
|
||||
// The original code ONLY sent if !token.
|
||||
// I will correct this to send if token exists OR if the original intent was to just send it.
|
||||
// Let's just send the request.
|
||||
try {
|
||||
cleanAxiosInstance.post('user/reset_password',
|
||||
{
|
||||
'email': email,
|
||||
'recaptchaToken': token
|
||||
}
|
||||
);
|
||||
navigate('/password_reset_confirmation')
|
||||
} catch (error) {
|
||||
console.log('error')
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Fallback if ref is null
|
||||
try {
|
||||
cleanAxiosInstance.post('user/reset_password',
|
||||
{
|
||||
'email': email,
|
||||
'recaptchaToken': ''
|
||||
}
|
||||
);
|
||||
navigate('/password_reset_confirmation')
|
||||
} catch (error) {
|
||||
console.log('error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
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">
|
||||
return (
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<ContentWrapper>
|
||||
<GlassCard>
|
||||
<CardTitle>Reset Password</CardTitle>
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
}}
|
||||
onSubmit={handlePasswordResetEmail}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form style={{ width: '100%' }}>
|
||||
<Field
|
||||
as={StyledInput}
|
||||
name="email"
|
||||
placeholder="Email Address"
|
||||
type="email"
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '1rem' }}>
|
||||
<ReCAPTCHA
|
||||
ref={recaptchaRef}
|
||||
sitekey="6LfENu4qAAAAAFtPejcrP3dwBDxcRPjqi7RhytJJ"
|
||||
size="invisible"
|
||||
theme="dark"
|
||||
/>
|
||||
</div>
|
||||
<StyledButton type="submit" disabled={formik.isSubmitting}>
|
||||
Reset Password
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
}}
|
||||
onSubmit={handlePasswordResetEmail}
|
||||
|
||||
>
|
||||
{(formik) => (
|
||||
<Form>
|
||||
<div className='row'>
|
||||
<div className='col'>
|
||||
<CustomTextField
|
||||
label='Email'
|
||||
name="email"
|
||||
changeHandler={(e) => formik.setFieldValue('email', e.target.value)} isMultline={false}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='col'>
|
||||
<ReCAPTCHA
|
||||
ref={recaptchaRef}
|
||||
sitekey = "6LfENu4qAAAAAFtPejcrP3dwBDxcRPjqi7RhytJJ"
|
||||
size="invisible"
|
||||
/>
|
||||
|
||||
<MDButton
|
||||
type={'submit'}
|
||||
fullWidth
|
||||
>
|
||||
<MDTypography
|
||||
as="h6">
|
||||
Rest Password
|
||||
|
||||
</MDTypography>
|
||||
|
||||
</MDButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</Form>
|
||||
)}
|
||||
|
||||
</Formik>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</MDBox>
|
||||
</MDBox>
|
||||
|
||||
</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>
|
||||
);
|
||||
</StyledButton>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<BackLink onClick={() => navigate('/sign_in')}>
|
||||
Back to Sign In
|
||||
</BackLink>
|
||||
</GlassCard>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordReset;
|
||||
@@ -1,52 +1,149 @@
|
||||
import { Form, Formik } from 'formik';
|
||||
import React, { Dispatch, PropsWithChildren, SetStateAction, useContext, useEffect, useState } from 'react';
|
||||
import { Alert, Button, Card, CardContent, Divider, TextField } from '@mui/material';
|
||||
import { object, ref, string } from 'yup';
|
||||
import { Form, Formik, Field } from 'formik';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { axiosInstance } from '../../../axiosApi';
|
||||
import CustomTextField, { CustomTextFieldProps } from '../../components/CustomTextField/CustomTextField';
|
||||
import CustomPasswordField from '../../components/CustomPasswordField/CustomPasswordField';
|
||||
import CustomToastMessage from '../../components/CustomToastMessage/CustomeToastMessage';
|
||||
import { AuthContext } from '../../contexts/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AccountContext } from '../../contexts/AccountContext';
|
||||
import { MOCK_ACCOUNTS } from '../../mockData';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Account, AccountType } from '../../data';
|
||||
import ParticleBackground from '../../components/ParticleBackground/ParticleBackground';
|
||||
import styled from 'styled-components';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import background from '../../../bg.jpeg'
|
||||
import PageWrapperLayout from '../../components/PageWrapperLayout/PageWrapperLayout';
|
||||
import MDBox from '../../ui-kit/components/MDBox';
|
||||
import MDTypography from '../../ui-kit/components/MDTypography';
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
import MDButton from '../../ui-kit/components/MDButton';
|
||||
import MDAlert from '../../ui-kit/components/MDAlert';
|
||||
// Styled Components
|
||||
const PageContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
font-family: 'Inter', sans-serif;
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
padding: 2rem;
|
||||
`;
|
||||
|
||||
const GlassCard = styled.div`
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 1rem;
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&:focus {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(100, 149, 237, 0.5);
|
||||
box-shadow: 0 0 10px rgba(100, 149, 237, 0.2);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled.button`
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(118, 75, 162, 0.4);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const ErrorMessage = styled.div`
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ForgotPasswordLink = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-top: 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
export type SignInValues = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const validationSchema = object().shape({
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
email: Yup.string().email('Invalid email').required('Required'),
|
||||
password: Yup.string().required('Required'),
|
||||
})
|
||||
|
||||
interface GetUserResponse {
|
||||
email: string
|
||||
}
|
||||
|
||||
const SignIn = ({}): JSX.Element => {
|
||||
const {authenticated, setAuthentication, setNeedsNewPassword} = useContext(AuthContext);
|
||||
const { account, setAccount} = useContext(AccountContext);
|
||||
const navigate = useNavigate();
|
||||
const SignIn = ({ }): JSX.Element => {
|
||||
const { authenticated, setAuthentication, setNeedsNewPassword } = useContext(AuthContext);
|
||||
const { account, setAccount } = useContext(AccountContext);
|
||||
const navigate = useNavigate();
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
|
||||
const handleSignIn = async ({email, password}: SignInValues): Promise<void> => {
|
||||
//const curr_account = MOCK_ACCOUNTS.find((account) => account.email === email)
|
||||
//if (curr_account){
|
||||
try{
|
||||
const response = await axiosInstance.post('/token/obtain/',{
|
||||
|
||||
const handleSignIn = async ({ email, password }: SignInValues): Promise<void> => {
|
||||
try {
|
||||
const response = await axiosInstance.post('/token/obtain/', {
|
||||
username: email,
|
||||
password: password,
|
||||
|
||||
@@ -55,7 +152,7 @@ const SignIn = ({}): JSX.Element => {
|
||||
localStorage.setItem('access_token', response.data.access);
|
||||
localStorage.setItem('refresh_token', response.data.refresh);
|
||||
|
||||
const get_user_response: AxiosResponse<AccountType> = await axiosInstance.get('/user/get/')
|
||||
const get_user_response: AxiosResponse<AccountType> = await axiosInstance.get('/user/get/')
|
||||
|
||||
const account = new Account({
|
||||
email: get_user_response.data.email,
|
||||
@@ -70,192 +167,66 @@ const SignIn = ({}): JSX.Element => {
|
||||
zipcode: get_user_response.data.company?.zipcode,
|
||||
address: get_user_response.data.company?.address,
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
setAccount(account);
|
||||
setAuthentication(true)
|
||||
setNeedsNewPassword(get_user_response.data.has_usable_password)
|
||||
// TODO: terms of service
|
||||
if (account.has_signed_tos){
|
||||
if (account.has_signed_tos) {
|
||||
navigate('/');
|
||||
} else {
|
||||
navigate('/terms_of_service/')
|
||||
}
|
||||
|
||||
}catch(error){
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
setErrorMessage('Error retrieving account. Try again');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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">
|
||||
Sign In
|
||||
</MDTypography>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
password: '',
|
||||
}}
|
||||
onSubmit={handleSignIn}
|
||||
validationSchema={validationSchema}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form>
|
||||
{errorMessage &&
|
||||
<MDAlert color="error" dismissible={true}>{errorMessage}</MDAlert>
|
||||
|
||||
}
|
||||
|
||||
<div className='row'>
|
||||
<div className='col'>
|
||||
<CustomTextField
|
||||
label='Email'
|
||||
name="email"
|
||||
changeHandler={(e) => formik.setFieldValue('email', e.target.value)} isMultline={false}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='col'>
|
||||
<CustomPasswordField
|
||||
label='Password'
|
||||
name="password"
|
||||
changeHandler={(e) => formik.setFieldValue('password', e.target.value)} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='col-6'>
|
||||
<MDButton
|
||||
type={'submit'}
|
||||
fullWidth
|
||||
>
|
||||
<MDTypography
|
||||
as="h6">
|
||||
Sign In
|
||||
|
||||
</MDTypography>
|
||||
|
||||
</MDButton>
|
||||
</div>
|
||||
{/* <div className='col-6'>
|
||||
<MDButton
|
||||
fullWidth
|
||||
onClick={() => navigate('/password_reset')}
|
||||
>
|
||||
<MDTypography
|
||||
color="error"
|
||||
as="h6"
|
||||
>
|
||||
Reset Password
|
||||
|
||||
</MDTypography>
|
||||
|
||||
</MDButton>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
</Form>
|
||||
)}
|
||||
|
||||
</Formik>
|
||||
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
|
||||
|
||||
</Row>
|
||||
</MDBox>
|
||||
|
||||
</MDBox>
|
||||
</PageWrapperLayout>
|
||||
|
||||
// <div className='main-content' style={{'height': '100%', minHeight: '100vh', display: 'flex', flexDirection: 'column', backgroundImage: `url(${background})`,backgroundSize: "cover",
|
||||
// backgroundRepeat: "no-repeat",}}>
|
||||
// <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'>Sign In</h4>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div className='card-body text-center'>
|
||||
// <Formik
|
||||
// initialValues={{
|
||||
// email: '',
|
||||
// password: '',
|
||||
// }}
|
||||
// onSubmit={handleSignIn}
|
||||
// validationSchema={validationSchema}
|
||||
// >
|
||||
// {(formik) => (
|
||||
// <Form>
|
||||
// {errorMessage &&
|
||||
// <Alert severity="error">{errorMessage}</Alert>
|
||||
|
||||
// }
|
||||
|
||||
// <div className='row'>
|
||||
// <div className='col'>
|
||||
// <CustomTextField
|
||||
// label='Email'
|
||||
// name="email"
|
||||
// changeHandler={(e) => formik.setFieldValue('email', e.target.value)} isMultline={false}/>
|
||||
|
||||
// </div>
|
||||
// </div>
|
||||
// <div className='row'>
|
||||
// <div className='col'>
|
||||
// <CustomPasswordField
|
||||
// label='Password'
|
||||
// name="password"
|
||||
// changeHandler={(e) => formik.setFieldValue('password', e.target.value)} />
|
||||
|
||||
// </div>
|
||||
// </div>
|
||||
// <div className='row'>
|
||||
// <div className='col'>
|
||||
// <Button
|
||||
// type={'submit'}
|
||||
// >
|
||||
// Sign In
|
||||
// </Button>
|
||||
|
||||
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// </Form>
|
||||
// )}
|
||||
|
||||
// </Formik>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
);
|
||||
return (
|
||||
<PageContainer>
|
||||
<ParticleBackground />
|
||||
<ContentWrapper>
|
||||
<GlassCard>
|
||||
<CardTitle>Sign In</CardTitle>
|
||||
{errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
password: '',
|
||||
}}
|
||||
onSubmit={handleSignIn}
|
||||
validationSchema={validationSchema}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form style={{ width: '100%' }}>
|
||||
<Field
|
||||
as={StyledInput}
|
||||
name="email"
|
||||
placeholder="Email Address"
|
||||
type="email"
|
||||
/>
|
||||
<Field
|
||||
as={StyledInput}
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
/>
|
||||
<StyledButton type="submit" disabled={formik.isSubmitting}>
|
||||
Sign In
|
||||
</StyledButton>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<ForgotPasswordLink onClick={() => navigate('/password_reset')}>
|
||||
Forgot Password?
|
||||
</ForgotPasswordLink>
|
||||
</GlassCard>
|
||||
</ContentWrapper>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
@@ -14,7 +14,7 @@ Coded by www.creative-tim.com
|
||||
*/
|
||||
|
||||
// @mui material components
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
import { createTheme as createThemeMUI } from "@mui/material/styles";
|
||||
// import Fade from "@mui/material/Fade";
|
||||
|
||||
// Material Dashboard 2 React base styles
|
||||
@@ -84,75 +84,91 @@ import dialogContent from "./components/dialog/dialogContent";
|
||||
import dialogContentText from "./components/dialog/dialogContentText";
|
||||
import dialogActions from "./components/dialog/dialogActions";
|
||||
|
||||
export default createTheme({
|
||||
breakpoints: { ...breakpoints },
|
||||
palette: { ...colors },
|
||||
typography: { ...typography },
|
||||
boxShadows: { ...boxShadows },
|
||||
borders: { ...borders },
|
||||
functions: {
|
||||
boxShadow,
|
||||
hexToRgb,
|
||||
linearGradient,
|
||||
pxToRem,
|
||||
rgba,
|
||||
},
|
||||
export default function createTheme(color) {
|
||||
const { main, focus } = color;
|
||||
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
...globals,
|
||||
...container,
|
||||
},
|
||||
const themeColors = {
|
||||
...colors,
|
||||
info: {
|
||||
main,
|
||||
focus,
|
||||
},
|
||||
MuiDrawer: { ...sidenav },
|
||||
MuiList: { ...list },
|
||||
MuiListItem: { ...listItem },
|
||||
MuiListItemText: { ...listItemText },
|
||||
MuiCard: { ...card },
|
||||
MuiCardMedia: { ...cardMedia },
|
||||
MuiCardContent: { ...cardContent },
|
||||
MuiButton: { ...button },
|
||||
MuiIconButton: { ...iconButton },
|
||||
MuiInput: { ...input },
|
||||
MuiInputLabel: { ...inputLabel },
|
||||
MuiOutlinedInput: { ...inputOutlined },
|
||||
MuiTextField: { ...textField },
|
||||
MuiMenu: { ...menu },
|
||||
MuiMenuItem: { ...menuItem },
|
||||
MuiSwitch: { ...switchButton },
|
||||
MuiDivider: { ...divider },
|
||||
MuiTableContainer: { ...tableContainer },
|
||||
MuiTableHead: { ...tableHead },
|
||||
MuiTableCell: { ...tableCell },
|
||||
MuiLinearProgress: { ...linearProgress },
|
||||
MuiBreadcrumbs: { ...breadcrumbs },
|
||||
MuiSlider: { ...slider },
|
||||
MuiAvatar: { ...avatar },
|
||||
MuiTooltip: { ...tooltip },
|
||||
MuiAppBar: { ...appBar },
|
||||
MuiTabs: { ...tabs },
|
||||
MuiTab: { ...tab },
|
||||
MuiStepper: { ...stepper },
|
||||
MuiStep: { ...step },
|
||||
MuiStepConnector: { ...stepConnector },
|
||||
MuiStepLabel: { ...stepLabel },
|
||||
MuiStepIcon: { ...stepIcon },
|
||||
MuiSelect: { ...select },
|
||||
MuiFormControlLabel: { ...formControlLabel },
|
||||
MuiFormLabel: { ...formLabel },
|
||||
MuiCheckbox: { ...checkbox },
|
||||
MuiRadio: { ...radio },
|
||||
MuiAutocomplete: { ...autocomplete },
|
||||
MuiPopover: { ...popover },
|
||||
MuiButtonBase: { ...buttonBase },
|
||||
MuiIcon: { ...icon },
|
||||
MuiSvgIcon: { ...svgIcon },
|
||||
MuiLink: { ...link },
|
||||
MuiDialog: { ...dialog },
|
||||
MuiDialogTitle: { ...dialogTitle },
|
||||
MuiDialogContent: { ...dialogContent },
|
||||
MuiDialogContentText: { ...dialogContentText },
|
||||
MuiDialogActions: { ...dialogActions },
|
||||
},
|
||||
});
|
||||
primary: {
|
||||
main,
|
||||
focus,
|
||||
},
|
||||
};
|
||||
|
||||
return createThemeMUI({
|
||||
breakpoints: { ...breakpoints },
|
||||
palette: { ...themeColors },
|
||||
typography: { ...typography },
|
||||
boxShadows: { ...boxShadows },
|
||||
borders: { ...borders },
|
||||
functions: {
|
||||
boxShadow,
|
||||
hexToRgb,
|
||||
linearGradient,
|
||||
pxToRem,
|
||||
rgba,
|
||||
},
|
||||
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
...globals,
|
||||
...container,
|
||||
},
|
||||
},
|
||||
MuiDrawer: { ...sidenav },
|
||||
MuiList: { ...list },
|
||||
MuiListItem: { ...listItem },
|
||||
MuiListItemText: { ...listItemText },
|
||||
MuiCard: { ...card },
|
||||
MuiCardMedia: { ...cardMedia },
|
||||
MuiCardContent: { ...cardContent },
|
||||
MuiButton: { ...button },
|
||||
MuiIconButton: { ...iconButton },
|
||||
MuiInput: { ...input },
|
||||
MuiInputLabel: { ...inputLabel },
|
||||
MuiOutlinedInput: { ...inputOutlined },
|
||||
MuiTextField: { ...textField },
|
||||
MuiMenu: { ...menu },
|
||||
MuiMenuItem: { ...menuItem },
|
||||
MuiSwitch: { ...switchButton },
|
||||
MuiDivider: { ...divider },
|
||||
MuiTableContainer: { ...tableContainer },
|
||||
MuiTableHead: { ...tableHead },
|
||||
MuiTableCell: { ...tableCell },
|
||||
MuiLinearProgress: { ...linearProgress },
|
||||
MuiBreadcrumbs: { ...breadcrumbs },
|
||||
MuiSlider: { ...slider },
|
||||
MuiAvatar: { ...avatar },
|
||||
MuiTooltip: { ...tooltip },
|
||||
MuiAppBar: { ...appBar },
|
||||
MuiTabs: { ...tabs },
|
||||
MuiTab: { ...tab },
|
||||
MuiStepper: { ...stepper },
|
||||
MuiStep: { ...step },
|
||||
MuiStepConnector: { ...stepConnector },
|
||||
MuiStepLabel: { ...stepLabel },
|
||||
MuiStepIcon: { ...stepIcon },
|
||||
MuiSelect: { ...select },
|
||||
MuiFormControlLabel: { ...formControlLabel },
|
||||
MuiFormLabel: { ...formLabel },
|
||||
MuiCheckbox: { ...checkbox },
|
||||
MuiRadio: { ...radio },
|
||||
MuiAutocomplete: { ...autocomplete },
|
||||
MuiPopover: { ...popover },
|
||||
MuiButtonBase: { ...buttonBase },
|
||||
MuiIcon: { ...icon },
|
||||
MuiSvgIcon: { ...svgIcon },
|
||||
MuiLink: { ...link },
|
||||
MuiDialog: { ...dialog },
|
||||
MuiDialogTitle: { ...dialogTitle },
|
||||
MuiDialogContent: { ...dialogContent },
|
||||
MuiDialogContentText: { ...dialogContentText },
|
||||
MuiDialogActions: { ...dialogActions },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
28
llm-fe/src/llm-fe/ui-kit/assets/theme/base/palettes.js
Normal file
28
llm-fe/src/llm-fe/ui-kit/assets/theme/base/palettes.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* The base color palettes for the Material Dashboard 2 React.
|
||||
*/
|
||||
|
||||
const palettes = {
|
||||
blue: {
|
||||
main: "#1A73E8",
|
||||
focus: "#1662C4",
|
||||
},
|
||||
green: {
|
||||
main: "#4CAF50",
|
||||
focus: "#67bb6a",
|
||||
},
|
||||
purple: {
|
||||
main: "#9c27b0",
|
||||
focus: "#ba68c8",
|
||||
},
|
||||
orange: {
|
||||
main: "#fb8c00",
|
||||
focus: "#fc9d26",
|
||||
},
|
||||
red: {
|
||||
main: "#F44335",
|
||||
focus: "#f65f53",
|
||||
},
|
||||
};
|
||||
|
||||
export default palettes;
|
||||
@@ -14,7 +14,7 @@ Coded by www.creative-tim.com
|
||||
*/
|
||||
|
||||
// @mui material components
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
import { createTheme as createThemeMUI } from "@mui/material/styles";
|
||||
|
||||
// Material Dashboard 2 React base styles
|
||||
import colors from "./base/colors";
|
||||
@@ -83,75 +83,91 @@ import dialogContent from "./components/dialog/dialogContent";
|
||||
import dialogContentText from "./components/dialog/dialogContentText";
|
||||
import dialogActions from "./components/dialog/dialogActions";
|
||||
|
||||
export default createTheme({
|
||||
breakpoints: { ...breakpoints },
|
||||
palette: { ...colors },
|
||||
typography: { ...typography },
|
||||
boxShadows: { ...boxShadows },
|
||||
borders: { ...borders },
|
||||
functions: {
|
||||
boxShadow,
|
||||
hexToRgb,
|
||||
linearGradient,
|
||||
pxToRem,
|
||||
rgba,
|
||||
},
|
||||
export default function createTheme(color) {
|
||||
const { main, focus } = color;
|
||||
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
...globals,
|
||||
...container,
|
||||
},
|
||||
const themeColors = {
|
||||
...colors,
|
||||
info: {
|
||||
main,
|
||||
focus,
|
||||
},
|
||||
MuiDrawer: { ...sidenav },
|
||||
MuiList: { ...list },
|
||||
MuiListItem: { ...listItem },
|
||||
MuiListItemText: { ...listItemText },
|
||||
MuiCard: { ...card },
|
||||
MuiCardMedia: { ...cardMedia },
|
||||
MuiCardContent: { ...cardContent },
|
||||
MuiButton: { ...button },
|
||||
MuiIconButton: { ...iconButton },
|
||||
MuiInput: { ...input },
|
||||
MuiInputLabel: { ...inputLabel },
|
||||
MuiOutlinedInput: { ...inputOutlined },
|
||||
MuiTextField: { ...textField },
|
||||
MuiMenu: { ...menu },
|
||||
MuiMenuItem: { ...menuItem },
|
||||
MuiSwitch: { ...switchButton },
|
||||
MuiDivider: { ...divider },
|
||||
MuiTableContainer: { ...tableContainer },
|
||||
MuiTableHead: { ...tableHead },
|
||||
MuiTableCell: { ...tableCell },
|
||||
MuiLinearProgress: { ...linearProgress },
|
||||
MuiBreadcrumbs: { ...breadcrumbs },
|
||||
MuiSlider: { ...slider },
|
||||
MuiAvatar: { ...avatar },
|
||||
MuiTooltip: { ...tooltip },
|
||||
MuiAppBar: { ...appBar },
|
||||
MuiTabs: { ...tabs },
|
||||
MuiTab: { ...tab },
|
||||
MuiStepper: { ...stepper },
|
||||
MuiStep: { ...step },
|
||||
MuiStepConnector: { ...stepConnector },
|
||||
MuiStepLabel: { ...stepLabel },
|
||||
MuiStepIcon: { ...stepIcon },
|
||||
MuiSelect: { ...select },
|
||||
MuiFormControlLabel: { ...formControlLabel },
|
||||
MuiFormLabel: { ...formLabel },
|
||||
MuiCheckbox: { ...checkbox },
|
||||
MuiRadio: { ...radio },
|
||||
MuiAutocomplete: { ...autocomplete },
|
||||
MuiPopover: { ...popover },
|
||||
MuiButtonBase: { ...buttonBase },
|
||||
MuiIcon: { ...icon },
|
||||
MuiSvgIcon: { ...svgIcon },
|
||||
MuiLink: { ...link },
|
||||
MuiDialog: { ...dialog },
|
||||
MuiDialogTitle: { ...dialogTitle },
|
||||
MuiDialogContent: { ...dialogContent },
|
||||
MuiDialogContentText: { ...dialogContentText },
|
||||
MuiDialogActions: { ...dialogActions },
|
||||
},
|
||||
});
|
||||
primary: {
|
||||
main,
|
||||
focus,
|
||||
},
|
||||
};
|
||||
|
||||
return createThemeMUI({
|
||||
breakpoints: { ...breakpoints },
|
||||
palette: { ...themeColors },
|
||||
typography: { ...typography },
|
||||
boxShadows: { ...boxShadows },
|
||||
borders: { ...borders },
|
||||
functions: {
|
||||
boxShadow,
|
||||
hexToRgb,
|
||||
linearGradient,
|
||||
pxToRem,
|
||||
rgba,
|
||||
},
|
||||
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
...globals,
|
||||
...container,
|
||||
},
|
||||
},
|
||||
MuiDrawer: { ...sidenav },
|
||||
MuiList: { ...list },
|
||||
MuiListItem: { ...listItem },
|
||||
MuiListItemText: { ...listItemText },
|
||||
MuiCard: { ...card },
|
||||
MuiCardMedia: { ...cardMedia },
|
||||
MuiCardContent: { ...cardContent },
|
||||
MuiButton: { ...button },
|
||||
MuiIconButton: { ...iconButton },
|
||||
MuiInput: { ...input },
|
||||
MuiInputLabel: { ...inputLabel },
|
||||
MuiOutlinedInput: { ...inputOutlined },
|
||||
MuiTextField: { ...textField },
|
||||
MuiMenu: { ...menu },
|
||||
MuiMenuItem: { ...menuItem },
|
||||
MuiSwitch: { ...switchButton },
|
||||
MuiDivider: { ...divider },
|
||||
MuiTableContainer: { ...tableContainer },
|
||||
MuiTableHead: { ...tableHead },
|
||||
MuiTableCell: { ...tableCell },
|
||||
MuiLinearProgress: { ...linearProgress },
|
||||
MuiBreadcrumbs: { ...breadcrumbs },
|
||||
MuiSlider: { ...slider },
|
||||
MuiAvatar: { ...avatar },
|
||||
MuiTooltip: { ...tooltip },
|
||||
MuiAppBar: { ...appBar },
|
||||
MuiTabs: { ...tabs },
|
||||
MuiTab: { ...tab },
|
||||
MuiStepper: { ...stepper },
|
||||
MuiStep: { ...step },
|
||||
MuiStepConnector: { ...stepConnector },
|
||||
MuiStepLabel: { ...stepLabel },
|
||||
MuiStepIcon: { ...stepIcon },
|
||||
MuiSelect: { ...select },
|
||||
MuiFormControlLabel: { ...formControlLabel },
|
||||
MuiFormLabel: { ...formLabel },
|
||||
MuiCheckbox: { ...checkbox },
|
||||
MuiRadio: { ...radio },
|
||||
MuiAutocomplete: { ...autocomplete },
|
||||
MuiPopover: { ...popover },
|
||||
MuiButtonBase: { ...buttonBase },
|
||||
MuiIcon: { ...icon },
|
||||
MuiSvgIcon: { ...svgIcon },
|
||||
MuiLink: { ...link },
|
||||
MuiDialog: { ...dialog },
|
||||
MuiDialogTitle: { ...dialogTitle },
|
||||
MuiDialogContent: { ...dialogContent },
|
||||
MuiDialogContentText: { ...dialogContentText },
|
||||
MuiDialogActions: { ...dialogActions },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ Coded by www.creative-tim.com
|
||||
you can customize the states for the different components here.
|
||||
*/
|
||||
|
||||
import { createContext, useContext, useReducer, useMemo } from "react";
|
||||
import { createContext, useContext, useReducer, useMemo, useEffect } from "react";
|
||||
|
||||
// prop-types is a library for typechecking of props
|
||||
import PropTypes from "prop-types";
|
||||
@@ -62,6 +62,9 @@ function reducer(state, action) {
|
||||
case "DARKMODE": {
|
||||
return { ...state, darkMode: action.value };
|
||||
}
|
||||
case "THEME_COLOR": {
|
||||
return { ...state, themeColor: action.value };
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unhandled action type: ${action.type}`);
|
||||
}
|
||||
@@ -80,13 +83,19 @@ function MaterialUIControllerProvider({ children }) {
|
||||
openConfigurator: false,
|
||||
direction: "ltr",
|
||||
layout: "dashboard",
|
||||
darkMode: false,
|
||||
darkMode: localStorage.getItem("darkMode") === "true",
|
||||
themeColor: localStorage.getItem("themeColor") || "blue",
|
||||
};
|
||||
|
||||
const [controller, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const value = useMemo(() => [controller, dispatch], [controller, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("darkMode", controller.darkMode);
|
||||
localStorage.setItem("themeColor", controller.themeColor);
|
||||
}, [controller.darkMode, controller.themeColor]);
|
||||
|
||||
return <MaterialUI.Provider value={value}>{children}</MaterialUI.Provider>;
|
||||
}
|
||||
|
||||
@@ -119,6 +128,7 @@ const setOpenConfigurator = (dispatch, value) => dispatch({ type: "OPEN_CONFIGUR
|
||||
const setDirection = (dispatch, value) => dispatch({ type: "DIRECTION", value });
|
||||
const setLayout = (dispatch, value) => dispatch({ type: "LAYOUT", value });
|
||||
const setDarkMode = (dispatch, value) => dispatch({ type: "DARKMODE", value });
|
||||
const setThemeColor = (dispatch, value) => dispatch({ type: "THEME_COLOR", value });
|
||||
|
||||
export {
|
||||
MaterialUIControllerProvider,
|
||||
@@ -133,4 +143,5 @@ export {
|
||||
setDirection,
|
||||
setLayout,
|
||||
setDarkMode,
|
||||
setThemeColor,
|
||||
};
|
||||
|
||||
@@ -42,8 +42,11 @@ import {
|
||||
setWhiteSidenav,
|
||||
setFixedNavbar,
|
||||
setSidenavColor,
|
||||
|
||||
setDarkMode,
|
||||
setThemeColor,
|
||||
} from "../../context";
|
||||
import palettes from "../../assets/theme/base/palettes";
|
||||
import MDBox from "../../components/MDBox";
|
||||
import MDTypography from "../../components/MDTypography";
|
||||
import MDButton from "../../components/MDButton";
|
||||
@@ -57,9 +60,11 @@ function Configurator() {
|
||||
transparentSidenav,
|
||||
whiteSidenav,
|
||||
darkMode,
|
||||
themeColor,
|
||||
} = controller;
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const sidenavColors = ["primary", "dark", "info", "success", "warning", "error"];
|
||||
const themeColors = Object.keys(palettes);
|
||||
|
||||
// Use the useEffect hook to change the button state for the sidenav type based on window size.
|
||||
useEffect(() => {
|
||||
@@ -208,6 +213,51 @@ function Configurator() {
|
||||
</MDBox>
|
||||
</MDBox>
|
||||
|
||||
<MDBox mt={3}>
|
||||
<MDTypography variant="h6">Theme Colors</MDTypography>
|
||||
|
||||
<MDBox mb={0.5}>
|
||||
{themeColors.map((color) => (
|
||||
<IconButton
|
||||
key={color}
|
||||
sx={({
|
||||
borders: { borderWidth },
|
||||
palette: { white, dark, background },
|
||||
transitions,
|
||||
}) => ({
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
padding: 0,
|
||||
border: `${borderWidth[1]} solid ${darkMode ? background.sidenav : white.main}`,
|
||||
borderColor: () => {
|
||||
let borderColorValue = themeColor === color && dark.main;
|
||||
|
||||
if (darkMode && themeColor === color) {
|
||||
borderColorValue = white.main;
|
||||
}
|
||||
|
||||
return borderColorValue;
|
||||
},
|
||||
transition: transitions.create("border-color", {
|
||||
easing: transitions.easing.sharp,
|
||||
duration: transitions.duration.shorter,
|
||||
}),
|
||||
backgroundColor: palettes[color].main,
|
||||
|
||||
"&:not(:last-child)": {
|
||||
mr: 1,
|
||||
},
|
||||
|
||||
"&:hover, &:focus, &:active": {
|
||||
borderColor: darkMode ? white.main : dark.main,
|
||||
},
|
||||
})}
|
||||
onClick={() => setThemeColor(dispatch, color)}
|
||||
/>
|
||||
))}
|
||||
</MDBox>
|
||||
</MDBox>
|
||||
|
||||
<MDBox mt={3} lineHeight={1}>
|
||||
<MDTypography variant="h6">Sidenav Type</MDTypography>
|
||||
<MDTypography variant="button" color="text">
|
||||
|
||||
Reference in New Issue
Block a user