@@ -363,6 +371,12 @@ export function AICourseContent(props: AICourseContentProps) {
{course.title ? course.difficulty : 'Please wait ..'}
+
+ {!isLoading && (
+
+ )}
{course.title ? (
diff --git a/src/components/GenerateCourse/AICourseFollowUpPopover.tsx b/src/components/GenerateCourse/AICourseFollowUpPopover.tsx
index d1e84976f..4a03fdf1c 100644
--- a/src/components/GenerateCourse/AICourseFollowUpPopover.tsx
+++ b/src/components/GenerateCourse/AICourseFollowUpPopover.tsx
@@ -2,18 +2,18 @@ import { useQuery } from '@tanstack/react-query';
import { BookOpen, Bot, Code, HelpCircle, LockIcon, Send } from 'lucide-react';
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
import { flushSync } from 'react-dom';
+import TextareaAutosize from 'react-textarea-autosize';
import { useOutsideClick } from '../../hooks/use-outside-click';
-import { readAICourseLessonStream } from '../../helper/read-stream';
-import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import { useToast } from '../../hooks/use-toast';
+import { readStream } from '../../lib/ai';
+import { cn } from '../../lib/classname';
+import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import {
markdownToHtml,
markdownToHtmlWithHighlighting,
} from '../../lib/markdown';
-import { cn } from '../../lib/classname';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
-import TextareaAutosize from 'react-textarea-autosize';
export type AllowedAIChatRole = 'user' | 'assistant';
export type AIChatHistoryType = {
@@ -142,7 +142,7 @@ export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) {
return;
}
- await readAICourseLessonStream(reader, {
+ await readStream(reader, {
onStream: async (content) => {
flushSync(() => {
setStreamedMessage(content);
diff --git a/src/components/GenerateCourse/AICourseLimit.tsx b/src/components/GenerateCourse/AICourseLimit.tsx
index db57beda3..5dac1a284 100644
--- a/src/components/GenerateCourse/AICourseLimit.tsx
+++ b/src/components/GenerateCourse/AICourseLimit.tsx
@@ -1,9 +1,9 @@
import { useQuery } from '@tanstack/react-query';
+import { Gift, Info } from 'lucide-react';
+import { getPercentage } from '../../lib/number';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
-import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
-import { getPercentage } from '../../helper/number';
-import { Gift, Info } from 'lucide-react';
+import { queryClient } from '../../stores/query-client';
type AICourseLimitProps = {
onUpgrade: () => void;
@@ -33,7 +33,7 @@ export function AICourseLimit(props: AICourseLimitProps) {
// has consumed 80% of the limit
const isNearLimit = used >= limit * 0.8;
- const isPaidUser = userBillingDetails.status !== 'none';
+ const isPaidUser = userBillingDetails.status === 'active';
return (
<>
diff --git a/src/components/GenerateCourse/AICourseModuleView.tsx b/src/components/GenerateCourse/AICourseModuleView.tsx
index 34e07a91b..735d8d3c8 100644
--- a/src/components/GenerateCourse/AICourseModuleView.tsx
+++ b/src/components/GenerateCourse/AICourseModuleView.tsx
@@ -8,7 +8,7 @@ import {
XIcon,
} from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
-import { readAICourseLessonStream } from '../../helper/read-stream';
+import { readStream } from '../../lib/ai';
import { cn } from '../../lib/classname';
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import {
@@ -136,7 +136,7 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
setIsLoading(false);
setIsGenerating(true);
- await readAICourseLessonStream(reader, {
+ await readStream(reader, {
onStream: async (result) => {
if (abortController.signal.aborted) {
return;
diff --git a/src/components/GenerateCourse/AILimitsPopup.tsx b/src/components/GenerateCourse/AILimitsPopup.tsx
index 7c87c6009..79244940c 100644
--- a/src/components/GenerateCourse/AILimitsPopup.tsx
+++ b/src/components/GenerateCourse/AILimitsPopup.tsx
@@ -24,7 +24,7 @@ export function AILimitsPopup(props: AILimitsPopupProps) {
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
- const isPaidUser = userBillingDetails?.status !== 'none';
+ const isPaidUser = userBillingDetails?.status === 'active';
return (
{
- const { term, difficulty } = options;
+ const { term, difficulty, isForce } = options;
if (!isLoggedIn()) {
window.location.href = '/ai-tutor';
return;
}
- setIsLoading(true);
- setCourse({
- title: '',
- modules: [],
- difficulty: '',
+ await generateCourse({
+ term,
+ difficulty,
+ onCourseIdChange: setCourseId,
+ onCourseSlugChange: setCourseSlug,
+ onCourseChange: setCourse,
+ onLoadingChange: setIsLoading,
+ onError: setError,
+ isForce,
});
- setError('');
-
- try {
- const response = await fetch(
- `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-course`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- keyword: term,
- difficulty,
- }),
- credentials: 'include',
- },
- );
-
- if (!response.ok) {
- const data = await response.json();
- console.error(
- 'Error generating course:',
- data?.message || 'Something went wrong',
- );
- setIsLoading(false);
- setError(data?.message || 'Something went wrong');
- return;
- }
-
- const reader = response.body?.getReader();
-
- if (!reader) {
- console.error('Failed to get reader from response');
- setError('Something went wrong');
- setIsLoading(false);
- return;
- }
-
- const COURSE_ID_REGEX = new RegExp('@COURSEID:(\\w+)@');
- const COURSE_SLUG_REGEX = new RegExp(/@COURSESLUG:([\w-]+)@/);
-
- await readAICourseStream(reader, {
- onStream: (result) => {
- if (result.includes('@COURSEID') || result.includes('@COURSESLUG')) {
- const courseIdMatch = result.match(COURSE_ID_REGEX);
- const courseSlugMatch = result.match(COURSE_SLUG_REGEX);
- const extractedCourseId = courseIdMatch?.[1] || '';
- const extractedCourseSlug = courseSlugMatch?.[1] || '';
-
- if (extractedCourseSlug) {
- window.history.replaceState(
- {
- courseId,
- courseSlug: extractedCourseSlug,
- term,
- difficulty,
- },
- '',
- `${origin}/ai-tutor/${extractedCourseSlug}`,
- );
- }
-
- result = result
- .replace(COURSE_ID_REGEX, '')
- .replace(COURSE_SLUG_REGEX, '');
-
- setCourseId(extractedCourseId);
- setCourseSlug(extractedCourseSlug);
- }
-
- try {
- const aiCourse = generateAiCourseStructure(result);
- setCourse({
- ...aiCourse,
- difficulty: difficulty || '',
- });
- } catch (e) {
- console.error('Error parsing streamed course content:', e);
- }
- },
- onStreamEnd: (result) => {
- result = result
- .replace(COURSE_ID_REGEX, '')
- .replace(COURSE_SLUG_REGEX, '');
- setIsLoading(false);
- queryClient.invalidateQueries(getAiCourseLimitOptions());
- },
- });
- } catch (error: any) {
- setError(error?.message || 'Something went wrong');
- console.error('Error in course generation:', error);
- setIsLoading(false);
- }
};
useEffect(() => {
@@ -167,7 +77,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
setDifficulty(difficulty);
setIsLoading(true);
- generateCourse({ term, difficulty }).finally(() => {
+ handleGenerateCourse({ term, difficulty }).finally(() => {
setIsLoading(false);
});
};
@@ -184,6 +94,9 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
course={course}
isLoading={isLoading}
error={error}
+ onRegenerateOutline={() => {
+ handleGenerateCourse({ term, difficulty, isForce: true });
+ }}
/>
);
}
diff --git a/src/components/GenerateCourse/GetAICourse.tsx b/src/components/GenerateCourse/GetAICourse.tsx
index d26f613c8..5eb38999f 100644
--- a/src/components/GenerateCourse/GetAICourse.tsx
+++ b/src/components/GenerateCourse/GetAICourse.tsx
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
import { AICourseContent } from './AICourseContent';
import { generateAiCourseStructure } from '../../lib/ai';
import { isLoggedIn } from '../../lib/jwt';
+import { generateCourse } from '../../helper/generate-ai-course';
type GetAICourseProps = {
courseSlug: string;
@@ -14,7 +15,8 @@ export function GetAICourse(props: GetAICourseProps) {
const { courseSlug } = props;
const [isLoading, setIsLoading] = useState(true);
- const { data: aiCourse, error } = useQuery(
+ const [error, setError] = useState('');
+ const { data: aiCourse, error: queryError } = useQuery(
{
...getAiCourseOptions({ aiCourseSlug: courseSlug }),
select: (data) => {
@@ -43,12 +45,27 @@ export function GetAICourse(props: GetAICourseProps) {
}, [aiCourse]);
useEffect(() => {
- if (!error) {
+ if (!queryError) {
return;
}
setIsLoading(false);
- }, [error]);
+ setError(queryError.message);
+ }, [queryError]);
+
+ const handleRegenerateCourse = async () => {
+ if (!aiCourse) {
+ return;
+ }
+
+ await generateCourse({
+ term: aiCourse.keyword,
+ difficulty: aiCourse.difficulty,
+ onLoadingChange: setIsLoading,
+ onError: setError,
+ isForce: true,
+ });
+ };
return (
);
}
diff --git a/src/components/GenerateCourse/RegenerateOutline.tsx b/src/components/GenerateCourse/RegenerateOutline.tsx
new file mode 100644
index 000000000..0bce70b0e
--- /dev/null
+++ b/src/components/GenerateCourse/RegenerateOutline.tsx
@@ -0,0 +1,75 @@
+import { PenSquare, RefreshCcw } from 'lucide-react';
+import { useRef, useState } from 'react';
+import { useOutsideClick } from '../../hooks/use-outside-click';
+import { cn } from '../../lib/classname';
+import { useIsPaidUser } from '../../queries/billing';
+import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
+
+type RegenerateOutlineProps = {
+ onRegenerateOutline: () => void;
+};
+
+export function RegenerateOutline(props: RegenerateOutlineProps) {
+ const { onRegenerateOutline } = props;
+
+ const [isDropdownVisible, setIsDropdownVisible] = useState(false);
+ const [showUpgradeModal, setShowUpgradeModal] = useState(false);
+ const ref = useRef(null);
+
+ const isPaidUser = useIsPaidUser();
+
+ useOutsideClick(ref, () => setIsDropdownVisible(false));
+
+ return (
+ <>
+ {showUpgradeModal && (
+ {
+ setShowUpgradeModal(false);
+ }}
+ />
+ )}
+
+
+
+ {isDropdownVisible && (
+
+
+
+
+ )}
+
+ >
+ );
+}
diff --git a/src/components/GenerateCourse/UserCoursesList.tsx b/src/components/GenerateCourse/UserCoursesList.tsx
index e6e10a50d..925c882f5 100644
--- a/src/components/GenerateCourse/UserCoursesList.tsx
+++ b/src/components/GenerateCourse/UserCoursesList.tsx
@@ -30,7 +30,7 @@ export function UserCoursesList(props: UserCoursesListProps) {
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
- const isPaidUser = userBillingDetails?.status !== 'none';
+ const isPaidUser = userBillingDetails?.status !== 'active';
const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery(
listUserAiCoursesOptions(),
diff --git a/src/components/GenerateCourse/re-generate b/src/components/GenerateCourse/re-generate
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/components/GenerateRoadmap/GenerateRoadmap.tsx b/src/components/GenerateRoadmap/GenerateRoadmap.tsx
index ccb2cf16f..945fc2a35 100644
--- a/src/components/GenerateRoadmap/GenerateRoadmap.tsx
+++ b/src/components/GenerateRoadmap/GenerateRoadmap.tsx
@@ -11,7 +11,6 @@ import { useToast } from '../../hooks/use-toast';
import { generateAIRoadmapFromText } from '../../../editor/utils/roadmap-generator';
import { renderFlowJSON } from '../../../editor/renderer/renderer';
import { replaceChildren } from '../../lib/dom';
-import { readAIRoadmapStream } from '../../helper/read-stream';
import {
getOpenAIKey,
isLoggedIn,
@@ -31,7 +30,7 @@ import { showLoginPopup } from '../../lib/popup.ts';
import { cn } from '../../lib/classname.ts';
import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx';
import { AIRoadmapAlert } from './AIRoadmapAlert.tsx';
-import { IS_KEY_ONLY_ROADMAP_GENERATION } from '../../lib/ai.ts';
+import { IS_KEY_ONLY_ROADMAP_GENERATION, readAIRoadmapStream } from '../../lib/ai.ts';
import { AITermSuggestionInput } from './AITermSuggestionInput.tsx';
import { IncreaseRoadmapLimit } from './IncreaseRoadmapLimit.tsx';
import { AuthenticationForm } from '../AuthenticationFlow/AuthenticationForm.tsx';
diff --git a/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx b/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx
index 560d9e940..33938800a 100644
--- a/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx
+++ b/src/components/GenerateRoadmap/RoadmapTopicDetail.tsx
@@ -3,13 +3,13 @@ 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, User, UserRound, X } from 'lucide-react';
+import { Ban, Cog, Contact, FileText, X } from 'lucide-react';
import { Spinner } from '../ReactIcons/Spinner';
import type { RoadmapNodeDetails } from './GenerateRoadmap';
import { getOpenAIKey, isLoggedIn, removeAuthToken } from '../../lib/jwt';
-import { readAIRoadmapContentStream } from '../../helper/read-stream';
import { cn } from '../../lib/classname';
import { showLoginPopup } from '../../lib/popup';
+import { readAIRoadmapContentStream } from '../../lib/ai';
type RoadmapTopicDetailProps = RoadmapNodeDetails & {
onClose?: () => void;
diff --git a/src/components/Navigation/Navigation.astro b/src/components/Navigation/Navigation.astro
index 1b87eefd4..bebe897f9 100644
--- a/src/components/Navigation/Navigation.astro
+++ b/src/components/Navigation/Navigation.astro
@@ -49,7 +49,10 @@ import { CourseAnnouncement } from '../SQLCourse/CourseAnnouncement';
-
+
Teams
diff --git a/src/components/UserPublicProfile/UserProfileRoadmap.tsx b/src/components/UserPublicProfile/UserProfileRoadmap.tsx
index 0d42ba79d..5dfb69ab4 100644
--- a/src/components/UserPublicProfile/UserProfileRoadmap.tsx
+++ b/src/components/UserPublicProfile/UserProfileRoadmap.tsx
@@ -2,7 +2,7 @@ import type {
GetUserProfileRoadmapResponse,
GetPublicProfileResponse,
} from '../../api/user';
-import { getPercentage } from '../../helper/number';
+import { getPercentage } from '../../lib/number';
import { PrivateProfileBanner } from './PrivateProfileBanner';
import { UserProfileRoadmapRenderer } from './UserProfileRoadmapRenderer';
diff --git a/src/components/UserPublicProfile/UserPublicProgressStats.tsx b/src/components/UserPublicProfile/UserPublicProgressStats.tsx
index 9b8fc85f5..c9eef634c 100644
--- a/src/components/UserPublicProfile/UserPublicProgressStats.tsx
+++ b/src/components/UserPublicProfile/UserPublicProgressStats.tsx
@@ -1,5 +1,5 @@
-import { getPercentage } from '../../helper/number';
import { getRelativeTimeString } from '../../lib/date';
+import { getPercentage } from '../../lib/number';
type UserPublicProgressStats = {
resourceType: 'roadmap';
diff --git a/src/components/UserPublicProfile/UserPublicProgresses.tsx b/src/components/UserPublicProfile/UserPublicProgresses.tsx
index 1eac8e296..cb5fb2fb1 100644
--- a/src/components/UserPublicProfile/UserPublicProgresses.tsx
+++ b/src/components/UserPublicProfile/UserPublicProgresses.tsx
@@ -1,6 +1,5 @@
import type { GetPublicProfileResponse } from '../../api/user';
-import { UserPublicProgressStats } from './UserPublicProgressStats';
-import { getPercentage } from '../../helper/number.ts';
+import { getPercentage } from '../../lib/number';
type UserPublicProgressesProps = {
userId: string;
@@ -73,15 +72,15 @@ export function UserPublicProgresses(props: UserPublicProgressesProps) {
target="_blank"
key={roadmap.id + counter}
href={`/${roadmap.id}?s=${userId}`}
- className="relative group border-gray-300 flex items-center justify-between rounded-md border bg-white px-3 py-2 text-left text-sm transition-all hover:border-gray-400 overflow-hidden"
+ className="group relative flex items-center justify-between overflow-hidden rounded-md border border-gray-300 bg-white px-3 py-2 text-left text-sm transition-all hover:border-gray-400"
>