Resolve merge conflicts

fix/ai-roadmap
Kamran Ahmed 9 months ago
commit 1779992250
  1. 38
      src/components/GenerateRoadmap/GenerateRoadmap.tsx
  2. 53
      src/components/GenerateRoadmap/RoadmapTopicDetail.tsx
  3. 2
      src/lib/jwt.ts

@ -1,10 +1,10 @@
import { import {
type FormEvent, type FormEvent,
type MouseEvent,
useCallback,
useEffect, useEffect,
useRef, useRef,
useState, useState,
useCallback,
type MouseEvent,
} from 'react'; } from 'react';
import './GenerateRoadmap.css'; import './GenerateRoadmap.css';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
@ -19,7 +19,6 @@ import {
BadgeCheck, BadgeCheck,
Ban, Ban,
Download, Download,
PencilRuler,
PenSquare, PenSquare,
Save, Save,
Telescope, Telescope,
@ -38,6 +37,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 = {
@ -93,6 +99,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);
@ -249,19 +257,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) => {
@ -369,6 +378,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();
}}
/> />
)} )}
@ -389,23 +403,23 @@ export function GenerateRoadmap() {
Beta Beta
</span> </span>
</h2> </h2>
<p className="mt-1 mb-2"> <p className="mb-2 mt-1">
This is an AI generated roadmap and is not verified by{' '} This is an AI generated roadmap and is not verified by{' '}
<span className={'font-semibold'}>roadmap.sh</span>. We are <span className={'font-semibold'}>roadmap.sh</span>. We are
currently in beta and working hard to improve the quality of currently in beta and working hard to improve the quality of
the generated roadmaps. the generated roadmaps.
</p> </p>
<p className="mt-2 mb-1.5 text-sm flex gap-2"> <p className="mb-1.5 mt-2 flex gap-2 text-sm">
<a <a
href="/ai/explore" href="/ai/explore"
className="rounded-md text-yellow-700 border border-yellow-600 hover:bg-yellow-300 hover:text-yellow-800 px-2 py-1 flex items-center gap-1.5 transition-colors" className="flex items-center gap-1.5 rounded-md border border-yellow-600 px-2 py-1 text-yellow-700 transition-colors hover:bg-yellow-300 hover:text-yellow-800"
> >
<Telescope size={15} /> <Telescope size={15} />
Explore other AI Roadmaps Explore other AI Roadmaps
</a> </a>
<a <a
href="/roadmaps" href="/roadmaps"
className="rounded-md border border-yellow-600 hover:bg-yellow-300 text-yellow-800 bg-yellow-200 px-2 py-1 flex items-center gap-1.5 transition-colors" className="flex items-center gap-1.5 rounded-md border border-yellow-600 bg-yellow-200 px-2 py-1 text-yellow-800 transition-colors hover:bg-yellow-300"
> >
<BadgeCheck size={15} /> <BadgeCheck size={15} />
Visit Official Roadmaps Visit Official Roadmaps

@ -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"

@ -57,7 +57,7 @@ export function visitAIRoadmap(roadmapId: string) {
Cookies.set(`crv-${roadmapId}`, '1', { Cookies.set(`crv-${roadmapId}`, '1', {
path: '/', path: '/',
expires: 30, expires: 1 / 24, // 1 hour
sameSite: 'lax', sameSite: 'lax',
secure: !import.meta.env.DEV, secure: !import.meta.env.DEV,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',

Loading…
Cancel
Save