Block UI for guest users

pull/5337/head
Kamran Ahmed 11 months ago
parent 5b7e7b9767
commit 64cfe503af
  1. 42
      src/components/GenerateRoadmap/GenerateRoadmap.tsx
  2. 10
      src/components/GenerateRoadmap/OpenAISettings.tsx
  3. 148
      src/components/GenerateRoadmap/RoadmapSearch.tsx
  4. 1
      src/lib/ai.ts

@ -35,6 +35,7 @@ import { cn } from '../../lib/classname.ts';
import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx'; import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx';
import { AIRoadmapAlert } from './AIRoadmapAlert.tsx'; import { AIRoadmapAlert } from './AIRoadmapAlert.tsx';
import { OpenAISettings } from './OpenAISettings.tsx'; import { OpenAISettings } from './OpenAISettings.tsx';
import { IS_KEY_ONLY_ROADMAP_GENERATION } from '../../lib/ai.ts';
export type GetAIRoadmapLimitResponse = { export type GetAIRoadmapLimitResponse = {
used: number; used: number;
@ -104,6 +105,7 @@ export function GenerateRoadmap() {
const [isConfiguring, setIsConfiguring] = useState(false); const [isConfiguring, setIsConfiguring] = useState(false);
const openAPIKey = getOpenAIKey(); const openAPIKey = getOpenAIKey();
const isKeyOnly = IS_KEY_ONLY_ROADMAP_GENERATION;
const renderRoadmap = async (roadmap: string) => { const renderRoadmap = async (roadmap: string) => {
const { nodes, edges } = generateAIRoadmapFromText(roadmap); const { nodes, edges } = generateAIRoadmapFromText(roadmap);
@ -372,6 +374,7 @@ export function GenerateRoadmap() {
limit={roadmapLimit} limit={roadmapLimit}
limitUsed={roadmapLimitUsed} limitUsed={roadmapLimitUsed}
loadAIRoadmapLimit={loadAIRoadmapLimit} loadAIRoadmapLimit={loadAIRoadmapLimit}
isKeyOnly={isKeyOnly}
onLoadTerm={(term: string) => { onLoadTerm={(term: string) => {
setRoadmapTerm(term); setRoadmapTerm(term);
loadTermRoadmap(term).finally(() => {}); loadTermRoadmap(term).finally(() => {});
@ -427,8 +430,36 @@ export function GenerateRoadmap() {
</span> </span>
)} )}
{!isLoading && ( {!isLoading && (
<div className="container flex flex-grow flex-col items-center"> <div className="container flex flex-grow flex-col items-start">
<AIRoadmapAlert /> <AIRoadmapAlert />
{isKeyOnly && (
<div className="flex flex-row gap-4">
{!openAPIKey && (
<p className={'text-left text-red-500'}>
We have hit the limit for AI roadmap generation. Please
try again tomorrow or{' '}
<button
onClick={() => setIsConfiguring(true)}
className="font-semibold text-purple-600 underline underline-offset-2"
>
add your own OpenAI API key
</button>
</p>
)}
{openAPIKey && (
<p className={'text-left text-gray-500'}>
You have added your own OpenAI API key.{' '}
<button
onClick={() => setIsConfiguring(true)}
className="font-semibold text-purple-600 underline underline-offset-2"
>
Configure it here if you want.
</button>
</p>
)}
</div>
)}
{!isKeyOnly && (
<div className="mt-2 flex w-full flex-col items-start justify-between gap-2 text-sm sm:flex-row sm:items-center sm:gap-0"> <div className="mt-2 flex w-full flex-col items-start justify-between gap-2 text-sm sm:flex-row sm:items-center sm:gap-0">
<span> <span>
<span <span
@ -459,7 +490,7 @@ export function GenerateRoadmap() {
{isLoggedInUser && !openAPIKey && ( {isLoggedInUser && !openAPIKey && (
<button <button
onClick={() => setIsConfiguring(true)} onClick={() => setIsConfiguring(true)}
className="text-left rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white" className="rounded-xl border border-current px-2 py-0.5 text-left text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
> >
By-pass all limits by{' '} By-pass all limits by{' '}
<span className="font-semibold"> <span className="font-semibold">
@ -478,12 +509,14 @@ export function GenerateRoadmap() {
</button> </button>
)} )}
</div> </div>
)}
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="my-3 flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-center" className="my-3 flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-center"
> >
<input <input
type="text" type="text"
autoFocus
placeholder="e.g. Try searching for Ansible or DevOps" placeholder="e.g. Try searching for Ansible or DevOps"
className="flex-grow rounded-md border border-gray-400 px-3 py-2 transition-colors focus:border-black focus:outline-none" className="flex-grow rounded-md border border-gray-400 px-3 py-2 transition-colors focus:border-black focus:outline-none"
value={roadmapTerm} value={roadmapTerm}
@ -501,7 +534,8 @@ export function GenerateRoadmap() {
!roadmapLimit || !roadmapLimit ||
!roadmapTerm || !roadmapTerm ||
roadmapLimitUsed >= roadmapLimit || roadmapLimitUsed >= roadmapLimit ||
roadmapTerm === currentRoadmap?.term roadmapTerm === currentRoadmap?.term ||
(isKeyOnly && !openAPIKey)
} }
> >
{roadmapLimit > 0 && canGenerateMore && ( {roadmapLimit > 0 && canGenerateMore && (
@ -514,7 +548,7 @@ export function GenerateRoadmap() {
{roadmapLimit === 0 && <span>Please wait..</span>} {roadmapLimit === 0 && <span>Please wait..</span>}
{roadmapLimit > 0 && !canGenerateMore && ( {roadmapLimit > 0 && !canGenerateMore && (
<span className="flex items-center text-sm"> <span className="flex items-center">
<Ban size={15} className="mr-2" /> <Ban size={15} className="mr-2" />
Limit reached Limit reached
</span> </span>

@ -1,10 +1,6 @@
import { Modal } from '../Modal.tsx'; import { Modal } from '../Modal.tsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import { deleteOpenAIKey, getOpenAIKey, saveOpenAIKey } from '../../lib/jwt.ts';
deleteOpenAIKey,
getOpenAIKey,
saveOpenAIKey,
} from '../../lib/jwt.ts';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { CloseIcon } from '../ReactIcons/CloseIcon.tsx'; import { CloseIcon } from '../ReactIcons/CloseIcon.tsx';
import { useToast } from '../../hooks/use-toast.ts'; import { useToast } from '../../hooks/use-toast.ts';
@ -121,6 +117,10 @@ export function OpenAISettings(props: OpenAISettingsProps) {
</button> </button>
)} )}
</div> </div>
<p className={'mb-2 mt-1 text-xs text-gray-500'}>
We do not store your API key on our servers.
</p>
{hasError && ( {hasError && (
<p className="mt-2 text-sm text-red-500"> <p className="mt-2 text-sm text-red-500">
Please enter a valid OpenAI API key Please enter a valid OpenAI API key

@ -21,6 +21,7 @@ type RoadmapSearchProps = {
onLoadTerm: (topic: string) => void; onLoadTerm: (topic: string) => void;
limit: number; limit: number;
limitUsed: number; limitUsed: number;
isKeyOnly: boolean;
}; };
export function RoadmapSearch(props: RoadmapSearchProps) { export function RoadmapSearch(props: RoadmapSearchProps) {
@ -32,16 +33,18 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
limitUsed = 0, limitUsed = 0,
onLoadTerm, onLoadTerm,
loadAIRoadmapLimit, loadAIRoadmapLimit,
isKeyOnly,
} = props; } = props;
const canGenerateMore = limitUsed < limit; const canGenerateMore = limitUsed < limit;
const [isConfiguring, setIsConfiguring] = useState(false); const [isConfiguring, setIsConfiguring] = useState(false);
const openAPIKey = getOpenAIKey(); const openAPIKey = getOpenAIKey();
const isAuthenticatedUser = isLoggedIn();
const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC']; const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC'];
return ( return (
<div className="flex flex-grow flex-col items-center sm:justify-center px-4 py-6 sm:px-6"> <div className="flex flex-grow flex-col items-center px-4 py-6 sm:px-6">
{isConfiguring && ( {isConfiguring && (
<OpenAISettings <OpenAISettings
onClose={() => { onClose={() => {
@ -50,7 +53,7 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
}} }}
/> />
)} )}
<div className="flex flex-col gap-0 text-center sm:gap-2"> <div className="flex flex-col gap-0 text-center sm:gap-2 md:mt-24 lg:mt-32">
<h1 className="relative text-2xl font-medium sm:text-3xl"> <h1 className="relative text-2xl font-medium sm:text-3xl">
<span className="hidden sm:inline">Generate roadmaps with AI</span> <span className="hidden sm:inline">Generate roadmaps with AI</span>
<span className="inline sm:hidden">AI Roadmap Generator</span> <span className="inline sm:hidden">AI Roadmap Generator</span>
@ -90,8 +93,28 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
'flex min-w-[154px] flex-shrink-0 items-center justify-center gap-2 rounded-md bg-black px-4 py-2 text-white', 'flex min-w-[154px] flex-shrink-0 items-center justify-center gap-2 rounded-md bg-black px-4 py-2 text-white',
'disabled:cursor-not-allowed disabled:opacity-50', 'disabled:cursor-not-allowed disabled:opacity-50',
)} )}
disabled={!limit || !roadmapTerm || limitUsed >= limit} onClick={(e) => {
if (!isAuthenticatedUser) {
e.preventDefault();
showLoginPopup();
}
}}
disabled={
isAuthenticatedUser &&
(!limit ||
!roadmapTerm ||
limitUsed >= limit ||
(isKeyOnly && !openAPIKey))
}
> >
{!isAuthenticatedUser && (
<>
<Wand size={20} />
Generate
</>
)}
{isAuthenticatedUser && (
<>
{(!limit || canGenerateMore) && ( {(!limit || canGenerateMore) && (
<> <>
<Wand size={20} /> <Wand size={20} />
@ -105,15 +128,22 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
Limit reached Limit reached
</span> </span>
)} )}
</>
)}
</button> </button>
</form> </form>
<div className="flex flex-row items-center justify-center gap-2 flex-wrap"> <div className="flex flex-row flex-wrap items-center justify-center gap-2">
{randomTerms.map((term) => ( {randomTerms.map((term) => (
<button <button
key={term} key={term}
disabled={!limit || !canGenerateMore} disabled={isAuthenticatedUser && (!limit || !canGenerateMore)}
type="button" type="button"
onClick={() => { onClick={() => {
if (!isAuthenticatedUser) {
showLoginPopup();
return;
}
onLoadTerm(term); onLoadTerm(term);
}} }}
className="flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-sm transition-colors hover:border-black hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50" className="flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-sm transition-colors hover:border-black hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
@ -129,38 +159,94 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
</a> </a>
</div> </div>
</div> </div>
<div className="mt-12 flex flex-col items-center gap-4"> {!isAuthenticatedUser && (
<p className="text-gray-500 text-center"> <div className="mt-8 flex max-w-[500px] flex-col items-center gap-3 rounded-xl border border-gray-400 px-4 pb-4 pt-3">
You have generated{' '} <p className={'text-center text-gray-500'}>
<span <button
className={cn( onClick={showLoginPopup}
'inline-block min-w-[50px] rounded-xl border px-1.5 text-center text-sm tabular-nums text-gray-800', className="font-medium text-purple-600 underline underline-offset-2 hover:text-purple-800"
{ >
'animate-pulse border-zinc-300 bg-zinc-300 text-zinc-300': Sign up (free and takes 2s) or login
!limit, </button>{' '}
}, to start generating roadmaps. Or explore the ones made by the
community.
</p>
<p className="flex flex-col gap-2 text-center text-gray-500 sm:flex-row">
<a
href="/ai/explore"
className="flex items-center gap-1.5 rounded-full border border-purple-600 px-2.5 py-0.5 text-sm text-purple-600 transition-colors hover:bg-purple-600 hover:text-white"
>
Explore AI Generated Roadmaps <Telescope size={15} />
</a>
<a
href="/roadmaps"
className="flex items-center gap-1.5 rounded-full border border-purple-600 px-2.5 py-0.5 text-sm text-purple-600 transition-colors hover:bg-purple-600 hover:text-white"
>
Visit Official Roadmaps <ArrowUpRight size={15} />
</a>
</p>
</div>
)} )}
{isKeyOnly && isAuthenticatedUser && (
<div className="mx-auto mt-12 flex max-w-[450px] flex-col items-center gap-4">
{!openAPIKey && (
<>
<p className={'text-center text-red-500'}>
We have hit the limit for AI roadmap generation. Please try
again later or{' '}
<button
onClick={() => setIsConfiguring(true)}
className="font-semibold text-purple-600 underline underline-offset-2"
> >
{limitUsed} of {limit} add your own OpenAI API key.
</span>{' '} </button>
roadmaps.
</p> </p>
<p className="flex min-h-[46px] sm:min-h-[26px] items-center text-sm"> </>
{limit > 0 && !isLoggedIn() && ( )}
{openAPIKey && (
<p className={'text-center text-gray-500'}>
You have added your own OpenAI API key.{' '}
<button <button
onClick={showLoginPopup} onClick={() => setIsConfiguring(true)}
className="rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white" className="font-semibold text-purple-600 underline underline-offset-2"
> >
Generate more by{' '} Configure it here if you want.
<span className="font-semibold">
signing up (free and takes 2 seconds)
</span>{' '}
or <span className="font-semibold">logging in</span>
</button> </button>
</p>
)} )}
<p className="flex flex-col gap-2 text-center text-gray-500 sm:flex-row">
<a
href="/ai/explore"
className="flex items-center gap-1.5 rounded-full border border-purple-600 px-2.5 py-0.5 text-sm text-purple-600 transition-colors hover:bg-purple-600 hover:text-white"
>
Explore AI Roadmaps <Telescope size={15} />
</a>
<a
href="/roadmaps"
className="flex items-center gap-1.5 rounded-full border border-purple-600 px-2.5 py-0.5 text-sm text-purple-600 transition-colors hover:bg-purple-600 hover:text-white"
>
Visit Official Roadmaps <ArrowUpRight size={15} />
</a>
</p>
</div>
)}
{!isKeyOnly && limit > 0 && isAuthenticatedUser && (
<div className="mt-12 flex flex-col items-center gap-4">
<p className="text-center text-gray-500">
You have generated{' '}
<span
className={
'inline-block min-w-[50px] rounded-xl border px-1.5 text-center text-sm tabular-nums text-gray-800'
}
>
{limitUsed} of {limit}
</span>{' '}
roadmaps.
</p> </p>
<p className="-mt-[75px] sm:-mt-[46px] flex min-h-[46px] sm:min-h-[26px] items-center text-sm"> {isAuthenticatedUser && (
{limit > 0 && isLoggedIn() && !openAPIKey && ( <p className="flex items-center text-sm">
{!openAPIKey && (
<button <button
onClick={() => setIsConfiguring(true)} onClick={() => setIsConfiguring(true)}
className="rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white" className="rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
@ -172,7 +258,7 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
</button> </button>
)} )}
{limit > 0 && isLoggedIn() && openAPIKey && ( {openAPIKey && (
<button <button
onClick={() => setIsConfiguring(true)} onClick={() => setIsConfiguring(true)}
className="flex flex-row items-center gap-1 rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white" className="flex flex-row items-center gap-1 rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
@ -182,7 +268,9 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
</button> </button>
)} )}
</p> </p>
)}
</div> </div>
)}
</div> </div>
); );
} }

@ -0,0 +1 @@
export const IS_KEY_ONLY_ROADMAP_GENERATION = false;
Loading…
Cancel
Save