feat: usage limit

feat/ai-courses
Arik Chakma 2 months ago
parent c9a402a233
commit e21e3ab331
  1. 3
      src/components/GenerateCourse/AICourseContent.tsx
  2. 102
      src/components/GenerateCourse/AICourseLimit.tsx
  3. 6
      src/components/GenerateCourse/AICourseModuleView.tsx
  4. 3
      src/components/GenerateCourse/GenerateAICourse.tsx
  5. 1
      src/lib/markdown.ts
  6. 19
      src/queries/ai-course.ts

@ -17,6 +17,7 @@ import { AICourseModuleList } from './AICourseModuleList';
import { AICourseModuleView } from './AICourseModuleView';
import { slugify } from '../../lib/slugger';
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { AICourseLimit } from './AICourseLimit';
type AICourseContentProps = {
courseSlug?: string;
@ -137,6 +138,8 @@ export function AICourseContent(props: AICourseContentProps) {
</h1>
</div>
<div className="flex items-center gap-2">
<AICourseLimit />
{viewMode === 'module' && (
<button
onClick={() => {

@ -0,0 +1,102 @@
import { useQuery } from '@tanstack/react-query';
import {
BookIcon,
BookOpenIcon,
MessageCircleQuestionIcon,
ChevronDownIcon,
ClockIcon,
} from 'lucide-react';
import { useState, useRef } from 'react';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { useOutsideClick } from '../../hooks/use-outside-click';
export function AICourseLimit() {
const containerRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const { data: limits, isLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
useOutsideClick(containerRef, () => {
setIsOpen(false);
});
if (isLoading || !limits) {
return (
<div className="h-[34px] w-[243px] animate-pulse rounded-lg border border-gray-200 bg-gray-200"></div>
);
}
const {
used: courseUsed,
limit: courseLimit,
lessonUsed,
lessonLimit,
} = limits;
const coursePercentage = Math.round((courseUsed / courseLimit) * 100);
const lessonPercentage = Math.round((lessonUsed / lessonLimit) * 100);
return (
<div className="relative" ref={containerRef}>
<button
className="flex cursor-pointer items-center rounded-lg border border-gray-200 px-2 py-1.5 text-sm hover:bg-gray-50"
onClick={() => setIsOpen(!isOpen)}
>
<div className="mr-3 flex items-center gap-1.5">
<BookIcon className="h-4 w-4" />
{coursePercentage}%
</div>
<div className="mr-3 flex items-center gap-1.5">
<BookOpenIcon className="h-4 w-4" />
{lessonPercentage}%
</div>
<span className="mr-1">of daily limits</span>
<ChevronDownIcon className="h-4 w-4" />
</button>
{isOpen && (
<div className="absolute right-0 top-full w-full translate-y-1 overflow-hidden rounded-lg border border-gray-200 bg-white p-2 pt-0 text-sm shadow-lg">
<div className="-mx-2">
<div className="relative overflow-hidden">
<div className="relative z-10 flex items-center gap-2 border-b border-b-gray-200 px-2 py-1">
<BookIcon className="size-3.5" />
{courseUsed} of {courseLimit} courses used
</div>
<div
className="absolute inset-0 bg-gray-100"
style={{
width: `${coursePercentage}%`,
}}
/>
</div>
<div className="relative overflow-hidden">
<div className="relative z-10 flex items-center gap-2 border-b border-b-gray-200 px-2 py-1">
<BookOpenIcon className="size-3.5" />
{lessonUsed} of {lessonLimit} lessons used
</div>
<div
className="absolute inset-0 bg-gray-100"
style={{
width: `${lessonPercentage}%`,
}}
/>
</div>
</div>
<div className="mt-2 flex items-center justify-center gap-2 text-gray-500">
<ClockIcon className="size-3.5" />
Limit resets every 24 hours
</div>
</div>
)}
</div>
);
}

@ -11,7 +11,10 @@ import { useMutation, useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { httpPost } from '../../lib/query-http';
import { slugify } from '../../lib/slugger';
import { getAiCourseProgressOptions } from '../../queries/ai-course';
import {
getAiCourseLimitOptions,
getAiCourseProgressOptions,
} from '../../queries/ai-course';
type AICourseModuleViewProps = {
courseSlug: string;
@ -133,6 +136,7 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
}
setLessonHtml(await markdownToHtmlWithHighlighting(result));
queryClient.invalidateQueries(getAiCourseLimitOptions());
setIsGenerating(false);
},
});

@ -5,6 +5,8 @@ import { showLoginPopup } from '../../lib/popup';
import { generateAiCourseStructure, type AiCourse } from '../../lib/ai';
import { readAICourseStream } from '../../helper/read-stream';
import { AICourseContent } from './AICourseContent';
import { queryClient } from '../../stores/query-client';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
type GenerateAICourseProps = {};
@ -144,6 +146,7 @@ export function GenerateAICourse(props: GenerateAICourseProps) {
.replace(COURSE_ID_REGEX, '')
.replace(COURSE_SLUG_REGEX, '');
setIsLoading(false);
queryClient.invalidateQueries(getAiCourseLimitOptions());
},
});
} catch (error: any) {

@ -91,7 +91,6 @@ export async function markdownToHtmlWithHighlighting(markdown: string) {
return self.renderToken(tokens, idx, options);
};
// @ts-ignore
markdownItAsync.renderer.rules.link_open = function (
tokens,
idx,

@ -65,3 +65,22 @@ export function getAiCourseOptions(params: GetAICourseParams) {
},
};
}
type GetAICourseLimitParams = {};
type GetAICourseLimitResponse = {
used: number;
limit: number;
lessonUsed: number;
lessonLimit: number;
};
export function getAiCourseLimitOptions() {
return {
queryKey: ['ai-course-limit'],
queryFn: () => {
return httpGet<GetAICourseLimitResponse>(`/v1-get-ai-course-limit`);
},
enabled: !!isLoggedIn(),
};
}

Loading…
Cancel
Save