diff --git a/src/components/GenerateRoadmap/GenerateRoadmap.tsx b/src/components/GenerateRoadmap/GenerateRoadmap.tsx index 4923135d9..92f8a721d 100644 --- a/src/components/GenerateRoadmap/GenerateRoadmap.tsx +++ b/src/components/GenerateRoadmap/GenerateRoadmap.tsx @@ -362,6 +362,7 @@ export function GenerateRoadmap() { handleSubmit={handleSubmit} limit={roadmapLimit} limitUsed={roadmapLimitUsed} + loadAIRoadmapLimit={loadAIRoadmapLimit} onLoadTerm={(term: string) => { setRoadmapTerm(term); loadTermRoadmap(term).finally(() => {}); diff --git a/src/components/GenerateRoadmap/OpenAISettings.tsx b/src/components/GenerateRoadmap/OpenAISettings.tsx new file mode 100644 index 000000000..48236cf44 --- /dev/null +++ b/src/components/GenerateRoadmap/OpenAISettings.tsx @@ -0,0 +1,120 @@ +import { Modal } from '../Modal.tsx'; +import { useEffect, useState } from 'react'; +import { + deleteOpenAPIKey, + getOpenAPIKey, + saveOpenAPIKey, +} from '../../lib/jwt.ts'; +import { cn } from '../../lib/classname.ts'; +import { CloseIcon } from '../ReactIcons/CloseIcon.tsx'; +import { useToast } from '../../hooks/use-toast.ts'; + +type OpenAISettingsProps = { + onClose: () => void; +}; + +export function OpenAISettings(props: OpenAISettingsProps) { + const { onClose } = props; + + const [hasError, setHasError] = useState(false); + const [openaiApiKey, setOpenaiApiKey] = useState(''); + const toast = useToast(); + + useEffect(() => { + const apiKey = getOpenAPIKey(); + setOpenaiApiKey(apiKey || ''); + }, []); + + return ( + +
+

OpenAI Settings

+
+

+ AI Roadmap generator uses OpenAI's GPT-4 model to generate roadmaps. +

+ +

+ + Create an account on OpenAI + {' '} + and enter your API key below to enable the AI Roadmap generator +

+ +
{ + e.preventDefault(); + setHasError(false); + + const normalizedKey = openaiApiKey.trim(); + if (!normalizedKey) { + deleteOpenAPIKey(); + toast.success('OpenAI API key removed'); + onClose(); + return; + } + + if (!normalizedKey.startsWith('sk-')) { + setHasError(true); + return; + } + + // Save the API key to cookies + saveOpenAPIKey(normalizedKey); + toast.success('OpenAI API key saved'); + onClose(); + }} + > +
+ { + setHasError(false); + setOpenaiApiKey((e.target as HTMLInputElement).value); + }} + /> + + {openaiApiKey && ( + + )} +
+ {hasError && ( +

+ Please enter a valid OpenAI API key +

+ )} + +
+
+
+
+ ); +} diff --git a/src/components/GenerateRoadmap/RoadmapSearch.tsx b/src/components/GenerateRoadmap/RoadmapSearch.tsx index bdc84fdf7..f2ddd6da2 100644 --- a/src/components/GenerateRoadmap/RoadmapSearch.tsx +++ b/src/components/GenerateRoadmap/RoadmapSearch.tsx @@ -2,18 +2,22 @@ import { ArrowUpRight, Ban, CircleFadingPlus, + Cog, Telescope, Wand, } from 'lucide-react'; import type { FormEvent } from 'react'; -import { isLoggedIn } from '../../lib/jwt'; +import { getOpenAPIKey, isLoggedIn } from '../../lib/jwt'; import { showLoginPopup } from '../../lib/popup'; import { cn } from '../../lib/classname.ts'; +import { useState } from 'react'; +import { OpenAISettings } from './OpenAISettings.tsx'; type RoadmapSearchProps = { roadmapTerm: string; setRoadmapTerm: (topic: string) => void; handleSubmit: (e: FormEvent) => void; + loadAIRoadmapLimit: () => void; onLoadTerm: (topic: string) => void; limit: number; limitUsed: number; @@ -27,14 +31,25 @@ export function RoadmapSearch(props: RoadmapSearchProps) { limit = 0, limitUsed = 0, onLoadTerm, + loadAIRoadmapLimit, } = props; const canGenerateMore = limitUsed < limit; + const [isConfiguring, setIsConfiguring] = useState(false); + const openAPIKey = getOpenAPIKey(); const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC']; return (
+ {isConfiguring && ( + { + setIsConfiguring(false); + loadAIRoadmapLimit(); + }} + /> + )}

Generate roadmaps with AI @@ -144,6 +159,29 @@ export function RoadmapSearch(props: RoadmapSearchProps) { )}

+

+ {limit > 0 && isLoggedIn() && !openAPIKey && ( + + )} + + {limit > 0 && isLoggedIn() && openAPIKey && ( + + )} +

); diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts index e7d0ab7a0..22643b589 100644 --- a/src/lib/jwt.ts +++ b/src/lib/jwt.ts @@ -63,3 +63,24 @@ export function visitAIRoadmap(roadmapId: string) { domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', }); } + +export function deleteOpenAPIKey() { + Cookies.remove('oak', { + path: '/', + domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', + }); +} + +export function saveOpenAPIKey(apiKey: string) { + Cookies.set('oak', apiKey, { + path: '/', + expires: 365, + sameSite: 'lax', + secure: true, + domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', + }); +} + +export function getOpenAPIKey() { + return Cookies.get('oak'); +}