feat: ai course subjects

feat/topic-chat
Arik Chakma 2 weeks ago
parent cb649055d0
commit f0cc7421fb
  1. 2
      src/components/TopicDetail/ResourceListSeparator.tsx
  2. 68
      src/components/TopicDetail/TopicDetailAI.tsx
  3. 26
      src/queries/roadmap-tree.ts

@ -27,7 +27,7 @@ export function ResourceListSeparator(props: ResourceSeparatorProps) {
{Icon && <Icon className="inline-block h-3 w-3 fill-current" />}
{text}
</span>
<hr className="absolute inset-x-0 grow border-current" />
<span className="absolute inset-x-0 h-px w-full grow bg-current" />
</p>
);
}

@ -26,6 +26,7 @@ import { readStream } from '../../lib/ai';
import { markdownToHtmlWithHighlighting } from '../../lib/markdown';
import type { ResourceType } from '../../lib/resource-progress';
import { getPercentage } from '../../lib/number';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
type TopicDetailAIProps = {
resourceId: string;
@ -51,6 +52,10 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const scrollareaRef = useRef<HTMLDivElement>(null);
const sanitizedTopicId = topicId?.includes('@')
? topicId?.split('@')?.[1]
: topicId;
const toast = useToast();
const [message, setMessage] = useState('');
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
@ -64,6 +69,20 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const { data: roadmapTreeMapping, isLoading: isRoadmapTreeMappingLoading } =
useQuery(
{
...roadmapTreeMappingOptions(resourceId),
select: (data) => {
const node = data.find(
(mapping) => mapping.nodeId === sanitizedTopicId,
);
return node;
},
},
queryClient,
);
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const isPaidUser = userBillingDetails?.status === 'active';
@ -109,10 +128,6 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
try {
setIsStreamingMessage(true);
const sanitizedTopicId = topicId?.includes('@')
? topicId?.split('@')?.[1]
: topicId;
const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-topic-detail-chat`,
{
@ -194,15 +209,53 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
scrollToBottom();
}, []);
const isDataLoading = isLoading || isBillingDetailsLoading;
const isDataLoading =
isLoading || isBillingDetailsLoading || isRoadmapTreeMappingLoading;
const usagePercentage = getPercentage(
tokenUsage?.used || 0,
tokenUsage?.limit || 0,
);
const hasSubjects =
roadmapTreeMapping?.subjects && roadmapTreeMapping?.subjects?.length > 0;
return (
<div className="mt-4 flex grow flex-col overflow-hidden rounded-lg border border-gray-200">
{hasSubjects && (
<div className="border-b border-gray-200 px-4 py-2">
<h4 className="flex items-center gap-2 text-base">
Complete the following courses on AI Tutor
</h4>
<div className="mt-2.5 flex flex-wrap gap-1 text-sm">
{roadmapTreeMapping?.subjects?.map((subject) => {
return (
<div className="mt-4 flex grow flex-col overflow-hidden rounded-lg border">
<div className="flex items-center justify-between gap-2 border-b border-gray-200 px-4 py-2 text-sm">
<a
key={subject}
target="_blank"
href={`/ai/search?term=${subject}&difficulty=beginner`}
className="rounded-md border px-1.5"
>
{subject}
</a>
);
})}
</div>
</div>
)}
<div
className={cn(
'flex items-center justify-between gap-2 border-gray-200 px-4 py-2 text-sm',
!hasSubjects && 'border-b',
)}
>
{hasSubjects && (
<span className="flex items-center gap-2 text-base">
or start chatting with AI
</span>
)}
{!hasSubjects && (
<h4 className="flex items-center gap-2 text-base font-medium">
<BotIcon
className="relative -top-[1px] size-5 shrink-0 text-black"
@ -210,6 +263,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
/>
AI Tutor
</h4>
)}
{!isDataLoading && !isPaidUser && (
<p className="text-sm text-gray-500">

@ -0,0 +1,26 @@
import { queryOptions } from '@tanstack/react-query';
import { httpGet } from '../lib/query-http';
export interface RoadmapTreeDocument {
_id?: string;
roadmapId: string;
mapping: {
_id?: string;
nodeId: string;
text: string;
subjects: string[];
}[];
createdAt: Date;
updatedAt: Date;
}
export function roadmapTreeMappingOptions(roadmapId: string) {
return queryOptions({
queryKey: ['roadmap-tree-mapping', { roadmapId }],
queryFn: () => {
return httpGet<RoadmapTreeDocument['mapping']>(
`${import.meta.env.PUBLIC_API_URL}/v1-roadmap-tree-mapping/${roadmapId}`,
);
},
});
}
Loading…
Cancel
Save