chore: limit roadmap topic content generation

fix/ai-roadmap
Arik Chakma 9 months ago
parent ecb803e78d
commit 560e9b0229
  1. 41
      src/components/GenerateRoadmap/GenerateRoadmap.tsx
  2. 53
      src/components/GenerateRoadmap/RoadmapTopicDetail.tsx

@ -29,6 +29,13 @@ import { showLoginPopup } from '../../lib/popup.ts';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx'; import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx';
export type GetAIRoadmapLimitResponse = {
used: number;
limit: number;
topicUsed: number;
topicLimit: number;
};
const ROADMAP_ID_REGEX = new RegExp('@ROADMAPID:(\\w+)@'); const ROADMAP_ID_REGEX = new RegExp('@ROADMAPID:(\\w+)@');
export type RoadmapNodeDetails = { export type RoadmapNodeDetails = {
@ -84,6 +91,8 @@ export function GenerateRoadmap() {
const [roadmapLimit, setRoadmapLimit] = useState(0); const [roadmapLimit, setRoadmapLimit] = useState(0);
const [roadmapLimitUsed, setRoadmapLimitUsed] = useState(0); const [roadmapLimitUsed, setRoadmapLimitUsed] = useState(0);
const [roadmapTopicLimit, setRoadmapTopicLimit] = useState(0);
const [roadmapTopicLimitUsed, setRoadmapTopicLimitUsed] = useState(0);
const renderRoadmap = async (roadmap: string) => { const renderRoadmap = async (roadmap: string) => {
const { nodes, edges } = generateAIRoadmapFromText(roadmap); const { nodes, edges } = generateAIRoadmapFromText(roadmap);
@ -240,19 +249,20 @@ export function GenerateRoadmap() {
}; };
const loadAIRoadmapLimit = async () => { const loadAIRoadmapLimit = async () => {
const { response, error } = await httpGet<{ const { response, error } = await httpGet<GetAIRoadmapLimitResponse>(
limit: number; `${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap-limit`,
used: number; );
}>(`${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap-limit`);
if (error || !response) { if (error || !response) {
toast.error(error?.message || 'Something went wrong'); toast.error(error?.message || 'Something went wrong');
return; return;
} }
const { limit, used } = response; const { limit, used, topicLimit, topicUsed } = response;
setRoadmapLimit(limit); setRoadmapLimit(limit);
setRoadmapLimitUsed(used); setRoadmapLimitUsed(used);
setRoadmapTopicLimit(topicLimit);
setRoadmapTopicLimitUsed(topicUsed);
}; };
const loadAIRoadmap = async (roadmapId: string) => { const loadAIRoadmap = async (roadmapId: string) => {
@ -360,6 +370,11 @@ export function GenerateRoadmap() {
parentTitle={selectedTopic.parentTitle} parentTitle={selectedTopic.parentTitle}
onClose={() => setSelectedTopic(null)} onClose={() => setSelectedTopic(null)}
roadmapId={currentRoadmap?.id || ''} roadmapId={currentRoadmap?.id || ''}
topicLimit={roadmapTopicLimit}
topicLimitUsed={roadmapTopicLimitUsed}
onTopicContentGenerateComplete={async () => {
await loadAIRoadmapLimit();
}}
/> />
)} )}
@ -393,10 +408,14 @@ export function GenerateRoadmap() {
</span> </span>
{!isLoggedIn() && ( {!isLoggedIn() && (
<button <button
className="rounded-xl border border-current px-1.5 py-0.5 text-sm font-medium text-blue-500 text-left sm:text-center" className="rounded-xl border border-current px-1.5 py-0.5 text-left text-sm font-medium text-blue-500 sm:text-center"
onClick={showLoginPopup} onClick={showLoginPopup}
> >
Generate more by <span className='font-semibold'>signing up (free, takes 2s)</span> or <span className='font-semibold'>logging in</span> Generate more by{' '}
<span className="font-semibold">
signing up (free, takes 2s)
</span>{' '}
or <span className="font-semibold">logging in</span>
</button> </button>
)} )}
</div> </div>
@ -472,12 +491,14 @@ export function GenerateRoadmap() {
disabled={isLoading} disabled={isLoading}
> >
<Save size={15} /> <Save size={15} />
<span className='hidden sm:inline'>Track your Progress on this Roadmap</span> <span className="hidden sm:inline">
<span className='inline sm:hidden'>Track Progress</span> Track your Progress on this Roadmap
</span>
<span className="inline sm:hidden">Track Progress</span>
</button> </button>
<button <button
className="hidden sm:inline-flex items-center justify-center gap-2 rounded-md bg-gray-200 py-1.5 pl-2.5 pr-3 text-xs font-medium text-black transition-colors duration-300 hover:bg-gray-300 sm:text-sm" className="hidden items-center justify-center gap-2 rounded-md bg-gray-200 py-1.5 pl-2.5 pr-3 text-xs font-medium text-black transition-colors duration-300 hover:bg-gray-300 sm:inline-flex sm:text-sm"
onClick={async () => { onClick={async () => {
const roadmapId = await saveAIRoadmap(); const roadmapId = await saveAIRoadmap();
if (roadmapId) { if (roadmapId) {

@ -6,16 +6,29 @@ import { markdownToHtml } from '../../lib/markdown';
import { Ban, FileText, X } from 'lucide-react'; import { Ban, FileText, X } from 'lucide-react';
import { Spinner } from '../ReactIcons/Spinner'; import { Spinner } from '../ReactIcons/Spinner';
import type { RoadmapNodeDetails } from './GenerateRoadmap'; import type { RoadmapNodeDetails } from './GenerateRoadmap';
import { removeAuthToken } from '../../lib/jwt'; import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import { readAIRoadmapContentStream } from '../../helper/read-stream'; import { readAIRoadmapContentStream } from '../../helper/read-stream';
import { cn } from '../../lib/classname';
import { showLoginPopup } from '../../lib/popup';
type RoadmapTopicDetailProps = RoadmapNodeDetails & { type RoadmapTopicDetailProps = RoadmapNodeDetails & {
onClose?: () => void; onClose?: () => void;
roadmapId: string; roadmapId: string;
topicLimitUsed: number;
topicLimit: number;
onTopicContentGenerateComplete?: () => void;
}; };
export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) { export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
const { onClose, roadmapId, nodeTitle, parentTitle } = props; const {
onClose,
roadmapId,
nodeTitle,
parentTitle,
topicLimit,
topicLimitUsed,
onTopicContentGenerateComplete,
} = props;
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
@ -28,6 +41,12 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
if (topicLimitUsed >= topicLimit) {
setError('You have reached the limit of topics');
setIsLoading(false);
return;
}
if (!roadmapId || !nodeTitle) { if (!roadmapId || !nodeTitle) {
setIsLoading(false); setIsLoading(false);
setError('Invalid roadmap id or node title'); setError('Invalid roadmap id or node title');
@ -76,6 +95,7 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
setTopicHtml(markdownToHtml(result, false)); setTopicHtml(markdownToHtml(result, false));
}, },
}); });
onTopicContentGenerateComplete?.();
}; };
// Close the topic detail when user clicks outside the topic detail // Close the topic detail when user clicks outside the topic detail
@ -109,8 +129,35 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
tabIndex={0} tabIndex={0}
className="fixed right-0 top-0 z-40 h-screen w-full overflow-y-auto bg-white p-4 focus:outline-0 sm:max-w-[600px] sm:p-6" className="fixed right-0 top-0 z-40 h-screen w-full overflow-y-auto bg-white p-4 focus:outline-0 sm:max-w-[600px] sm:p-6"
> >
<div className="flex flex-col items-start gap-2">
<span>
<span
className={cn(
'mr-0.5 inline-block rounded-xl border px-1.5 text-center text-sm tabular-nums text-gray-800',
{
'animate-pulse border-zinc-300 bg-zinc-300 text-zinc-300':
!topicLimit,
},
)}
>
{topicLimitUsed} of {topicLimit}
</span>{' '}
topic content generated.
</span>
{!isLoggedIn() && (
<button
className="rounded-xl border border-current px-1.5 py-0.5 text-left text-sm font-medium text-blue-500 sm:text-center"
onClick={showLoginPopup}
>
Generate more by{' '}
<span className="font-semibold">signing up (free, takes 2s)</span>{' '}
or <span className="font-semibold">logging in</span>
</button>
)}
</div>
{isLoading && ( {isLoading && (
<div className="flex w-full justify-center"> <div className="mt-6 flex w-full justify-center">
<Spinner <Spinner
outerFill="#d1d5db" outerFill="#d1d5db"
className="h-6 w-6 sm:h-12 sm:w-12" className="h-6 w-6 sm:h-12 sm:w-12"

Loading…
Cancel
Save