user profile and AI chat

main
alex 2024-09-09 22:30:05 +02:00
parent bb1bf8ef0e
commit 679ee5bf28
12 changed files with 561 additions and 611 deletions

View File

@ -1,95 +1,78 @@
import { ConfigProvider, Layout, theme } from "antd"; import { ConfigProvider, Layout, theme } from 'antd';
import DashboardLayout from "./core/components/DashboardLayout"; import DashboardLayout from './core/components/DashboardLayout';
import { import { darkMode, primaryColor, setBannerUrl, setLogoUrl, setPrimaryColor, setUserAuthenticated, userAuthenticated, setUserProfilePictureUrl } from './core/reducers/appSlice';
darkMode, import { useDispatch, useSelector } from 'react-redux';
primaryColor, import SignIn from './features/Auth/SignIn';
setBannerUrl, import { useEffect } from 'react';
setLogoUrl, import { myFetch } from './core/utils/utils';
setPrimaryColor, import MyCenteredSpin from './shared/components/MyCenteredSpin';
setUserAuthenticated, import webSocketService, { WebSocketMessageHandler } from 'core/services/websocketService';
userAuthenticated, import { MessageProvider } from 'core/context/MessageContext';
} from "./core/reducers/appSlice";
import { useDispatch, useSelector } from "react-redux";
import SignIn from "./features/Auth/SignIn";
import { useEffect } from "react";
import { myFetch } from "./core/utils/utils";
import MyCenteredSpin from "./shared/components/MyCenteredSpin";
import webSocketService, {
WebSocketMessageHandler,
} from "core/services/websocketService";
import { MessageProvider } from "core/context/MessageContext";
const { defaultAlgorithm, darkAlgorithm } = theme; const { defaultAlgorithm, darkAlgorithm } = theme;
export default function App() { export default function App() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isDarkMode = useSelector(darkMode); const isDarkMode = useSelector(darkMode);
const uAuthenticated = useSelector(userAuthenticated); const uAuthenticated = useSelector(userAuthenticated);
const primColor = useSelector(primaryColor); const primColor = useSelector(primaryColor);
console.info( console.info(
"\n %c LMS %c v1.0.0 %c \n", '\n %c LMS %c v1.0.0 %c \n',
"background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 3px 0 0 3px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)", 'background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 3px 0 0 3px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)',
"background-color: #bc81e0;background-image: linear-gradient(90deg, #e67e22, #9b59b6);color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 3px 3px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)", 'background-color: #bc81e0;background-image: linear-gradient(90deg, #e67e22, #9b59b6);color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 3px 3px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)',
"background-color: transparent" 'background-color: transparent'
); );
useEffect(() => { useEffect(() => {
if (uAuthenticated) { if (uAuthenticated) {
(async () => { (async () => {
try { try {
const response = await myFetch({ const response = await myFetch({
url: "/app", url: '/app',
method: "GET", method: 'GET',
}); });
if (response) { if (response) {
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`)); dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
dispatch(setLogoUrl(response.Organization.LogoUrl)); dispatch(setLogoUrl(response.Organization.LogoUrl));
dispatch(setBannerUrl(response.Organization.BannerUrl)); dispatch(setBannerUrl(response.Organization.BannerUrl));
dispatch(setUserProfilePictureUrl(response.User.ProfilePictureUrl));
dispatch(setUserAuthenticated(true));
webSocketService.connect();
webSocketService.setHandler(WebSocketMessageHandler, dispatch);
}
} catch (error) {}
})();
return () => {
webSocketService.disconnect();
};
}
}, [uAuthenticated]);
useEffect(() => {
if (!localStorage.getItem('session')) {
dispatch(setUserAuthenticated(false));
} else {
dispatch(setUserAuthenticated(true)); dispatch(setUserAuthenticated(true));
}
}, [dispatch]);
webSocketService.connect(); return (
webSocketService.setHandler(WebSocketMessageHandler, dispatch); <Layout style={{ minHeight: '100vh' }}>
} <ConfigProvider
} catch (error) {} theme={{
})(); algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
token: {
return () => { colorPrimary: primColor,
webSocketService.disconnect(); },
}; }}
} >
}, [uAuthenticated]); <MessageProvider>{uAuthenticated == null ? <MyCenteredSpin /> : uAuthenticated ? <DashboardLayout /> : <SignIn />}</MessageProvider>
</ConfigProvider>
useEffect(() => { </Layout>
if (!localStorage.getItem("session")) { );
dispatch(setUserAuthenticated(false));
} else {
dispatch(setUserAuthenticated(true));
}
}, [dispatch]);
return (
<Layout style={{ minHeight: "100vh" }}>
<ConfigProvider
theme={{
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
token: {
colorPrimary: primColor,
},
}}
>
<MessageProvider>
{uAuthenticated == null ? (
<MyCenteredSpin />
) : uAuthenticated ? (
<DashboardLayout />
) : (
<SignIn />
)}
</MessageProvider>
</ConfigProvider>
</Layout>
);
} }

View File

@ -1,168 +1,131 @@
import { Avatar, Dropdown, Flex } from "antd"; import { Avatar, Dropdown, Flex } from 'antd';
import { import { isSideMenuCollapsed, setIsSideMenuCollapsed } from '../SideMenu/sideMenuSlice';
isSideMenuCollapsed, import { useDispatch, useSelector } from 'react-redux';
setIsSideMenuCollapsed, import { EditOutlined, EyeOutlined, LeftOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, MoonOutlined, SunOutlined, UserOutlined } from '@ant-design/icons';
} from "../SideMenu/sideMenuSlice"; import { Link, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from "react-redux"; import { darkMode, setDarkMode, setUserAuthenticated } from '../../reducers/appSlice';
import { import styles from './styles.module.css';
EditOutlined, import { Constants } from 'core/utils/utils';
EyeOutlined, import webSocketService from 'core/services/websocketService';
LeftOutlined, import { userProfilePictureUrl } from 'core/reducers/appSlice';
LogoutOutlined, import MyUserAvatar from 'shared/components/MyUserAvatar';
MenuFoldOutlined,
MenuUnfoldOutlined,
MoonOutlined,
SunOutlined,
UserOutlined,
} from "@ant-design/icons";
import { Link, useNavigate } from "react-router-dom";
import {
darkMode,
setDarkMode,
setUserAuthenticated,
} from "../../reducers/appSlice";
import styles from "./styles.module.css";
import { Constants } from "core/utils/utils";
import webSocketService from "core/services/websocketService";
type HeaderBarProps = { type HeaderBarProps = {
theme?: "light" | "dark"; theme?: 'light' | 'dark';
onView?: () => void; onView?: () => void;
onEdit?: () => void; onEdit?: () => void;
backTo?: string; backTo?: string;
}; };
export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) { export default function HeaderBar(props: HeaderBarProps = { theme: 'light' }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isCollpased = useSelector(isSideMenuCollapsed); const isCollpased = useSelector(isSideMenuCollapsed);
const isDarkMode = useSelector(darkMode); const isDarkMode = useSelector(darkMode);
const profilePictureUrl = useSelector(userProfilePictureUrl);
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<Flex <Flex
justify="space-between" justify="space-between"
align="center" align="center"
style={{ style={{
paddingTop: 12, paddingTop: 12,
paddingLeft: 12, paddingLeft: 12,
paddingRight: 12, paddingRight: 12,
}} }}
>
<Flex align="center" gap={16}>
<div
className={
props.theme === "light"
? styles.containerLight
: styles.containerDark
}
style={{ borderRadius: 28, padding: 4 }}
> >
{isCollpased ? ( <Flex align="center" gap={16}>
<div <div className={props.theme === 'light' ? styles.containerLight : styles.containerDark} style={{ borderRadius: 28, padding: 4 }}>
className={styles.iconContainer} {isCollpased ? (
onClick={() => dispatch(setIsSideMenuCollapsed(false))} <div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(false))}>
> <MenuUnfoldOutlined className={styles.icon} />
<MenuUnfoldOutlined className={styles.icon} /> </div>
</div> ) : (
) : ( <div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(true))}>
<div <MenuFoldOutlined className={styles.icon} />
className={styles.iconContainer} </div>
onClick={() => dispatch(setIsSideMenuCollapsed(true))} )}
> </div>
<MenuFoldOutlined className={styles.icon} />
</div>
)}
</div>
{props.backTo && ( {props.backTo && (
<Link to={props.backTo}> <Link to={props.backTo}>
<Flex gap={4}> <Flex gap={4}>
<LeftOutlined /> <LeftOutlined />
<span>Back</span> <span>Back</span>
</Flex>
</Link>
)}
</Flex> </Flex>
</Link>
)}
</Flex>
<Flex <Flex
align="center" align="center"
className={ className={props.theme === 'light' ? styles.containerLight : styles.containerDark}
props.theme === "light" ? styles.containerLight : styles.containerDark style={{
} borderRadius: 28,
style={{ paddingLeft: 6,
borderRadius: 28, paddingRight: 6,
paddingLeft: 6, paddingTop: 4,
paddingRight: 6, paddingBottom: 4,
paddingTop: 4, }}
paddingBottom: 4, gap={8}
}} >
gap={8} {props.onView && (
> <div className={styles.iconContainer} onClick={props.onView}>
{props.onView && ( <EyeOutlined className={styles.icon} />
<div className={styles.iconContainer} onClick={props.onView}> </div>
<EyeOutlined className={styles.icon} /> )}
</div>
)}
{props.onEdit && ( {props.onEdit && (
<div className={styles.iconContainer} onClick={props.onEdit}> <div className={styles.iconContainer} onClick={props.onEdit}>
<EditOutlined className={styles.icon} /> <EditOutlined className={styles.icon} />
</div> </div>
)} )}
{isDarkMode ? ( {isDarkMode ? (
<div <div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(false))}>
className={styles.iconContainer} <SunOutlined className={styles.icon} />
onClick={() => dispatch(setDarkMode(false))} </div>
> ) : (
<SunOutlined className={styles.icon} /> <div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(true))}>
</div> <MoonOutlined className={styles.icon} />
) : ( </div>
<div )}
className={styles.iconContainer} <Dropdown
onClick={() => dispatch(setDarkMode(true))} overlayStyle={{ minWidth: 150 }}
> trigger={['click']}
<MoonOutlined className={styles.icon} /> menu={{
</div> items: [
)} {
<Dropdown key: '1',
overlayStyle={{ minWidth: 150 }} label: 'Profile',
trigger={["click"]} icon: <UserOutlined />,
menu={{ onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS),
items: [ },
{ {
key: "1", key: '2',
label: "Profile", label: 'Logout',
icon: <UserOutlined />, icon: <LogoutOutlined />,
onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS), danger: true,
}, onClick: () => {
{ webSocketService.disconnect();
key: "2", window.localStorage.removeItem('session');
label: "Logout", window.location.href = '/';
icon: <LogoutOutlined />, },
danger: true, },
onClick: () => { ],
webSocketService.disconnect(); }}
window.localStorage.removeItem("session"); >
window.location.href = "/"; <div>
}, <MyUserAvatar size={34} profilePictureUrl={profilePictureUrl ? profilePictureUrl : ''} />
}, </div>
], </Dropdown>
}} </Flex>
> </Flex>
<Avatar );
size="default"
icon={<UserOutlined />}
style={{ cursor: "pointer" }}
/>
</Dropdown>
</Flex>
</Flex>
);
/* return ( /* return (
<Header <Header
style={{ style={{
position: "sticky", position: "sticky",

View File

@ -1,47 +1,45 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from '@reduxjs/toolkit';
export const appSlice = createSlice({ export const appSlice = createSlice({
name: "app", name: 'app',
initialState: { initialState: {
darkMode: false, darkMode: false,
userAuthenticated: null, userAuthenticated: null,
primaryColor: "#111", userProfilePictureUrl: null,
logoUrl: null, primaryColor: '#111',
bannerUrl: null, logoUrl: null,
}, bannerUrl: null,
reducers: {
setDarkMode: (state, action) => {
state.darkMode = action.payload;
}, },
setUserAuthenticated: (state, action) => { reducers: {
state.userAuthenticated = action.payload; setDarkMode: (state, action) => {
state.darkMode = action.payload;
},
setUserAuthenticated: (state, action) => {
state.userAuthenticated = action.payload;
},
setUserProfilePictureUrl: (state, action) => {
state.userProfilePictureUrl = action.payload;
},
setPrimaryColor: (state, action) => {
state.primaryColor = action.payload;
},
setLogoUrl: (state, action) => {
state.logoUrl = action.payload;
},
setBannerUrl: (state, action) => {
state.bannerUrl = action.payload;
},
}, },
setPrimaryColor: (state, action) => { selectors: {
state.primaryColor = action.payload; darkMode: (state) => state.darkMode,
userAuthenticated: (state) => state.userAuthenticated,
userProfilePictureUrl: (state) => state.userProfilePictureUrl,
primaryColor: (state) => state.primaryColor,
logoUrl: (state) => state.logoUrl,
bannerUrl: (state) => state.bannerUrl,
}, },
setLogoUrl: (state, action) => {
state.logoUrl = action.payload;
},
setBannerUrl: (state, action) => {
state.bannerUrl = action.payload;
},
},
selectors: {
darkMode: (state) => state.darkMode,
userAuthenticated: (state) => state.userAuthenticated,
primaryColor: (state) => state.primaryColor,
logoUrl: (state) => state.logoUrl,
bannerUrl: (state) => state.bannerUrl,
},
}); });
export const { export const { setDarkMode, setUserAuthenticated, setUserProfilePictureUrl, setPrimaryColor, setLogoUrl, setBannerUrl } = appSlice.actions;
setDarkMode,
setUserAuthenticated,
setPrimaryColor,
setLogoUrl,
setBannerUrl,
} = appSlice.actions;
export const { darkMode, userAuthenticated, primaryColor, logoUrl, bannerUrl } = export const { darkMode, userAuthenticated, userProfilePictureUrl, primaryColor, logoUrl, bannerUrl } = appSlice.selectors;
appSlice.selectors;

View File

@ -3,6 +3,7 @@ import { baseQueryWithErrorHandling } from "core/helper/api";
import { import {
Lesson, Lesson,
LessonContent, LessonContent,
LessonQuestion,
LessonSettings, LessonSettings,
UpdateLessonPreviewThumbnail, UpdateLessonPreviewThumbnail,
} from "core/types/lesson"; } from "core/types/lesson";
@ -93,6 +94,12 @@ export const lessonsApi = createApi({
method: "DELETE", method: "DELETE",
}), }),
}), }),
getQuestions: builder.query<LessonQuestion[], string>({
query: (lessonId) => ({
url: `lessons/${lessonId}/questions`,
method: "GET",
}),
}),
}), }),
}); });

View File

@ -1,286 +1,255 @@
import { Dispatch } from "@reduxjs/toolkit"; import { Dispatch } from '@reduxjs/toolkit';
import { setBannerUrl, setLogoUrl, setPrimaryColor, setUserProfilePictureUrl } from 'core/reducers/appSlice';
import { store } from 'core/store/store';
import { BrowserTabSession, Constants } from 'core/utils/utils';
import { WebSocketReceivedMessagesCmds } from 'core/utils/webSocket';
import { addLessonPageContent, deleteLessonPageContent, updateLessonPageContent, updateLessonPageContentPosition } from 'features/Lessons/LessonPage/lessonPageSlice';
import { import {
setBannerUrl, addLessonContent,
setLogoUrl, deleteLessonContent,
setPrimaryColor, setLessonThumbnailTitle,
} from "core/reducers/appSlice"; setLessonThumbnailUrl,
import { store } from "core/store/store"; setPageEditorLessonState,
import { BrowserTabSession, Constants } from "core/utils/utils"; updateLessonContent,
import { WebSocketReceivedMessagesCmds } from "core/utils/webSocket"; updateLessonContentPosition,
import { } from 'features/Lessons/LessonPageEditor/lessonPageEditorSlice';
addLessonPageContent, import { addLesson, updateLessonPreviewThumbnail, updateLessonPreviewTitle, updateLessonState } from 'features/Lessons/lessonsSlice';
deleteLessonPageContent, import { addTeamMember, deleteTeamMember, updateTeamMemberRole } from 'features/Team/teamSlice';
updateLessonPageContent, import { setProfilePictureUrl } from 'features/UserProfile/userProfileSlice';
updateLessonPageContentPosition,
} from "features/Lessons/LessonPage/lessonPageSlice";
import {
addLessonContent,
deleteLessonContent,
setLessonThumbnailTitle,
setLessonThumbnailUrl,
setPageEditorLessonState,
updateLessonContent,
updateLessonContentPosition,
} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice";
import {
addLesson,
updateLessonPreviewThumbnail,
updateLessonPreviewTitle,
updateLessonState,
} from "features/Lessons/lessonsSlice";
import {
addTeamMember,
deleteTeamMember,
updateTeamMemberRole,
} from "features/Team/teamSlice";
import { setProfilePictureUrl } from "features/UserProfile/userProfileSlice";
interface WebSocketMessage { interface WebSocketMessage {
Cmd: number; Cmd: number;
Body: any; Body: any;
} }
class WebSocketService { class WebSocketService {
private url: string; private url: string;
private socket: WebSocket | null = null; private socket: WebSocket | null = null;
private reconnectInterval: number = 2000; // in ms private reconnectInterval: number = 2000; // in ms
private offlineQueue: WebSocketMessage[] = []; private offlineQueue: WebSocketMessage[] = [];
private firstConnect: boolean = true; private firstConnect: boolean = true;
private messageHandler: private messageHandler: ((message: WebSocketMessage, dispatch: Dispatch) => void) | null = null;
| ((message: WebSocketMessage, dispatch: Dispatch) => void)
| null = null;
constructor(url: string) { constructor(url: string) {
this.url = url; this.url = url;
}
private dispatch: Dispatch | null = null;
public connect(): void {
this.socket = new WebSocket(
`${this.url}?auth=${localStorage.getItem(
"session"
)}&bts=${BrowserTabSession}`
);
this.socket.onopen = () => {
console.log("WebSocket connected", this.firstConnect);
// Send all messages from the offline queue
this.offlineQueue.forEach((message) => this.send(message));
this.offlineQueue = [];
// Dispatch event to notify that the WebSocket connection is established
if (!this.firstConnect) {
document.dispatchEvent(webSocketConnectionEvent);
} else {
this.firstConnect = false;
}
};
this.socket.onmessage = (event: MessageEvent) => {
const data: WebSocketMessage = JSON.parse(event.data);
if (this.messageHandler) {
this.messageHandler(data, this.dispatch!);
} else {
console.error("No handler defined for WebSocket messages");
}
};
this.socket.onclose = () => {
console.log("WebSocket disconnected. Reconnecting...");
setTimeout(() => this.connect(), this.reconnectInterval);
};
this.socket.onerror = (error: Event) => {
console.error("WebSocket error:", error);
};
}
public setHandler(
handler: (message: WebSocketMessage, dispatch: Dispatch) => void,
dispatch: Dispatch
): void {
this.messageHandler = handler;
this.dispatch = dispatch;
}
public send(message: WebSocketMessage): void {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(
JSON.stringify({
Cmd: message.Cmd,
Body: message.Body,
})
);
} else {
this.offlineQueue.push(message);
} }
}
public disconnect(): void { private dispatch: Dispatch | null = null;
if (this.socket) {
this.socket.close(); public connect(): void {
this.socket = new WebSocket(`${this.url}?auth=${localStorage.getItem('session')}&bts=${BrowserTabSession}`);
this.socket.onopen = () => {
console.log('WebSocket connected', this.firstConnect);
// Send all messages from the offline queue
this.offlineQueue.forEach((message) => this.send(message));
this.offlineQueue = [];
// Dispatch event to notify that the WebSocket connection is established
if (!this.firstConnect) {
document.dispatchEvent(webSocketConnectionEvent);
} else {
this.firstConnect = false;
}
};
this.socket.onmessage = (event: MessageEvent) => {
const data: WebSocketMessage = JSON.parse(event.data);
if (this.messageHandler) {
this.messageHandler(data, this.dispatch!);
} else {
console.error('No handler defined for WebSocket messages');
}
};
this.socket.onclose = () => {
console.log('WebSocket disconnected. Reconnecting...');
setTimeout(() => this.connect(), this.reconnectInterval);
};
this.socket.onerror = (error: Event) => {
console.error('WebSocket error:', error);
};
}
public setHandler(handler: (message: WebSocketMessage, dispatch: Dispatch) => void, dispatch: Dispatch): void {
this.messageHandler = handler;
this.dispatch = dispatch;
}
public send(message: WebSocketMessage): void {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(
JSON.stringify({
Cmd: message.Cmd,
Body: message.Body,
})
);
} else {
this.offlineQueue.push(message);
}
}
public disconnect(): void {
if (this.socket) {
this.socket.close();
}
} }
}
} }
const webSocketConnectionEventName = "WebSocketConnectionEvent"; const webSocketConnectionEventName = 'WebSocketConnectionEvent';
const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, { const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, {
detail: "wsReconnect", detail: 'wsReconnect',
}); });
export function addWebSocketReconnectListener(callback: () => void): void { export function addWebSocketReconnectListener(callback: () => void): void {
document.addEventListener(webSocketConnectionEventName, callback); document.addEventListener(webSocketConnectionEventName, callback);
} }
export function removeWebSocketReconnectListener(callback: () => void): void { export function removeWebSocketReconnectListener(callback: () => void): void {
document.removeEventListener(webSocketConnectionEventName, callback); document.removeEventListener(webSocketConnectionEventName, callback);
} }
const webSocketService = new WebSocketService(Constants.WS_ADDRESS); const webSocketService = new WebSocketService(Constants.WS_ADDRESS);
export default webSocketService; export default webSocketService;
export function WebSocketMessageHandler( export function WebSocketMessageHandler(message: WebSocketMessage, dispatch: Dispatch) {
message: WebSocketMessage, const { Cmd, Body } = message;
dispatch: Dispatch
) {
const { Cmd, Body } = message;
console.log("WebSocketMessageHandler", Cmd, Body); console.log('WebSocketMessageHandler', Cmd, Body);
switch (Cmd) { switch (Cmd) {
case WebSocketReceivedMessagesCmds.SettingsUpdated: case WebSocketReceivedMessagesCmds.SettingsUpdated:
dispatch(setPrimaryColor(Body.PrimaryColor)); dispatch(setPrimaryColor(Body.PrimaryColor));
break; break;
case WebSocketReceivedMessagesCmds.SettingsUpdatedLogo: case WebSocketReceivedMessagesCmds.SettingsUpdatedLogo:
dispatch(setLogoUrl(Body)); dispatch(setLogoUrl(Body));
break; break;
case WebSocketReceivedMessagesCmds.SettingsUpdatedBanner: case WebSocketReceivedMessagesCmds.SettingsUpdatedBanner:
dispatch(setBannerUrl(Body)); dispatch(setBannerUrl(Body));
break; break;
case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain: case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain:
localStorage.removeItem("session"); localStorage.removeItem('session');
window.location.href = `${ window.location.href = `${window.location.protocol}//${Body}.${window.location.hostname.split('.').slice(1).join('.')}`;
window.location.protocol break;
}//${Body}.${window.location.hostname.split(".").slice(1).join(".")}`; case WebSocketReceivedMessagesCmds.TeamAddedMember:
break; dispatch(addTeamMember(Body));
case WebSocketReceivedMessagesCmds.TeamAddedMember: break;
dispatch(addTeamMember(Body)); case WebSocketReceivedMessagesCmds.TeamUpdatedMemberRole:
break; dispatch(updateTeamMemberRole(Body));
case WebSocketReceivedMessagesCmds.TeamUpdatedMemberRole: break;
dispatch(updateTeamMemberRole(Body)); case WebSocketReceivedMessagesCmds.TeamDeletedMember:
break; dispatch(deleteTeamMember(Body));
case WebSocketReceivedMessagesCmds.TeamDeletedMember: break;
dispatch(deleteTeamMember(Body)); case WebSocketReceivedMessagesCmds.LessonCreated:
break; dispatch(addLesson(Body));
case WebSocketReceivedMessagesCmds.LessonCreated: break;
dispatch(addLesson(Body)); case WebSocketReceivedMessagesCmds.LessonPreviewTitleUpdated:
break; dispatch(updateLessonPreviewTitle(Body));
case WebSocketReceivedMessagesCmds.LessonPreviewTitleUpdated:
dispatch(updateLessonPreviewTitle(Body));
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
dispatch(setLessonThumbnailTitle(Body.Title)); dispatch(setLessonThumbnailTitle(Body.Title));
} }
break; break;
case WebSocketReceivedMessagesCmds.LessonPreviewThumbnailUpdated: case WebSocketReceivedMessagesCmds.LessonPreviewThumbnailUpdated:
dispatch(updateLessonPreviewThumbnail(Body)); dispatch(updateLessonPreviewThumbnail(Body));
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
dispatch(setLessonThumbnailUrl(Body.ThumbnailUrl)); dispatch(setLessonThumbnailUrl(Body.ThumbnailUrl));
} }
break; break;
case WebSocketReceivedMessagesCmds.LessonStateUpdated: case WebSocketReceivedMessagesCmds.LessonStateUpdated:
dispatch(updateLessonState(Body)); dispatch(updateLessonState(Body));
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
dispatch(setPageEditorLessonState(Body.State)); dispatch(setPageEditorLessonState(Body.State));
} }
break; break;
case WebSocketReceivedMessagesCmds.LessonAddedContent: case WebSocketReceivedMessagesCmds.LessonAddedContent:
if (Body.LessonId === store.getState().lessonPage.currentLessonId) { if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
dispatch(addLessonPageContent(Body)); dispatch(addLessonPageContent(Body));
} }
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
dispatch(addLessonContent(Body)); dispatch(addLessonContent(Body));
} }
break; break;
case WebSocketReceivedMessagesCmds.LessonDeletedContent: case WebSocketReceivedMessagesCmds.LessonDeletedContent:
if (Body.LessonId === store.getState().lessonPage.currentLessonId) { if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
dispatch(deleteLessonPageContent(Body.ContentId)); dispatch(deleteLessonPageContent(Body.ContentId));
} }
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
dispatch(deleteLessonContent(Body.ContentId)); dispatch(deleteLessonContent(Body.ContentId));
} }
break; break;
case WebSocketReceivedMessagesCmds.LessonContentUpdated: case WebSocketReceivedMessagesCmds.LessonContentUpdated:
if (Body.LessonId === store.getState().lessonPage.currentLessonId) { if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
dispatch( dispatch(
updateLessonPageContent({ updateLessonPageContent({
id: Body.ContentId, id: Body.ContentId,
data: Body.Data, data: Body.Data,
}) })
); );
} }
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
dispatch( dispatch(
updateLessonContent({ updateLessonContent({
id: Body.ContentId, id: Body.ContentId,
data: Body.Data, data: Body.Data,
}) })
); );
} }
break; break;
case WebSocketReceivedMessagesCmds.LessonContentUpdatedPosition: case WebSocketReceivedMessagesCmds.LessonContentUpdatedPosition:
if (Body.LessonId === store.getState().lessonPage.currentLessonId) { if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
dispatch( dispatch(
updateLessonPageContentPosition({ updateLessonPageContentPosition({
contentId: Body.ContentId, contentId: Body.ContentId,
position: Body.Position, position: Body.Position,
}) })
); );
} }
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
dispatch( dispatch(
updateLessonContentPosition({ updateLessonContentPosition({
contentId: Body.ContentId, contentId: Body.ContentId,
position: Body.Position, position: Body.Position,
}) })
); );
} }
break; break;
case WebSocketReceivedMessagesCmds.LessonContentFileUpdated: case WebSocketReceivedMessagesCmds.LessonContentFileUpdated:
if (Body.LessonId === store.getState().lessonPage.currentLessonId) { if (Body.LessonId === store.getState().lessonPage.currentLessonId) {
dispatch( dispatch(
updateLessonPageContent({ updateLessonPageContent({
id: Body.ContentId, id: Body.ContentId,
data: Body.Data, data: Body.Data,
}) })
); );
} }
if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) { if (Body.LessonId === store.getState().lessonPageEditor.currentLessonId) {
dispatch( dispatch(
updateLessonContent({ updateLessonContent({
id: Body.ContentId, id: Body.ContentId,
data: Body.Data, data: Body.Data,
}) })
); );
} }
break; break;
case WebSocketReceivedMessagesCmds.UserProfilePictureUpdated: case WebSocketReceivedMessagesCmds.UserProfilePictureUpdated:
dispatch(setProfilePictureUrl(Body)); dispatch(setProfilePictureUrl(Body));
break; dispatch(setUserProfilePictureUrl(Body));
default: break;
console.error("Unknown message type:", Cmd); default:
} console.error('Unknown message type:', Cmd);
}
} }

View File

@ -1,55 +1,55 @@
export interface Lesson { export interface Lesson {
Id: string; Id: string;
State: number; State: number;
Title: string; Title: string;
ThumbnailUrl: string; ThumbnailUrl: string;
CreatorUserId: string; CreatorUserId: string;
CreatedAt: string; CreatedAt: string;
} }
export enum LessonState { export enum LessonState {
Published = 1, Published = 1,
Draft = 2, Draft = 2,
} }
// used for the preview card on /lessions page and on the lesson editor // used for the preview card on /lessions page and on the lesson editor
export interface LessonSettings { export interface LessonSettings {
Title: string; Title: string;
ThumbnailUrl: string; ThumbnailUrl: string;
State?: LessonState; State?: LessonState;
} }
// used on lesson page and on the lesson editor // used on lesson page and on the lesson editor
export interface LessonContent { export interface LessonContent {
Id: string; Id: string;
Page: number; Page: number;
Position: number; Position: number;
Type: number; Type: number;
Data: string; Data: string;
} }
export interface UpdateLessonPreviewThumbnail { export interface UpdateLessonPreviewThumbnail {
lessonId: string; lessonId: string;
formData: FormData; formData: FormData;
} }
export interface LessonQuestion { export interface LessonQuestion {
Id: string; Id: string;
LessionId: string; LessionId: string;
Question: string; Question: string;
Likes: number; Likes: number;
CreatorUserId: string; CreatorUserId: string;
CreatedAt: string; CreatedAt: string;
UpdatedAt: string; UpdatedAt: string;
} }
export interface LessonQuestionReply { export interface LessonQuestionReply {
Id: string; Id: string;
QuestionId: string; QuestionId: string;
ReplyId?: string; ReplyId?: string;
Reply: string; Reply: string;
Likes: number; Likes: number;
CreatorUserId: string; CreatorUserId: string;
CreatedAt: string; CreatedAt: string;
UpdatedAt: string; UpdatedAt: string;
} }

View File

@ -1,32 +1,33 @@
export interface TeamMember { export interface TeamMember {
Id: string; Id: string;
FirstName: string; FirstName: string;
LastName: string; LastName: string;
Email: string; Email: string;
RoleId: string; RoleId: string;
Online: boolean; ProfilePictureUrl: string;
Online: boolean;
} }
export interface OrganizationSettings { export interface OrganizationSettings {
Subdomain: string; Subdomain: string;
CompanyName: string; CompanyName: string;
PrimaryColor: string; PrimaryColor: string;
LogoUrl: string; LogoUrl: string;
BannerUrl: string; BannerUrl: string;
} }
interface RoleUser { interface RoleUser {
FirstName: string; FirstName: string;
LastName: string; LastName: string;
ProfilePictureUrl: string; ProfilePictureUrl: string;
} }
export interface Role { export interface Role {
Id: string; Id: string;
Permissions: number[]; Permissions: number[];
Users: RoleUser[]; Users: RoleUser[];
} }
export interface Roles { export interface Roles {
Roles: Role[]; Roles: Role[];
} }

View File

@ -2,6 +2,7 @@ import React from "react";
import { FloatButton } from "antd"; import { FloatButton } from "antd";
import { CommentOutlined } from "@ant-design/icons"; import { CommentOutlined } from "@ant-design/icons";
import { DeepChat } from "deep-chat-react"; import { DeepChat } from "deep-chat-react";
import { getUserSessionFromLocalStorage } from "core/utils/utils";
function AiChat() { function AiChat() {
const [visible, setVisible] = React.useState(false); const [visible, setVisible] = React.useState(false);
@ -30,20 +31,17 @@ function AiChat() {
bottom: 0, bottom: 0,
position: "absolute", position: "absolute",
}} }}
history={[ history={[{ text: "Stell mir Fragen :)", role: "ai" }]}
{ text: "Show me a modern city", role: "user" }, connect={{
{ url: "/api/chat/v1/prompt/",
files: [ method: "POST",
{ headers: {
src: "https://test.ex.umbach.dev/api/statico/809fe37e-8c41-4a44-98d1-d9247affd531/67c763b6-ea67-4b49-9621-2f78b85eb180.png", "X-Authorization": getUserSessionFromLocalStorage() || "",
type: "image",
},
],
role: "ai",
}, },
{ text: "Whats on your mind?", role: "user" }, }}
{ text: "Peace and tranquility", role: "ai" }, onMessage={async (message) => {
]} console.log("onMessagee", message);
}}
></DeepChat> ></DeepChat>
</div> </div>
) : null} ) : null}

View File

@ -156,7 +156,7 @@ function GeneralCard({
} }
debounceRef.current = setTimeout(() => { debounceRef.current = setTimeout(() => {
dispatch(setPrimaryColor(color.toHexString())); dispatch(setPrimaryColor(color.toHexString().split("#")[1]));
}, 600); }, 600);
}} }}
/> />
@ -218,12 +218,9 @@ function MediaCard({
<MyUpload <MyUpload
action="/organization/file/banner" action="/organization/file/banner"
onChange={(info) => { onChange={(info) => {
console.log("Banner updated1!", info.file.status);
if (info.file.status === "done" && info.file.response.Data) { if (info.file.status === "done" && info.file.response.Data) {
dispatch(setBannerUrl(info.file.response.Data)); dispatch(setBannerUrl(info.file.response.Data));
console.log("Banner updated!");
success("Banner updated successfully!"); success("Banner updated successfully!");
} }
}} }}
@ -337,14 +334,7 @@ function SubdomainCard({
</p> </p>
</Modal> </Modal>
<Form <Form form={form} layout="vertical" requiredMark={false}>
form={form}
layout="vertical"
requiredMark={false}
onFinish={(values) => {
console.log(values);
}}
>
<MyMiddleCard <MyMiddleCard
title="Subdomain" title="Subdomain"
loading={isLoading} loading={isLoading}

View File

@ -21,6 +21,7 @@ import {
removeWebSocketReconnectListener, removeWebSocketReconnectListener,
} from "core/services/websocketService"; } from "core/services/websocketService";
import { useMessage } from "core/context/MessageContext"; import { useMessage } from "core/context/MessageContext";
import MyUserAvatar from "shared/components/MyUserAvatar";
const TeamList: React.FC = () => { const TeamList: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -146,7 +147,17 @@ const TeamList: React.FC = () => {
dataTeamMembers.forEach((item) => { dataTeamMembers.forEach((item) => {
items.push({ items.push({
key: item.Id, key: item.Id,
firstName: item.FirstName, firstName: (
<Space>
<MyUserAvatar
size={42}
profilePictureUrl={item.ProfilePictureUrl}
firstName={item.FirstName}
disableCursorPointer
/>
<span>{item.FirstName}</span>
</Space>
),
lastName: item.LastName, lastName: item.LastName,
email: item.Email, email: item.Email,
role: tmpRoleNames[item.RoleId], role: tmpRoleNames[item.RoleId],

View File

@ -35,6 +35,8 @@ import { tmpRoleNames } from "features/Roles";
import MyErrorResult from "shared/components/MyResult"; import MyErrorResult from "shared/components/MyResult";
import MyUpload from "shared/components/MyUpload"; import MyUpload from "shared/components/MyUpload";
import { useMessage } from "core/context/MessageContext"; import { useMessage } from "core/context/MessageContext";
import MyUserAvatar from "shared/components/MyUserAvatar";
import { setUserProfilePictureUrl } from "core/reducers/appSlice";
export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) { export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -122,6 +124,9 @@ export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
success("Profile picture updated successfully"); success("Profile picture updated successfully");
dispatch(setProfilePictureUrl(info.file.response.Data)); dispatch(setProfilePictureUrl(info.file.response.Data));
dispatch(
setUserProfilePictureUrl(info.file.response.Data)
);
} }
}} }}
imgCropProps={{ imgCropProps={{
@ -129,16 +134,7 @@ export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
children: <></>, children: <></>,
}} }}
> >
{dataProfilePictureUrl === "" ? ( <MyUserAvatar profilePictureUrl={dataProfilePictureUrl} />
<Avatar size={56} style={{ backgroundColor: "#1677ff" }}>
{dataFirstName.charAt(0).toUpperCase()}
</Avatar>
) : (
<Avatar
src={`${Constants.STATIC_CONTENT_ADDRESS}/${dataProfilePictureUrl}`}
size={56}
/>
)}
</MyUpload> </MyUpload>
} }
title={`${dataFirstName} ${dataLastName}`} title={`${dataFirstName} ${dataLastName}`}

View File

@ -0,0 +1,34 @@
import { Avatar } from 'antd';
import { Constants } from 'core/utils/utils';
import { UserOutlined } from '@ant-design/icons';
import { primaryColor } from 'core/reducers/appSlice';
import { useSelector } from 'react-redux';
interface MyUserAvatarProps {
size?: number;
firstName?: string;
profilePictureUrl: string;
disableCursorPointer?: boolean;
}
const MyUserAvatar: React.FC<MyUserAvatarProps> = ({ size = 56, firstName, profilePictureUrl, disableCursorPointer }) => {
const appPrimaryColor = useSelector(primaryColor);
const defaultStyle = disableCursorPointer === undefined ? { cursor: 'pointer' } : {};
const isProfilePictureEmpty = profilePictureUrl === '';
const avatarContent = isProfilePictureEmpty && firstName !== undefined ? firstName.charAt(0) : undefined;
const iconContent = isProfilePictureEmpty && firstName === undefined ? <UserOutlined /> : undefined;
const avatarSrc = isProfilePictureEmpty ? undefined : `${Constants.STATIC_CONTENT_ADDRESS}/${profilePictureUrl}`;
const avatarStyle = isProfilePictureEmpty ? { ...defaultStyle, backgroundColor: `#${appPrimaryColor}` } : defaultStyle;
return (
<div style={{ userSelect: 'none' }}>
<Avatar size={size} style={avatarStyle} src={avatarSrc} icon={iconContent}>
{avatarContent}
</Avatar>
</div>
);
};
export default MyUserAvatar;