feat: ai course chat (#8426)
* feat: ai course chat * wip: remove old code * wip * feat: responsiveness of ai chat * fix: key warning * feat: make chat resizeable * wip * wip: default questions * wip * fix: fixed position * fix: hide button * Fix scroll issue * Improve questions UI * Refactor UI * Add close icon * Update UI for course chat * Close AI chat question --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>pull/8446/head
parent
981af58fa9
commit
dfd54b35b0
9 changed files with 561 additions and 354 deletions
@ -1,74 +0,0 @@ |
|||||||
import { ArrowRightIcon, BotIcon } from 'lucide-react'; |
|
||||||
import { useState } from 'react'; |
|
||||||
import { |
|
||||||
AICourseFollowUpPopover, |
|
||||||
type AIChatHistoryType, |
|
||||||
} from './AICourseFollowUpPopover'; |
|
||||||
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; |
|
||||||
|
|
||||||
type AICourseFollowUpProps = { |
|
||||||
courseSlug: string; |
|
||||||
moduleTitle: string; |
|
||||||
lessonTitle: string; |
|
||||||
}; |
|
||||||
|
|
||||||
export function AICourseFollowUp(props: AICourseFollowUpProps) { |
|
||||||
const { courseSlug, moduleTitle, lessonTitle } = props; |
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false); |
|
||||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false); |
|
||||||
|
|
||||||
const [courseAIChatHistory, setCourseAIChatHistory] = useState< |
|
||||||
AIChatHistoryType[] |
|
||||||
>([ |
|
||||||
{ |
|
||||||
role: 'assistant', |
|
||||||
content: |
|
||||||
'Hey, I am your AI instructor. Here are some examples of what you can ask me about 🤖', |
|
||||||
isDefault: true, |
|
||||||
}, |
|
||||||
]); |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className="relative"> |
|
||||||
<button |
|
||||||
className="mt-4 flex w-full items-center gap-2 rounded-lg border border-yellow-300 bg-yellow-100 p-4 hover:bg-yellow-200 max-lg:mt-3 max-lg:text-sm" |
|
||||||
onClick={() => setIsOpen(true)} |
|
||||||
> |
|
||||||
<BotIcon className="h-4 w-4" /> |
|
||||||
<span>Ask AI some follow up questions</span> |
|
||||||
|
|
||||||
<ArrowRightIcon className="ml-auto h-4 w-4 max-sm:hidden" /> |
|
||||||
</button> |
|
||||||
|
|
||||||
{showUpgradeModal && ( |
|
||||||
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} /> |
|
||||||
)} |
|
||||||
|
|
||||||
{isOpen && ( |
|
||||||
<AICourseFollowUpPopover |
|
||||||
courseSlug={courseSlug} |
|
||||||
moduleTitle={moduleTitle} |
|
||||||
lessonTitle={lessonTitle} |
|
||||||
courseAIChatHistory={courseAIChatHistory} |
|
||||||
setCourseAIChatHistory={setCourseAIChatHistory} |
|
||||||
onUpgradeClick={() => { |
|
||||||
setIsOpen(false); |
|
||||||
setShowUpgradeModal(true); |
|
||||||
}} |
|
||||||
onOutsideClick={() => { |
|
||||||
if (!isOpen) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
setIsOpen(false); |
|
||||||
}} |
|
||||||
/> |
|
||||||
)} |
|
||||||
|
|
||||||
{isOpen && ( |
|
||||||
<div className="pointer-events-none fixed inset-0 z-50 bg-black/50" /> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
@ -0,0 +1,19 @@ |
|||||||
|
import { cn } from '../../lib/classname'; |
||||||
|
|
||||||
|
type AICourseFooterProps = { |
||||||
|
className?: string; |
||||||
|
}; |
||||||
|
export function AICourseFooter(props: AICourseFooterProps) { |
||||||
|
const { className } = props; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
'mx-auto mb-10 mt-5 text-center text-sm text-gray-400', |
||||||
|
className, |
||||||
|
)} |
||||||
|
> |
||||||
|
AI can make mistakes, check important info. |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
import { GripVertical } from 'lucide-react'; |
||||||
|
import * as ResizablePrimitive from 'react-resizable-panels'; |
||||||
|
import { cn } from '../../lib/classname'; |
||||||
|
|
||||||
|
const ResizablePanelGroup = ({ |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => ( |
||||||
|
<ResizablePrimitive.PanelGroup |
||||||
|
className={cn( |
||||||
|
'flex h-full w-full data-[panel-group-direction=vertical]:flex-col', |
||||||
|
className, |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
); |
||||||
|
|
||||||
|
const ResizablePanel = ResizablePrimitive.Panel; |
||||||
|
|
||||||
|
const ResizableHandle = ({ |
||||||
|
withHandle, |
||||||
|
className, |
||||||
|
...props |
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { |
||||||
|
withHandle?: boolean; |
||||||
|
}) => ( |
||||||
|
<ResizablePrimitive.PanelResizeHandle |
||||||
|
className={cn( |
||||||
|
'relative flex w-px items-center justify-center bg-gray-200 after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90', |
||||||
|
className, |
||||||
|
)} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{withHandle && ( |
||||||
|
<div className="z-10 flex h-[30px] w-3 items-center justify-center rounded-sm bg-gray-200 text-black hover:bg-gray-300"> |
||||||
|
<GripVertical className="size-5" /> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</ResizablePrimitive.PanelResizeHandle> |
||||||
|
); |
||||||
|
|
||||||
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; |
Loading…
Reference in new issue