Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions next/components/organisms/chatbot/ChatbotAccessGate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import cookies from "js-cookie";
import { redirectToChatbotCheckout, clearClientSession } from "../../../utils";

async function clearAuthCookiesAndRedirectLogin(router) {
if (typeof window !== "undefined") {
localStorage.setItem("previousPath", window.location.href);
}
await clearClientSession();
router.replace("/user/login");
}

function hasUserCookie() {
const userRaw = cookies.get("userBD");
if (!userRaw || userRaw === "undefined") return false;
try {
JSON.parse(userRaw);
return true;
} catch {
return false;
}
}

export default function ChatbotAccessGate({ children }) {
const router = useRouter();
const [canEnter, setCanEnter] = useState(false);

useEffect(() => {
let cancelled = false;

async function checkAccess() {
if (typeof window === "undefined") return;
if (!hasUserCookie()) {
await clearAuthCookiesAndRedirectLogin(router);
return;
}
try {
const res = await fetch("/api/user/validateToken", {
method: "GET",
credentials: "same-origin"
});
const data = await res.json();
if (cancelled) return;
if (!res.ok || !data.success) {
await clearAuthCookiesAndRedirectLogin(router);
return;
}
if (!data.has_chatbot_access) {
await redirectToChatbotCheckout(router);
return;
}
setCanEnter(true);
} catch {
if (!cancelled) await clearAuthCookiesAndRedirectLogin(router);
}
}

checkAccess();
return () => {
cancelled = true;
};
}, [router]);

if (!canEnter) return null;
return children;
}
209 changes: 209 additions & 0 deletions next/components/organisms/chatbot/HelpContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { Box, Link, UnorderedList, ListItem } from "@chakra-ui/react";
import { Trans, useTranslation } from "next-i18next";
import { useRouter } from "next/router";
import TitleText from "../../atoms/Text/TitleText";
import BodyText from "../../atoms/Text/BodyText";
import LabelText from "../../atoms/Text/LabelText";
import DownloadIcon from "../../../public/img/icons/downloadIcon";
import ThumbUpIcon from "../../../public/img/icons/thumbUpIcon";
import ThumbDownIcon from "../../../public/img/icons/thumbDownIcon";
import { CopyIcon } from "../../../public/img/icons/copyIcon";

const DiscordUrlByLocale = {
pt: "https://discord.gg/huKWpsVYx4",
en: "https://discord.gg/tx57ek6zqQ",
es: "https://discord.gg/nNfQYcmrvM",
};

const WhatsAppCommunityUrl =
"https://chat.whatsapp.com/CLLFXb1ogPPDomCM6tQT22";

const SupportEmail = "suporte.bdpro@basedosdados.org";

const InlineIconProps = {
as: "span",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
verticalAlign: "text-bottom",
width: "18px",
height: "18px",
marginX: "2px",
};

const HelpLinkProps = {
color: "#2B8C4D",
fontWeight: "500",
textDecoration: "underline",
_hover: { opacity: 0.8 },
};

const HelpListProps = {
spacing: "12px",
margin: "0 0 0 20px",
paddingLeft: "0",
stylePosition: "outside",
};

function InlineIcon({ icon: Icon }) {
return (
<Box {...InlineIconProps}>
<Icon width="16px" height="16px" fill="currentColor" />
</Box>
);
}

function HelpSection({ title, children }) {
return (
<Box as="section">
<LabelText
as="h2"
typography="large"
marginBottom="12px"
>
{title}
</LabelText>
{children}
</Box>
);
}

function HelpList({ items }) {
return (
<UnorderedList {...HelpListProps}>
{items.map((item) => (
<ListItem key={item}>
<BodyText as="span">{item}</BodyText>
</ListItem>
))}
</UnorderedList>
);
}

export default function HelpContent() {
const { t } = useTranslation("chatbot");
const { locale } = useRouter();
const promptTips = t("help.promptTips", { returnObjects: true });
const tips = Array.isArray(promptTips) ? promptTips : [];
const capabilitiesRaw = t("help.capabilities", { returnObjects: true });
const capabilities = Array.isArray(capabilitiesRaw) ? capabilitiesRaw : [];

return (
<Box
flex={1}
minHeight={0}
overflowY="auto"
width="100%"
paddingX={{ base: "4px", md: "24px" }}
sx={{
"&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-track": { background: "transparent" },
"&::-webkit-scrollbar-thumb": {
background: "#C4C4C4",
borderRadius: "24px",
},
scrollbarWidth: "thin",
scrollbarColor: "#C4C4C4 transparent",
}}
>
<Box
maxWidth="720px"
marginX="auto"
paddingBottom="48px"
display="flex"
flexDirection="column"
gap="28px"
>
<Box>
<TitleText
as="h1"
typography="large"
marginBottom="8px"
>
{t("help.title")}
</TitleText>
<BodyText color="#71757A">
{t("help.subtitle")}
</BodyText>
</Box>

<HelpSection title={t("help.welcomeTitle")}>
<BodyText marginBottom="12px">{t("help.welcomeP1")}</BodyText>
<BodyText marginBottom="12px">{t("help.welcomeP2")}</BodyText>
<BodyText marginBottom="12px">{t("help.welcomeP3")}</BodyText>
<BodyText marginBottom="12px">{t("help.welcomeP4")}</BodyText>
<Box marginBottom="12px">
<HelpList items={capabilities} />
</Box>
<BodyText marginBottom="12px">{t("help.welcomeP5")}</BodyText>
<BodyText>
<Trans
i18nKey="help.contact"
ns="chatbot"
components={{
email: (
<Link
href={`mailto:${SupportEmail}`}
{...HelpLinkProps}
/>
),
discord: (
<Link
href={DiscordUrlByLocale[locale] || DiscordUrlByLocale.pt}
isExternal
{...HelpLinkProps}
/>
),
whatsapp: (
<Link
href={WhatsAppCommunityUrl}
isExternal
{...HelpLinkProps}
/>
),
}}
/>
</BodyText>
</HelpSection>

<HelpSection title={t("help.featuresTitle")}>
<BodyText marginBottom="12px">{t("help.dataSources")}</BodyText>
<BodyText marginBottom="12px">{t("help.suggestedQuestions")}</BodyText>
<BodyText marginBottom="12px">
<Trans
i18nKey="help.download"
ns="chatbot"
components={{
download: <InlineIcon icon={DownloadIcon} />,
}}
/>
</BodyText>
<BodyText marginBottom="12px">
<Trans
i18nKey="help.copyResults"
ns="chatbot"
components={{
copy: <InlineIcon icon={CopyIcon} />,
}}
/>
</BodyText>
<BodyText>
<Trans
i18nKey="help.feedback"
ns="chatbot"
components={{
up: <InlineIcon icon={ThumbUpIcon} />,
down: <InlineIcon icon={ThumbDownIcon} />,
}}
/>
</BodyText>
</HelpSection>

<HelpSection title={t("help.promptGuideTitle")}>
<BodyText marginBottom="12px">{t("help.promptGuideIntro")}</BodyText>
<HelpList items={tips} />
</HelpSection>
</Box>
</Box>
);
}
57 changes: 7 additions & 50 deletions next/components/organisms/chatbot/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@ import {
Stack,
Flex,
Divider,
HStack,
useMediaQuery,
} from '@chakra-ui/react'
import BDLogoImage from '../../../public/img/logos/bd_logo'
import { clearClientSession } from '../../../utils'
import SidebarIcon from '../../../public/img/icons/sidebarIcon'
import CrossIcon from '../../../public/img/icons/crossIcon'
import BodyText from '../../atoms/Text/BodyText'
import SignOutIcon from '../../../public/img/icons/signOutIcon'
import ThreadList from './ThreadList'
import UserMenu from './UserMenu'

function Sidebar({
onNewChat,
onSelectThread,
onHelp,
currentThreadId,
isMobileOpen = false,
onMobileClose,
Expand All @@ -28,20 +27,6 @@ function Sidebar({

const isOpen = isMobile ? true : isExpanded

const handleLogout = useCallback(async () => {
await clearClientSession()
if (typeof window === 'undefined') return
if (window.location.pathname.includes('/user/')) {
window.location.href = '/'
return
}
if (window.location.pathname.includes('/chatbot')) {
window.location.href = '/user/login'
return
}
window.location.reload()
}, [])

const handleToggle = useCallback(() => {
if (isMobile) {
onMobileClose?.()
Expand Down Expand Up @@ -248,39 +233,11 @@ function Sidebar({
</Stack>
<Box flexShrink={0}>
<Divider borderColor="#DEDFE0" />
<HStack
as="button"
type="button"
spacing="8px"
align="center"
justifyContent={isOpen ? "flex-start" : "center"}
width="100%"
padding="16px"
borderRadius="8px"
cursor="pointer"
background="transparent"
border="none"
color="#252A32"
fill="#D0D0D0"
onClick={handleLogout}
_hover={{
backgroundColor: "#EEEEEE",
opacity: 0.9,
}}
>
<SignOutIcon width="18px" height="18px" fill="currentColor" />
<BodyText
typography="small"
color="currentColor"
opacity={isOpen ? 1 : 0}
width={isOpen ? "auto" : 0}
overflow="hidden"
whiteSpace="nowrap"
transition="opacity 0.2s ease, width 0.2s ease"
>
Sair
</BodyText>
</HStack>
<UserMenu
isSidebarOpen={isOpen}
onHelp={onHelp}
onMobileClose={onMobileClose}
/>
</Box>
</Box>
</>
Expand Down
Loading