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,23 +1,13 @@
import { ConfigProvider, Layout, theme } from "antd";
import DashboardLayout from "./core/components/DashboardLayout";
import {
darkMode,
primaryColor,
setBannerUrl,
setLogoUrl,
setPrimaryColor,
setUserAuthenticated,
userAuthenticated,
} 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";
import { ConfigProvider, Layout, theme } from 'antd';
import DashboardLayout from './core/components/DashboardLayout';
import { darkMode, primaryColor, setBannerUrl, setLogoUrl, setPrimaryColor, setUserAuthenticated, userAuthenticated, setUserProfilePictureUrl } 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;
@ -29,10 +19,10 @@ export default function App() {
const primColor = useSelector(primaryColor);
console.info(
"\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: #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"
'\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: #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'
);
useEffect(() => {
@ -40,14 +30,15 @@ export default function App() {
(async () => {
try {
const response = await myFetch({
url: "/app",
method: "GET",
url: '/app',
method: 'GET',
});
if (response) {
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
dispatch(setLogoUrl(response.Organization.LogoUrl));
dispatch(setBannerUrl(response.Organization.BannerUrl));
dispatch(setUserProfilePictureUrl(response.User.ProfilePictureUrl));
dispatch(setUserAuthenticated(true));
webSocketService.connect();
@ -63,7 +54,7 @@ export default function App() {
}, [uAuthenticated]);
useEffect(() => {
if (!localStorage.getItem("session")) {
if (!localStorage.getItem('session')) {
dispatch(setUserAuthenticated(false));
} else {
dispatch(setUserAuthenticated(true));
@ -71,7 +62,7 @@ export default function App() {
}, [dispatch]);
return (
<Layout style={{ minHeight: "100vh" }}>
<Layout style={{ minHeight: '100vh' }}>
<ConfigProvider
theme={{
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
@ -80,15 +71,7 @@ export default function App() {
},
}}
>
<MessageProvider>
{uAuthenticated == null ? (
<MyCenteredSpin />
) : uAuthenticated ? (
<DashboardLayout />
) : (
<SignIn />
)}
</MessageProvider>
<MessageProvider>{uAuthenticated == null ? <MyCenteredSpin /> : uAuthenticated ? <DashboardLayout /> : <SignIn />}</MessageProvider>
</ConfigProvider>
</Layout>
);

View File

@ -1,42 +1,28 @@
import { Avatar, Dropdown, Flex } from "antd";
import {
isSideMenuCollapsed,
setIsSideMenuCollapsed,
} from "../SideMenu/sideMenuSlice";
import { useDispatch, useSelector } from "react-redux";
import {
EditOutlined,
EyeOutlined,
LeftOutlined,
LogoutOutlined,
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";
import { Avatar, Dropdown, Flex } from 'antd';
import { isSideMenuCollapsed, setIsSideMenuCollapsed } from '../SideMenu/sideMenuSlice';
import { useDispatch, useSelector } from 'react-redux';
import { EditOutlined, EyeOutlined, LeftOutlined, LogoutOutlined, 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';
import { userProfilePictureUrl } from 'core/reducers/appSlice';
import MyUserAvatar from 'shared/components/MyUserAvatar';
type HeaderBarProps = {
theme?: "light" | "dark";
theme?: 'light' | 'dark';
onView?: () => void;
onEdit?: () => void;
backTo?: string;
};
export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
export default function HeaderBar(props: HeaderBarProps = { theme: 'light' }) {
const dispatch = useDispatch();
const isCollpased = useSelector(isSideMenuCollapsed);
const isDarkMode = useSelector(darkMode);
const profilePictureUrl = useSelector(userProfilePictureUrl);
const navigate = useNavigate();
@ -51,26 +37,13 @@ export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
}}
>
<Flex align="center" gap={16}>
<div
className={
props.theme === "light"
? styles.containerLight
: styles.containerDark
}
style={{ borderRadius: 28, padding: 4 }}
>
<div className={props.theme === 'light' ? styles.containerLight : styles.containerDark} style={{ borderRadius: 28, padding: 4 }}>
{isCollpased ? (
<div
className={styles.iconContainer}
onClick={() => dispatch(setIsSideMenuCollapsed(false))}
>
<div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(false))}>
<MenuUnfoldOutlined className={styles.icon} />
</div>
) : (
<div
className={styles.iconContainer}
onClick={() => dispatch(setIsSideMenuCollapsed(true))}
>
<div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(true))}>
<MenuFoldOutlined className={styles.icon} />
</div>
)}
@ -88,9 +61,7 @@ export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
<Flex
align="center"
className={
props.theme === "light" ? styles.containerLight : styles.containerDark
}
className={props.theme === 'light' ? styles.containerLight : styles.containerDark}
style={{
borderRadius: 28,
paddingLeft: 6,
@ -113,50 +84,42 @@ export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
)}
{isDarkMode ? (
<div
className={styles.iconContainer}
onClick={() => dispatch(setDarkMode(false))}
>
<div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(false))}>
<SunOutlined className={styles.icon} />
</div>
) : (
<div
className={styles.iconContainer}
onClick={() => dispatch(setDarkMode(true))}
>
<div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(true))}>
<MoonOutlined className={styles.icon} />
</div>
)}
<Dropdown
overlayStyle={{ minWidth: 150 }}
trigger={["click"]}
trigger={['click']}
menu={{
items: [
{
key: "1",
label: "Profile",
key: '1',
label: 'Profile',
icon: <UserOutlined />,
onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS),
},
{
key: "2",
label: "Logout",
key: '2',
label: 'Logout',
icon: <LogoutOutlined />,
danger: true,
onClick: () => {
webSocketService.disconnect();
window.localStorage.removeItem("session");
window.location.href = "/";
window.localStorage.removeItem('session');
window.location.href = '/';
},
},
],
}}
>
<Avatar
size="default"
icon={<UserOutlined />}
style={{ cursor: "pointer" }}
/>
<div>
<MyUserAvatar size={34} profilePictureUrl={profilePictureUrl ? profilePictureUrl : ''} />
</div>
</Dropdown>
</Flex>
</Flex>

View File

@ -1,11 +1,12 @@
import { createSlice } from "@reduxjs/toolkit";
import { createSlice } from '@reduxjs/toolkit';
export const appSlice = createSlice({
name: "app",
name: 'app',
initialState: {
darkMode: false,
userAuthenticated: null,
primaryColor: "#111",
userProfilePictureUrl: null,
primaryColor: '#111',
logoUrl: null,
bannerUrl: null,
},
@ -16,6 +17,9 @@ export const appSlice = createSlice({
setUserAuthenticated: (state, action) => {
state.userAuthenticated = action.payload;
},
setUserProfilePictureUrl: (state, action) => {
state.userProfilePictureUrl = action.payload;
},
setPrimaryColor: (state, action) => {
state.primaryColor = action.payload;
},
@ -29,19 +33,13 @@ export const appSlice = createSlice({
selectors: {
darkMode: (state) => state.darkMode,
userAuthenticated: (state) => state.userAuthenticated,
userProfilePictureUrl: (state) => state.userProfilePictureUrl,
primaryColor: (state) => state.primaryColor,
logoUrl: (state) => state.logoUrl,
bannerUrl: (state) => state.bannerUrl,
},
});
export const {
setDarkMode,
setUserAuthenticated,
setPrimaryColor,
setLogoUrl,
setBannerUrl,
} = appSlice.actions;
export const { setDarkMode, setUserAuthenticated, setUserProfilePictureUrl, setPrimaryColor, setLogoUrl, setBannerUrl } = appSlice.actions;
export const { darkMode, userAuthenticated, primaryColor, logoUrl, bannerUrl } =
appSlice.selectors;
export const { darkMode, userAuthenticated, userProfilePictureUrl, primaryColor, logoUrl, bannerUrl } = appSlice.selectors;

View File

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

View File

@ -1,18 +1,9 @@
import { Dispatch } from "@reduxjs/toolkit";
import {
setBannerUrl,
setLogoUrl,
setPrimaryColor,
} 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 { 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 {
addLessonContent,
deleteLessonContent,
@ -21,19 +12,10 @@ import {
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";
} 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 {
Cmd: number;
@ -47,9 +29,7 @@ class WebSocketService {
private offlineQueue: WebSocketMessage[] = [];
private firstConnect: boolean = true;
private messageHandler:
| ((message: WebSocketMessage, dispatch: Dispatch) => void)
| null = null;
private messageHandler: ((message: WebSocketMessage, dispatch: Dispatch) => void) | null = null;
constructor(url: string) {
this.url = url;
@ -58,14 +38,10 @@ class WebSocketService {
private dispatch: Dispatch | null = null;
public connect(): void {
this.socket = new WebSocket(
`${this.url}?auth=${localStorage.getItem(
"session"
)}&bts=${BrowserTabSession}`
);
this.socket = new WebSocket(`${this.url}?auth=${localStorage.getItem('session')}&bts=${BrowserTabSession}`);
this.socket.onopen = () => {
console.log("WebSocket connected", this.firstConnect);
console.log('WebSocket connected', this.firstConnect);
// Send all messages from the offline queue
@ -87,24 +63,21 @@ class WebSocketService {
if (this.messageHandler) {
this.messageHandler(data, this.dispatch!);
} else {
console.error("No handler defined for WebSocket messages");
console.error('No handler defined for WebSocket messages');
}
};
this.socket.onclose = () => {
console.log("WebSocket disconnected. Reconnecting...");
console.log('WebSocket disconnected. Reconnecting...');
setTimeout(() => this.connect(), this.reconnectInterval);
};
this.socket.onerror = (error: Event) => {
console.error("WebSocket error:", error);
console.error('WebSocket error:', error);
};
}
public setHandler(
handler: (message: WebSocketMessage, dispatch: Dispatch) => void,
dispatch: Dispatch
): void {
public setHandler(handler: (message: WebSocketMessage, dispatch: Dispatch) => void, dispatch: Dispatch): void {
this.messageHandler = handler;
this.dispatch = dispatch;
}
@ -129,10 +102,10 @@ class WebSocketService {
}
}
const webSocketConnectionEventName = "WebSocketConnectionEvent";
const webSocketConnectionEventName = 'WebSocketConnectionEvent';
const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, {
detail: "wsReconnect",
detail: 'wsReconnect',
});
export function addWebSocketReconnectListener(callback: () => void): void {
@ -146,13 +119,10 @@ export function removeWebSocketReconnectListener(callback: () => void): void {
const webSocketService = new WebSocketService(Constants.WS_ADDRESS);
export default webSocketService;
export function WebSocketMessageHandler(
message: WebSocketMessage,
dispatch: Dispatch
) {
export function WebSocketMessageHandler(message: WebSocketMessage, dispatch: Dispatch) {
const { Cmd, Body } = message;
console.log("WebSocketMessageHandler", Cmd, Body);
console.log('WebSocketMessageHandler', Cmd, Body);
switch (Cmd) {
case WebSocketReceivedMessagesCmds.SettingsUpdated:
@ -165,11 +135,9 @@ export function WebSocketMessageHandler(
dispatch(setBannerUrl(Body));
break;
case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain:
localStorage.removeItem("session");
localStorage.removeItem('session');
window.location.href = `${
window.location.protocol
}//${Body}.${window.location.hostname.split(".").slice(1).join(".")}`;
window.location.href = `${window.location.protocol}//${Body}.${window.location.hostname.split('.').slice(1).join('.')}`;
break;
case WebSocketReceivedMessagesCmds.TeamAddedMember:
dispatch(addTeamMember(Body));
@ -279,8 +247,9 @@ export function WebSocketMessageHandler(
break;
case WebSocketReceivedMessagesCmds.UserProfilePictureUpdated:
dispatch(setProfilePictureUrl(Body));
dispatch(setUserProfilePictureUrl(Body));
break;
default:
console.error("Unknown message type:", Cmd);
console.error('Unknown message type:', Cmd);
}
}

View File

@ -4,6 +4,7 @@ export interface TeamMember {
LastName: string;
Email: string;
RoleId: string;
ProfilePictureUrl: string;
Online: boolean;
}

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import {
removeWebSocketReconnectListener,
} from "core/services/websocketService";
import { useMessage } from "core/context/MessageContext";
import MyUserAvatar from "shared/components/MyUserAvatar";
const TeamList: React.FC = () => {
const dispatch = useDispatch();
@ -146,7 +147,17 @@ const TeamList: React.FC = () => {
dataTeamMembers.forEach((item) => {
items.push({
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,
email: item.Email,
role: tmpRoleNames[item.RoleId],

View File

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