Allow overriding with own API key

fix/ai-roadmap
Kamran Ahmed 9 months ago
parent 00fa41773c
commit 4408fd0218
  1. 1
      src/components/GenerateRoadmap/GenerateRoadmap.tsx
  2. 120
      src/components/GenerateRoadmap/OpenAISettings.tsx
  3. 40
      src/components/GenerateRoadmap/RoadmapSearch.tsx
  4. 21
      src/lib/jwt.ts

@ -362,6 +362,7 @@ export function GenerateRoadmap() {
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
limit={roadmapLimit} limit={roadmapLimit}
limitUsed={roadmapLimitUsed} limitUsed={roadmapLimitUsed}
loadAIRoadmapLimit={loadAIRoadmapLimit}
onLoadTerm={(term: string) => { onLoadTerm={(term: string) => {
setRoadmapTerm(term); setRoadmapTerm(term);
loadTermRoadmap(term).finally(() => {}); loadTermRoadmap(term).finally(() => {});

@ -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 (
<Modal onClose={onClose}>
<div className="overflow-hidden rounded-lg bg-white p-6 shadow-xl">
<h2 className="text-xl font-medium text-gray-800">OpenAI Settings</h2>
<div className="mt-4">
<p className="text-gray-700">
AI Roadmap generator uses OpenAI's GPT-4 model to generate roadmaps.
</p>
<p className="mt-2">
<a
className="font-semibold underline underline-offset-2"
href={'https://platform.openai.com/signup'}
target="_blank"
>
Create an account on OpenAI
</a>{' '}
and enter your API key below to enable the AI Roadmap generator
</p>
<form
className="mt-4"
onSubmit={(e) => {
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();
}}
>
<div className="relative">
<input
type="text"
name="openai-api-key"
id="openai-api-key"
className={cn(
'block w-full rounded-md border border-gray-300 px-3 py-2 text-gray-800 transition-colors focus:border-black focus:outline-none',
{
'border-red-500 bg-red-100 focus:border-red-500': hasError,
},
)}
placeholder="Enter your OpenAI API key"
value={openaiApiKey}
onChange={(e) => {
setHasError(false);
setOpenaiApiKey((e.target as HTMLInputElement).value);
}}
/>
{openaiApiKey && (
<button
type={'button'}
onClick={() => {
setOpenaiApiKey('');
}}
className="absolute right-2 top-1/2 flex h-[20px] w-[20px] -translate-y-1/2 items-center justify-center rounded-full bg-gray-400 text-white hover:bg-gray-600"
>
<CloseIcon className="h-[13px] w-[13px] stroke-[3.5]" />
</button>
)}
</div>
{hasError && (
<p className="mt-2 text-sm text-red-500">
Please enter a valid OpenAI API key
</p>
)}
<button
type="submit"
className="mt-2 w-full rounded-md bg-gray-700 px-4 py-2 text-white transition-colors hover:bg-black"
>
Save
</button>
</form>
</div>
</div>
</Modal>
);
}

@ -2,18 +2,22 @@ import {
ArrowUpRight, ArrowUpRight,
Ban, Ban,
CircleFadingPlus, CircleFadingPlus,
Cog,
Telescope, Telescope,
Wand, Wand,
} from 'lucide-react'; } from 'lucide-react';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import { isLoggedIn } from '../../lib/jwt'; import { getOpenAPIKey, isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup'; import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { useState } from 'react';
import { OpenAISettings } from './OpenAISettings.tsx';
type RoadmapSearchProps = { type RoadmapSearchProps = {
roadmapTerm: string; roadmapTerm: string;
setRoadmapTerm: (topic: string) => void; setRoadmapTerm: (topic: string) => void;
handleSubmit: (e: FormEvent<HTMLFormElement>) => void; handleSubmit: (e: FormEvent<HTMLFormElement>) => void;
loadAIRoadmapLimit: () => void;
onLoadTerm: (topic: string) => void; onLoadTerm: (topic: string) => void;
limit: number; limit: number;
limitUsed: number; limitUsed: number;
@ -27,14 +31,25 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
limit = 0, limit = 0,
limitUsed = 0, limitUsed = 0,
onLoadTerm, onLoadTerm,
loadAIRoadmapLimit,
} = props; } = props;
const canGenerateMore = limitUsed < limit; const canGenerateMore = limitUsed < limit;
const [isConfiguring, setIsConfiguring] = useState(false);
const openAPIKey = getOpenAPIKey();
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 justify-center px-4 py-6 sm:px-6"> <div className="flex flex-grow flex-col items-center justify-center px-4 py-6 sm:px-6">
{isConfiguring && (
<OpenAISettings
onClose={() => {
setIsConfiguring(false);
loadAIRoadmapLimit();
}}
/>
)}
<div className="flex flex-col gap-0 text-center sm:gap-2"> <div className="flex flex-col gap-0 text-center sm:gap-2">
<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>
@ -144,6 +159,29 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
</button> </button>
)} )}
</p> </p>
<p className="-mt-[45px] flex min-h-[26px] items-center text-sm">
{limit > 0 && isLoggedIn() && !openAPIKey && (
<button
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"
>
By-pass all limits by{' '}
<span className="font-semibold">
adding your own OpenAI API key
</span>
</button>
)}
{limit > 0 && isLoggedIn() && openAPIKey && (
<button
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"
>
<Cog size={15} />
Configure OpenAI API key
</button>
)}
</p>
</div> </div>
</div> </div>
); );

@ -63,3 +63,24 @@ export function visitAIRoadmap(roadmapId: string) {
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', 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');
}

Loading…
Cancel
Save