Skip to content

Commit 61acd93

Browse files
committed
#1618 | 1) Added WelcomeModal component 2) Refactor DifyChatbot to use Redux state directly instead of prop callbacks 3) Move chat initialization logic to Redux initialState with localStorage check
1 parent 87a1eea commit 61acd93

File tree

6 files changed

+239
-52
lines changed

6 files changed

+239
-52
lines changed

src/common/WelcomeModal.jsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {
2+
Dialog,
3+
DialogContent,
4+
Typography,
5+
IconButton,
6+
Box,
7+
Card,
8+
CardContent,
9+
Grid,
10+
} from "@mui/material";
11+
import { Close as CloseIcon, Description, SmartToy } from "@mui/icons-material";
12+
import { styled } from "@mui/material/styles";
13+
14+
const StyledDialog = styled(Dialog)(({ theme }) => ({
15+
"& .MuiDialog-paper": {
16+
borderRadius: "1rem",
17+
maxWidth: "62.5rem",
18+
width: "90%",
19+
margin: "auto",
20+
padding: 0,
21+
},
22+
}));
23+
24+
const HeaderContainer = styled(Box)({
25+
display: "flex",
26+
justifyContent: "space-between",
27+
alignItems: "flex-start",
28+
padding: "2rem 2rem 1.5rem",
29+
position: "relative",
30+
});
31+
32+
const CloseButton = styled(IconButton)({
33+
position: "absolute",
34+
top: "1rem",
35+
right: "1rem",
36+
color: "#666",
37+
"&:hover": {
38+
backgroundColor: "rgba(0, 0, 0, 0.04)",
39+
},
40+
});
41+
42+
const StyledCard = styled(Card)({
43+
height: "17.5rem",
44+
width: "17.5rem",
45+
display: "flex",
46+
flexDirection: "column",
47+
borderRadius: "0.75rem",
48+
border: "1px solid #e0e0e0",
49+
transition: "box-shadow 0.3s ease-in-out, transform 0.2s ease-in-out",
50+
cursor: "pointer",
51+
"&:hover": {
52+
boxShadow: "0 0.25rem 1.25rem rgba(0, 0, 0, 0.1)",
53+
transform: "translateY(-0.125rem)",
54+
},
55+
});
56+
57+
const CardContentWrapper = styled(CardContent)({
58+
display: "flex",
59+
flexDirection: "column",
60+
alignItems: "center",
61+
justifyContent: "center",
62+
textAlign: "center",
63+
padding: "2rem 1.5rem",
64+
height: "100%",
65+
});
66+
67+
const IconContainer = styled(Box)(({ theme }) => ({
68+
marginBottom: "1.5rem",
69+
"& svg": {
70+
fontSize: "4rem",
71+
color: theme.palette.primary.main,
72+
},
73+
}));
74+
75+
const WelcomeModal = ({ open, onClose, onOptionSelect }) => {
76+
const handleOptionClick = (option) => {
77+
onClose();
78+
// Delay action dispatch to allow modal to close first
79+
if (onOptionSelect) {
80+
setTimeout(() => {
81+
onOptionSelect(option);
82+
}, 100);
83+
}
84+
};
85+
86+
return (
87+
<StyledDialog open={open} onClose={onClose} maxWidth={false}>
88+
<HeaderContainer>
89+
<Box>
90+
<Typography variant="h4" sx={{ fontWeight: 500, marginBottom: 1 }}>
91+
Welcome to Avni!
92+
</Typography>
93+
<Typography variant="subtitle1" sx={{ fontSize: "1rem" }}>
94+
Your one stop solution to all your program needs
95+
</Typography>
96+
</Box>
97+
<CloseButton onClick={onClose}>
98+
<CloseIcon />
99+
</CloseButton>
100+
</HeaderContainer>
101+
102+
<DialogContent sx={{ padding: "0 2rem 2rem" }}>
103+
<Grid container spacing={3} justifyContent="center">
104+
<Grid item xs={12} md={4}>
105+
<StyledCard onClick={() => handleOptionClick("templates")}>
106+
<CardContentWrapper>
107+
<IconContainer>
108+
<Description />
109+
</IconContainer>
110+
<Typography
111+
variant="h6"
112+
sx={{ fontWeight: 500, marginBottom: 2 }}
113+
>
114+
Explore Avni Templates
115+
</Typography>
116+
<Typography variant="body2" sx={{ lineHeight: 1.5 }}>
117+
Learn about Avni through our varied use cases
118+
</Typography>
119+
</CardContentWrapper>
120+
</StyledCard>
121+
</Grid>
122+
123+
<Grid item xs={12} md={4}>
124+
<StyledCard onClick={() => handleOptionClick("ai")}>
125+
<CardContentWrapper>
126+
<IconContainer>
127+
<SmartToy />
128+
</IconContainer>
129+
<Typography
130+
variant="h6"
131+
sx={{ fontWeight: 500, marginBottom: 2 }}
132+
>
133+
Explore Using AI
134+
</Typography>
135+
<Typography variant="body2" sx={{ lineHeight: 1.5 }}>
136+
Design your programs from scratch using the App Designer
137+
</Typography>
138+
</CardContentWrapper>
139+
</StyledCard>
140+
</Grid>
141+
</Grid>
142+
</DialogContent>
143+
</StyledDialog>
144+
);
145+
};
146+
147+
export default WelcomeModal;

src/common/components/DifyChatbot.jsx

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
import { useEffect, useState } from "react";
2-
import { useSelector } from "react-redux";
2+
import { useSelector, useDispatch } from "react-redux";
33
import { Box, Slide, useTheme, IconButton } from "@mui/material";
44
import { ChevronRight } from "@mui/icons-material";
55
import IdpDetails from "../../rootApp/security/IdpDetails.ts";
6-
const DifyChatbot = ({ onChatToggle }) => {
7-
// Check if this is first time after login
8-
const [isPanelOpen, setIsPanelOpen] = useState(() => {
9-
const hasSeenChatbot = localStorage.getItem("avni-chatbot-seen");
10-
return !hasSeenChatbot; // Open only if user hasn't seen it before
11-
});
6+
import { setChatOpen } from "../../rootApp/ducks";
7+
const DifyChatbot = () => {
8+
const dispatch = useDispatch();
129
const theme = useTheme();
1310

1411
const [aiConfig, setAiConfig] = useState(null);
1512
const [configError, setConfigError] = useState(false);
1613

17-
// Notify parent when panel state changes
18-
useEffect(() => {
19-
onChatToggle?.(isPanelOpen);
20-
}, [isPanelOpen, onChatToggle]);
14+
// Get user and organization data from Redux state
15+
const userInfo = useSelector((state) => state.app?.userInfo);
16+
const organisation = useSelector((state) => state.app?.organisation);
17+
const authSession = useSelector((state) => state.app?.authSession);
18+
const isChatOpen = useSelector((state) => state.app?.isChatOpen);
2119

2220
// Fetch AI assistant configuration
2321
useEffect(() => {
@@ -39,11 +37,6 @@ const DifyChatbot = ({ onChatToggle }) => {
3937

4038
fetchAiConfig();
4139
}, []);
42-
43-
// Get user and organization data from Redux state
44-
const userInfo = useSelector((state) => state.app?.userInfo);
45-
const organisation = useSelector((state) => state.app?.organisation);
46-
const authSession = useSelector((state) => state.app?.authSession);
4740
const authToken = localStorage.getItem(IdpDetails.AuthTokenName);
4841
const avni_mcp_server_url = aiConfig?.mcp_server_url || "";
4942

@@ -148,7 +141,7 @@ const DifyChatbot = ({ onChatToggle }) => {
148141
z-index: 1201;
149142
box-shadow: ${theme.shadows[4]};
150143
transition: all 0.3s ease;
151-
display: ${isPanelOpen ? "none" : "flex"};
144+
display: ${isChatOpen ? "none" : "flex"};
152145
align-items: center;
153146
justify-content: center;
154147
`;
@@ -164,7 +157,7 @@ const DifyChatbot = ({ onChatToggle }) => {
164157
};
165158

166159
chatButton.onclick = () => {
167-
setIsPanelOpen(true);
160+
dispatch(setChatOpen(true));
168161
};
169162

170163
document.body.appendChild(chatButton);
@@ -179,7 +172,7 @@ const DifyChatbot = ({ onChatToggle }) => {
179172
const style = document.getElementById("dify-chat-animations");
180173
if (style) style.remove();
181174
};
182-
}, [isPanelOpen, theme, aiConfig, configError]);
175+
}, [isChatOpen, theme, aiConfig, configError, dispatch]);
183176

184177
// Build chatbot URL with user context
185178
const buildChatUrl = () => {
@@ -214,7 +207,7 @@ const DifyChatbot = ({ onChatToggle }) => {
214207
return (
215208
<>
216209
{/* Embedded Right Panel */}
217-
<Slide direction="left" in={isPanelOpen} mountOnEnter unmountOnExit>
210+
<Slide direction="left" in={isChatOpen} mountOnEnter unmountOnExit>
218211
<Box
219212
sx={{
220213
position: "fixed",
@@ -233,7 +226,7 @@ const DifyChatbot = ({ onChatToggle }) => {
233226
{/* Close Button */}
234227
<IconButton
235228
onClick={() => {
236-
setIsPanelOpen(false);
229+
dispatch(setChatOpen(false));
237230
// Mark chatbot as seen so it won't auto-open again
238231
localStorage.setItem("avni-chatbot-seen", "true");
239232
}}

src/rootApp/App.jsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect } from "react";
22
import { useSelector, useDispatch } from "react-redux";
33
import { Box } from "@mui/material";
44
import Routes from "./Routes";
5-
import { getUserInfo, setChatOpen } from "./ducks";
5+
import { getUserInfo } from "./ducks";
66
import IdpDetails from "./security/IdpDetails";
77
import { httpClient } from "../common/utils/httpClient";
88
import Footer from "../common/components/Footer";
99
import DifyChatbot from "../common/components/DifyChatbot";
1010

1111
const App = () => {
1212
const dispatch = useDispatch();
13-
const [isChatOpen, setIsChatOpen] = useState(false);
14-
15-
// Dispatch chat state to Redux when it changes
16-
useEffect(() => {
17-
dispatch(setChatOpen(isChatOpen));
18-
}, [isChatOpen, dispatch]);
13+
const isChatOpen = useSelector((state) => state.app.isChatOpen);
1914

2015
const appInitialised = useSelector(
2116
(state) => state.app?.appInitialised || false,
@@ -69,7 +64,7 @@ const App = () => {
6964
</Box>
7065
</Box>
7166
<Footer />
72-
<DifyChatbot onChatToggle={setIsChatOpen} />
67+
<DifyChatbot />
7368
</Box>
7469
);
7570
};

src/rootApp/ducks.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const types = {
2020
LOGOUT: "app/LOGOUT",
2121
INIT_GENERIC_CONFIG: "app/INIT_GENERIC_CONFIG",
2222
SET_CHAT_OPEN: "app/SET_CHAT_OPEN",
23+
SET_IS_NEW_IMPLEMENTATION: "app/SET_IS_NEW_IMPLEMENTATION",
2324
};
2425

2526
export const getAdminOrgs = () => ({
@@ -81,6 +82,11 @@ export const setChatOpen = (isChatOpen) => ({
8182
payload: isChatOpen,
8283
});
8384

85+
export const setIsNewImplementation = (isNewImplementation) => ({
86+
type: types.SET_IS_NEW_IMPLEMENTATION,
87+
payload: isNewImplementation,
88+
});
89+
8490
const initialState = {
8591
idpDetails: undefined,
8692
authSession: new NoAuthSession(),
@@ -96,7 +102,11 @@ const initialState = {
96102
copilotEnabled: false,
97103
avniMcpServerUrl: "http://localhost:8023",
98104
},
99-
isChatOpen: false,
105+
isChatOpen: (() => {
106+
const hasSeenChatbot = localStorage.getItem("avni-chatbot-seen");
107+
return !hasSeenChatbot; // Open if user hasn't seen it before
108+
})(),
109+
isNewImplementation: false,
100110
};
101111

102112
// reducer
@@ -184,6 +194,12 @@ export default function (state = initialState, action) {
184194
isChatOpen: action.payload,
185195
};
186196
}
197+
case types.SET_IS_NEW_IMPLEMENTATION: {
198+
return {
199+
...state,
200+
isNewImplementation: action.payload,
201+
};
202+
}
187203
default:
188204
if (_.get(action, "payload.error")) console.log(action.payload.error);
189205
return state;

src/rootApp/saga.jsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
setAdminOrgs,
66
setOrganisationConfig,
77
setUserInfo,
8-
types
8+
setIsNewImplementation,
9+
types,
910
} from "./ducks";
1011
import { httpClient as http } from "common/utils/httpClient";
1112
import i18n from "i18next";
@@ -17,15 +18,15 @@ import { authProvider } from "adminApp/react-admin-config/authProvider";
1718

1819
const api = {
1920
fetchUserInfo: () =>
20-
http.fetchJson("/web/userInfo").then(response => response.json),
21+
http.fetchJson("/web/userInfo").then((response) => response.json),
2122
fetchAdminOrgs: () =>
22-
http.fetchJson("/organisation", {}, true).then(response => response.json),
23+
http.fetchJson("/organisation", {}, true).then((response) => response.json),
2324
fetchTranslations: () =>
24-
http.fetchJson("/web/translations").then(response => response.json),
25+
http.fetchJson("/web/translations").then((response) => response.json),
2526
fetchOrganisationConfig: () =>
26-
http.fetchJson("/web/organisationConfig").then(response => response.json),
27-
saveUserInfo: userInfo => http.post("/me", userInfo),
28-
logout: () => http.get("/web/logout")
27+
http.fetchJson("/web/organisationConfig").then((response) => response.json),
28+
saveUserInfo: (userInfo) => http.post("/me", userInfo),
29+
logout: () => http.get("/web/logout"),
2930
};
3031

3132
export function* onSetAuthSession() {
@@ -59,13 +60,20 @@ function* setUserDetails() {
5960

6061
const organisationName = get(userDetails, "organisationName", "");
6162
document.cookie = `IMPLEMENTATION-NAME=${encodeURIComponent(
62-
organisationName
63+
organisationName,
6364
)}; path=/; SameSite=Lax; Secure=true`;
6465

6566
if (!isEmpty(organisationName)) {
66-
const organisationConfig = yield call(api.fetchOrganisationConfig);
67+
const organisationConfigResponse = yield call(api.fetchOrganisationConfig);
6768
yield put(
68-
setOrganisationConfig(get(organisationConfig, "organisationConfig", {}))
69+
setOrganisationConfig(
70+
get(organisationConfigResponse, "organisationConfig", {}),
71+
),
72+
);
73+
yield put(
74+
setIsNewImplementation(
75+
organisationConfigResponse.isNewImplementation || false,
76+
),
6977
);
7078
}
7179

@@ -81,13 +89,13 @@ function* setUserDetails() {
8189
nsSeparator: false,
8290
interpolation: {
8391
escapeValue: false,
84-
formatSeparator: ","
92+
formatSeparator: ",",
8593
},
8694
react: {
87-
useSuspense: false
88-
}
95+
useSuspense: false,
96+
},
8997
};
90-
const init = params => i18nInstance.init(params);
98+
const init = (params) => i18nInstance.init(params);
9199
yield call(init, i18nParams);
92100

93101
yield put(sendInitComplete());

0 commit comments

Comments
 (0)