feat: course ai token limit

feat/course
Arik Chakma 2 days ago
parent 4248620471
commit 8d5824bc2e
  1. 2
      src/components/CourseAI/CourseAI.tsx
  2. 24
      src/components/CourseAI/CourseAILimit.tsx
  3. 32
      src/components/CourseAI/CourseAIPopover.tsx
  4. 18
      src/hooks/use-course.ts

@ -33,7 +33,7 @@ export function CourseAI(props: CourseAIProps) {
return (
<div className="relative">
<button
className="flex items-center gap-1 rounded-lg border border-black pl-3 pr-4 py-2 text-sm leading-none disabled:opacity-60 hover:bg-black hover:text-white transition-colors"
className="flex items-center gap-1 rounded-lg border border-black py-2 pl-3 pr-4 text-sm leading-none transition-colors hover:bg-black hover:text-white disabled:opacity-60"
onClick={() => setIsOpen(!isOpen)}
>
<Sparkles className="size-4 stroke-[2]" />

@ -0,0 +1,24 @@
import { getPercentage } from '../../helper/number';
import { useCourseAILimit } from '../../hooks/use-course';
export function CourseAILimit() {
const { data: tokenUsage, isLoading } = useCourseAILimit();
if (isLoading || !tokenUsage) {
return (
<div className="h-5 w-1/4 animate-pulse rounded-md bg-zinc-600"></div>
);
}
const percentageUsed = getPercentage(
tokenUsage.usedTokenCount,
tokenUsage.maxTokenCount,
);
return (
<p className="text-sm text-yellow-500">
<strong className="font-medium">{percentageUsed}%</strong> of daily limit
used
</p>
);
}

@ -12,6 +12,8 @@ import { flushSync } from 'react-dom';
import type { AIChatHistoryType, AllowedAIChatRole } from './CourseAI';
import { useToast } from '../../hooks/use-toast';
import { removeAuthToken } from '../../lib/jwt';
import { useCourseAILimit } from '../../hooks/use-course';
import { CourseAILimit } from './CourseAILimit';
type CourseAIPopoverProps = {
courseId: string;
@ -42,17 +44,26 @@ export function CourseAIPopover(props: CourseAIPopoverProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const scrollareaRef = useRef<HTMLDivElement | null>(null);
const [isLoading, setIsLoading] = useState(false);
const {
data: tokenUsage,
isLoading,
refetch: refreshLimit,
} = useCourseAILimit();
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
const [message, setMessage] = useState('');
const [streamedMessage, setStreamedMessage] = useState('');
useOutsideClick(containerRef, onOutsideClick);
const isLimitExceeded =
(tokenUsage?.maxTokenCount || 0) <= (tokenUsage?.usedTokenCount || 0);
const handleChatSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const trimmedMessage = message.trim();
if (!trimmedMessage || isLoading) {
if (!trimmedMessage || isStreamingMessage || isLoading || isLimitExceeded) {
return;
}
@ -81,7 +92,7 @@ export function CourseAIPopover(props: CourseAIPopoverProps) {
};
const completeCourseAIChat = async (messages: AIChatHistoryType[]) => {
setIsLoading(true);
setIsStreamingMessage(true);
const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-course-ai/${courseId}`,
@ -105,7 +116,7 @@ export function CourseAIPopover(props: CourseAIPopoverProps) {
toast.error(data?.message || 'Something went wrong');
setCourseAIChatHistory([...messages].slice(0, messages.length - 1));
setIsLoading(false);
setIsStreamingMessage(false);
// Logout user if token is invalid
if (data.status === 401) {
@ -117,7 +128,7 @@ export function CourseAIPopover(props: CourseAIPopoverProps) {
const reader = response.body?.getReader();
if (!reader) {
setIsLoading(false);
setIsStreamingMessage(false);
toast.error('Something went wrong');
return;
}
@ -141,7 +152,7 @@ export function CourseAIPopover(props: CourseAIPopoverProps) {
flushSync(() => {
setStreamedMessage('');
setIsLoading(false);
setIsStreamingMessage(false);
setCourseAIChatHistory(newMessages);
});
@ -149,7 +160,8 @@ export function CourseAIPopover(props: CourseAIPopoverProps) {
},
});
setIsLoading(false);
refreshLimit();
setIsStreamingMessage(false);
};
useEffect(() => {
@ -163,6 +175,8 @@ export function CourseAIPopover(props: CourseAIPopoverProps) {
>
<div className="flex items-center justify-between gap-2 border-b border-zinc-700 px-4 py-2 text-sm">
<h4 className="text-base font-medium">Roadmap AI</h4>
<CourseAILimit />
</div>
<div
@ -182,7 +196,7 @@ export function CourseAIPopover(props: CourseAIPopoverProps) {
);
})}
{isLoading && !streamedMessage && (
{isStreamingMessage && !streamedMessage && (
<AIChatCard role="assistant" content="Thinking..." />
)}
@ -207,7 +221,7 @@ export function CourseAIPopover(props: CourseAIPopoverProps) {
/>
<button
type="submit"
disabled={isLoading}
disabled={isStreamingMessage || isLoading || isLimitExceeded}
className="flex aspect-square h-full items-center justify-center text-zinc-500 hover:text-zinc-50"
>
<Send className="size-4 stroke-[2.5]" />

@ -56,3 +56,21 @@ export function useCompleteLessonMutation(courseId: string) {
queryClient,
);
}
export type CourseAILimitResponse = {
maxTokenCount: number;
usedTokenCount: number;
};
export function useCourseAILimit() {
return useQuery(
{
queryKey: ['course-ai-limit'],
queryFn: async () => {
return httpGet<CourseAILimitResponse>('/v1-course-ai-limit');
},
enabled: isLoggedIn(),
},
queryClient,
);
}

Loading…
Cancel
Save