parent
9e08bd08f6
commit
47a0537480
3 changed files with 176 additions and 0 deletions
@ -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…
Reference in new issue