refactor: remove open ai key

feat/ai-roadmap
Arik Chakma 4 weeks ago
parent 80dcc0f39c
commit 55b4cc634a
  1. 70
      src/components/GenerateRoadmap/GenerateRoadmap.tsx
  2. 17
      src/components/GenerateRoadmap/IncreaseRoadmapLimit.tsx
  3. 171
      src/components/GenerateRoadmap/OpenAISettings.tsx
  4. 72
      src/components/GenerateRoadmap/RoadmapSearch.tsx
  5. 30
      src/components/GenerateRoadmap/RoadmapTopicDetail.tsx
  6. 25
      src/lib/ai.ts
  7. 21
      src/lib/jwt.ts

@ -12,7 +12,6 @@ import { generateAIRoadmapFromText } from '../../../editor/utils/roadmap-generat
import { renderFlowJSON } from '../../../editor/renderer/renderer';
import { replaceChildren } from '../../lib/dom';
import {
getOpenAIKey,
isLoggedIn,
removeAuthToken,
setAIReferralCode,
@ -31,6 +30,7 @@ import { cn } from '../../lib/classname.ts';
import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx';
import { AIRoadmapAlert } from './AIRoadmapAlert.tsx';
import {
generateAICourseRoadmapStructure,
IS_KEY_ONLY_ROADMAP_GENERATION,
readAIRoadmapStream,
} from '../../lib/ai.ts';
@ -129,13 +129,11 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
const [roadmapTopicLimitUsed, setRoadmapTopicLimitUsed] = useState(0);
const [isConfiguring, setIsConfiguring] = useState(false);
const [openAPIKey, setOpenAPIKey] = useState<string | undefined>(
getOpenAIKey(),
);
const isKeyOnly = IS_KEY_ONLY_ROADMAP_GENERATION;
const renderRoadmap = async (roadmap: string) => {
const { nodes, edges } = generateAIRoadmapFromText(roadmap);
const result = generateAICourseRoadmapStructure(roadmap);
const { nodes, edges } = generateAIRoadmapFromText(result);
const svg = await renderFlowJSON({ nodes, edges });
if (roadmapContainerRef?.current) {
replaceChildren(roadmapContainerRef?.current, svg);
@ -481,7 +479,6 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
{isConfiguring && (
<IncreaseRoadmapLimit
onClose={() => {
setOpenAPIKey(getOpenAIKey());
setIsConfiguring(false);
loadAIRoadmapLimit().finally(() => null);
}}
@ -524,29 +521,16 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
<AIRoadmapAlert />
{isKeyOnly && isAuthenticatedUser && (
<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>
)}
<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 more credits.
</button>
</p>
</div>
)}
{!isKeyOnly && isAuthenticatedUser && (
@ -565,25 +549,13 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
</span>{' '}
roadmaps generated today.
</span>
{!openAPIKey && (
<button
onClick={() => setIsConfiguring(true)}
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"
>
Need to generate more?{' '}
<span className="font-semibold">Click here.</span>
</button>
)}
{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 key
</button>
)}
<button
onClick={() => setIsConfiguring(true)}
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"
>
Need to generate more?{' '}
<span className="font-semibold">Click here.</span>
</button>
</div>
)}
{!isAuthenticatedUser && (
@ -626,7 +598,7 @@ export function GenerateRoadmap(props: GenerateRoadmapProps) {
!roadmapTerm ||
roadmapLimitUsed >= roadmapLimit ||
roadmapTerm === currentRoadmap?.term ||
(isKeyOnly && !openAPIKey)))
isKeyOnly))
}
>
{isLoadingResults && (

@ -1,20 +1,16 @@
import { useState } from 'react';
import { cn } from '../../lib/classname';
import { ChevronUp } from 'lucide-react';
import { Modal } from '../Modal';
import { ReferYourFriend } from './ReferYourFriend';
import { OpenAISettings } from './OpenAISettings';
import { PayToBypass } from './PayToBypass';
import { PickLimitOption } from './PickLimitOption';
import { getOpenAIKey } from '../../lib/jwt.ts';
export type IncreaseTab = 'api-key' | 'refer-friends' | 'payment';
export type IncreaseTab = 'refer-friends' | 'payment';
export const increaseLimitTabs: {
key: IncreaseTab;
title: string;
}[] = [
{ key: 'api-key', title: 'Add your own API Key' },
{ key: 'refer-friends', title: 'Refer your Friends' },
{ key: 'payment', title: 'Pay to Bypass the limit' },
];
@ -25,9 +21,8 @@ type IncreaseRoadmapLimitProps = {
export function IncreaseRoadmapLimit(props: IncreaseRoadmapLimitProps) {
const { onClose } = props;
const openAPIKey = getOpenAIKey();
const [activeTab, setActiveTab] = useState<IncreaseTab | null>(
openAPIKey ? 'api-key' : null,
'refer-friends',
);
return (
@ -44,14 +39,6 @@ export function IncreaseRoadmapLimit(props: IncreaseRoadmapLimitProps) {
<PickLimitOption activeTab={activeTab} setActiveTab={setActiveTab} />
)}
{activeTab === 'api-key' && (
<OpenAISettings
onClose={() => {
onClose();
}}
onBack={() => setActiveTab(null)}
/>
)}
{activeTab === 'refer-friends' && (
<ReferYourFriend onBack={() => setActiveTab(null)} />
)}

@ -1,171 +0,0 @@
import { useEffect, useState } from 'react';
import { deleteOpenAIKey, getOpenAIKey, saveOpenAIKey } from '../../lib/jwt.ts';
import { cn } from '../../lib/classname.ts';
import { CloseIcon } from '../ReactIcons/CloseIcon.tsx';
import { useToast } from '../../hooks/use-toast.ts';
import { httpPost } from '../../lib/http.ts';
import { ChevronLeft } from 'lucide-react';
type OpenAISettingsProps = {
onClose: () => void;
onBack: () => void;
};
export function OpenAISettings(props: OpenAISettingsProps) {
const { onClose, onBack } = props;
const [defaultOpenAIKey, setDefaultOpenAIKey] = useState('');
const [error, setError] = useState('');
const [openaiApiKey, setOpenaiApiKey] = useState('');
const [isLoading, setIsLoading] = useState(false);
const toast = useToast();
useEffect(() => {
const apiKey = getOpenAIKey();
setOpenaiApiKey(apiKey || '');
setDefaultOpenAIKey(apiKey || '');
}, []);
return (
<div className="p-4">
<button
onClick={onBack}
className="mb-5 flex items-center gap-1.5 text-sm leading-none opacity-40 transition-opacity hover:opacity-100 focus:outline-none"
>
<ChevronLeft size={16} />
Back to options
</button>
<h2 className="text-xl font-semibold text-gray-800">OpenAI Settings</h2>
<p className="mt-2 text-sm leading-normal text-gray-500">
Add your OpenAI API key below to bypass the roadmap generation limits.
You can use your existing key or{' '}
<a
className="underline underline-offset-2 hover:text-gray-900"
href={'https://platform.openai.com/signup'}
target="_blank"
>
create a new one here
</a>
.
</p>
<form
className="mt-4"
onSubmit={async (e) => {
e.preventDefault();
setError('');
const normalizedKey = openaiApiKey.trim();
if (!normalizedKey) {
deleteOpenAIKey();
toast.success('OpenAI API key removed');
onClose();
return;
}
if (!normalizedKey.startsWith('sk-')) {
setError("Invalid OpenAI API key. It should start with 'sk-'");
return;
}
setIsLoading(true);
const { response, error } = await httpPost(
`${import.meta.env.PUBLIC_API_URL}/v1-validate-openai-key`,
{
key: normalizedKey,
},
);
if (error) {
setError(error.message);
setIsLoading(false);
return;
}
// Save the API key to cookies
saveOpenAIKey(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': error,
},
)}
placeholder="Enter your OpenAI API key"
value={openaiApiKey}
onChange={(e) => {
setError('');
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>
<p className={'mb-2 mt-1 text-xs text-gray-500'}>
We do not store your API key on our servers.
</p>
{error && (
<p className="mt-2 text-sm text-red-500">
{error}
</p>
)}
<button
disabled={isLoading}
type="submit"
className={
'mt-2 w-full rounded-md bg-gray-700 px-4 py-2 text-white transition-colors hover:bg-black disabled:cursor-not-allowed disabled:opacity-50'
}
>
{!isLoading && 'Save'}
{isLoading && 'Validating ..'}
</button>
{!defaultOpenAIKey && (
<button
type="button"
onClick={() => {
onClose();
}}
className="mt-1 w-full rounded-md border border-red-500 px-4 py-2 text-sm text-red-600 transition-colors hover:bg-red-700 hover:text-white"
>
Cancel
</button>
)}
{defaultOpenAIKey && (
<button
type="button"
onClick={() => {
deleteOpenAIKey();
onClose();
toast.success('OpenAI API key removed');
}}
className="mt-1 w-full rounded-md border border-red-500 px-4 py-2 text-sm text-red-600 transition-colors hover:bg-red-700 hover:text-white"
>
Remove API Key
</button>
)}
</form>
</div>
);
}

@ -1,10 +1,9 @@
import { ArrowUpRight, Ban, Cog, Telescope, Wand } from 'lucide-react';
import type { FormEvent } from 'react';
import { useEffect, useState } from 'react';
import { getOpenAIKey, isLoggedIn } from '../../lib/jwt';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname.ts';
import { OpenAISettings } from './OpenAISettings.tsx';
import { AITermSuggestionInput } from './AITermSuggestionInput.tsx';
import { IncreaseRoadmapLimit } from './IncreaseRoadmapLimit.tsx';
@ -33,12 +32,10 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
const canGenerateMore = limitUsed < limit;
const [isConfiguring, setIsConfiguring] = useState(false);
const [openAPIKey, setOpenAPIKey] = useState('');
const [isAuthenticatedUser, setIsAuthenticatedUser] = useState(false);
const [isLoadingResults, setIsLoadingResults] = useState(false);
useEffect(() => {
setOpenAPIKey(getOpenAIKey() || '');
setIsAuthenticatedUser(isLoggedIn());
}, []);
@ -49,7 +46,6 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
{isConfiguring && (
<IncreaseRoadmapLimit
onClose={() => {
setOpenAPIKey(getOpenAIKey()!);
setIsConfiguring(false);
loadAIRoadmapLimit();
}}
@ -104,10 +100,7 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
disabled={
isLoadingResults ||
(isAuthenticatedUser &&
(!limit ||
!roadmapTerm ||
limitUsed >= limit ||
(isKeyOnly && !openAPIKey)))
(!limit || !roadmapTerm || limitUsed >= limit || isKeyOnly))
}
>
{isLoadingResults && (
@ -202,31 +195,16 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
)}
{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"
>
add your own OpenAI API key.
</button>
</p>
</>
)}
{openAPIKey && (
<p className={'text-center 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>
)}
<p className={'text-center text-red-500'}>
We have hit the limit for AI roadmap generation. Please try again
again later or{' '}
<button
onClick={() => setIsConfiguring(true)}
className="font-semibold text-purple-600 underline underline-offset-2"
>
get more credits.
</button>
</p>
<p className="flex flex-col gap-2 text-center text-gray-500 sm:flex-row">
<a
@ -259,25 +237,13 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
</p>
{isAuthenticatedUser && (
<p className="flex items-center text-sm">
{!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"
>
Need to generate more?{' '}
<span className="font-semibold">Click here.</span>
</button>
)}
{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 key
</button>
)}
<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"
>
Need to generate more?{' '}
<span className="font-semibold">Click here.</span>
</button>
</p>
)}
</div>

@ -3,10 +3,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { markdownToHtml } from '../../lib/markdown';
import { Ban, Cog, Contact, FileText, X } from 'lucide-react';
import { Ban, Contact, FileText, X } from 'lucide-react';
import { Spinner } from '../ReactIcons/Spinner';
import type { RoadmapNodeDetails } from './GenerateRoadmap';
import { getOpenAIKey, isLoggedIn, removeAuthToken } from '../../lib/jwt';
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import { cn } from '../../lib/classname';
import { showLoginPopup } from '../../lib/popup';
import { readAIRoadmapContentStream } from '../../lib/ai';
@ -121,7 +121,6 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
}, []);
const hasContent = topicHtml?.length > 0;
const openAIKey = getOpenAIKey();
return (
<div className={'relative z-[92]'}>
@ -146,24 +145,13 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
</span>{' '}
topics generated
</span>
{!openAIKey && (
<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={onConfigureOpenAI}
>
Need to generate more?{' '}
<span className="font-semibold">Click here.</span>
</button>
)}
{openAIKey && (
<button
className="flex items-center gap-1 rounded-xl border border-current px-1.5 py-0.5 text-left text-sm font-medium text-blue-500 sm:text-center"
onClick={onConfigureOpenAI}
>
<Cog className="-mt-0.5 inline-block h-4 w-4" />
Configure OpenAI Key
</button>
)}
<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={onConfigureOpenAI}
>
Need to generate more?{' '}
<span className="font-semibold">Click here.</span>
</button>
</div>
)}

@ -54,6 +54,7 @@ export function generateAiCourseStructure(
return {
title,
modules,
done: [],
};
}
@ -245,14 +246,7 @@ export function generateAICourseRoadmapStructure(data: string): ResultItem[] {
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (i === 0 && line.startsWith('#')) {
const title = line.replace('#', '').trim();
result.push({
id: nanoid(),
type: 'title',
label: title,
});
} else if (line.startsWith('###')) {
if (line.startsWith('###')) {
if (currentTopic) {
result.push(currentTopic);
}
@ -264,7 +258,20 @@ export function generateAICourseRoadmapStructure(data: string): ResultItem[] {
label,
children: [],
};
} else if (line.startsWith('- ')) {
} else if (line.startsWith('##')) {
result.push({
id: nanoid(),
type: 'label',
label: line.replace('##', '').trim(),
});
} else if (i === 0 && line.startsWith('#')) {
const title = line.replace('#', '').trim();
result.push({
id: nanoid(),
type: 'title',
label: title,
});
} else if (line.startsWith('-')) {
if (currentTopic) {
const label = line.replace('-', '').trim();
currentTopic.children?.push({

@ -70,27 +70,6 @@ export function visitAIRoadmap(roadmapId: string) {
});
}
export function deleteOpenAIKey() {
Cookies.remove('oak', {
path: '/',
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
}
export function saveOpenAIKey(apiKey: string) {
Cookies.set('oak', apiKey, {
path: '/',
expires: 365,
sameSite: 'lax',
secure: true,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
}
export function getOpenAIKey() {
return Cookies.get('oak');
}
const AI_REFERRAL_COOKIE_NAME = 'referral_code';
export function setAIReferralCode(code: string) {

Loading…
Cancel
Save