feat/course
Arik Chakma 3 weeks ago
parent 9e08bd08f6
commit 47a0537480
  1. 7
      src/components/Course/CourseLayout.tsx
  2. 32
      src/components/CourseAI/CourseAI.tsx
  3. 137
      src/components/CourseAI/CourseAIPopover.tsx

@ -11,6 +11,7 @@ import { currentLesson } from '../../stores/course';
import { getPercentage } from '../../helper/number'; import { getPercentage } from '../../helper/number';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { CourseNotes } from '../CourseNotes/CourseNotes'; import { CourseNotes } from '../CourseNotes/CourseNotes';
import { CourseAI } from '../CourseAI/CourseAI';
export type CourseLayoutProps = { export type CourseLayoutProps = {
children: React.ReactNode; children: React.ReactNode;
@ -140,6 +141,12 @@ export function CourseLayout(props: CourseLayoutProps) {
{activeChapterId && activeLessonId && ( {activeChapterId && activeLessonId && (
<footer className="flex items-center justify-between border-t border-zinc-800 px-4"> <footer className="flex items-center justify-between border-t border-zinc-800 px-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CourseAI
courseId={activeCourseId}
currentChapterId={activeChapterId}
currentLessonId={activeLessonId}
chapters={chapters}
/>
<CourseNotes <CourseNotes
courseId={activeCourseId} courseId={activeCourseId}
chapters={chapters} chapters={chapters}

@ -0,0 +1,32 @@
import { Loader2, MessageSquareCode, Sparkles } from 'lucide-react';
import type { ChapterFileType } from '../../lib/course';
import { useState } from 'react';
import { CourseAIPopover } from './CourseAIPopover';
type CourseAIProps = {
courseId: string;
currentChapterId: string;
currentLessonId: string;
chapters: ChapterFileType[];
};
export function CourseAI(props: CourseAIProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="relative">
<button
className="flex items-center gap-1 rounded-lg border border-zinc-800 px-2 py-1.5 text-sm leading-none disabled:opacity-60"
onClick={() => setIsOpen(!isOpen)}
>
<Sparkles className="size-4 stroke-[2.5]" />
Ask AI
</button>
{isOpen && (
<CourseAIPopover {...props} onOutsideClick={() => setIsOpen(false)} />
)}
</div>
);
}

@ -0,0 +1,137 @@
import { useMemo, useRef, useState } from 'react';
import { useListCourseNote } from '../../hooks/use-course-note';
import type { ChapterFileType } from '../../lib/course';
import { Bot, Loader2 } from 'lucide-react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { cn } from '../../lib/classname';
import { markdownToHtml } from '../../lib/markdown';
type CourseAIPopoverProps = {
courseId: string;
currentChapterId: string;
currentLessonId: string;
chapters: ChapterFileType[];
onOutsideClick?: () => void;
};
export function CourseAIPopover(props: CourseAIPopoverProps) {
const {
courseId,
chapters,
currentChapterId,
currentLessonId,
onOutsideClick,
} = props;
const containerRef = useRef<HTMLDivElement | null>(null);
useOutsideClick(containerRef, onOutsideClick);
return (
<div
className="absolute bottom-full left-0 z-10 flex h-[65dvh] w-[420px] -translate-y-2 flex-col overflow-hidden rounded-xl border border-zinc-700 bg-zinc-800 text-white"
ref={containerRef}
>
<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>
</div>
<div className="relative grow overflow-y-auto [scrollbar-color:#3f3f46_#27272a;]">
<div className="absolute inset-0 flex flex-col">
<div className="flex flex-col justify-end gap-2 p-2">
<AIChatCard
type="system"
message="Hey, how can I help you today? 🤖"
/>
<AIChatCard
type="user"
message={`What's wrong with this query?
\`\`\`sql
SELECT *
FROM users
WHERE id = 1
\`\`\``}
/>
<AIChatCard
type="system"
message={`Looks like you're missing a semicolon at the end of the query. Try this:
\`\`\`sql
SELECT *
FROM users
WHERE id = 1;
\`\`\``}
/>
<AIChatCard type="user" message={`Got it! Thanks! 🙏`} />
<AIChatCard
type="system"
message={`You're welcome! If you have any other questions, feel free to ask. 🤖`}
/>
<AIChatCard
type="system"
message={`Looks like you're missing a semicolon at the end of the query. Try this:
\`\`\`sql
SELECT *
FROM users
WHERE id = 1;
\`\`\``}
/>
<AIChatCard type="user" message={`Got it! Thanks! 🙏`} />
<AIChatCard
type="system"
message={`You're welcome! If you have any other questions, feel free to ask. 🤖`}
/>
</div>
</div>
</div>
<input
className="h-[41px] w-full border-t border-zinc-700 bg-zinc-800 px-4 py-2 text-sm text-white focus:outline-none"
placeholder="Ask AI anything about the course..."
/>
</div>
);
}
type AIChatCardProps = {
type: 'user' | 'system';
message: string;
};
function AIChatCard(props: AIChatCardProps) {
const { type, message } = props;
const html = useMemo(() => {
const html = markdownToHtml(message, false);
// FIXME: Sanitize HTML
return html;
}, [message]);
return (
<div
className={cn(
'flex items-start gap-2.5 rounded-xl p-3',
type === 'user' ? 'bg-zinc-500/30' : 'bg-yellow-500/30',
)}
>
<div
className={cn(
'flex size-6 shrink-0 items-center justify-center rounded-full',
type === 'user'
? 'bg-zinc-500 text-zinc-50'
: 'bg-yellow-500 text-zinc-950',
)}
>
<Bot className="size-4 stroke-[2.5]" />
</div>
<div
className="course-content prose prose-sm prose-invert w-full text-sm text-white"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
}
Loading…
Cancel
Save