wip: autogrow textarea & examples

feat/ai-courses
Arik Chakma 2 months ago
parent 8897e43b21
commit 9fb00a617f
  1. 1
      package.json
  2. 77
      pnpm-lock.yaml
  3. 89
      src/components/GenerateCourse/AICourseFollowUpPopover.tsx

@ -64,6 +64,7 @@
"react-calendar-heatmap": "^1.9.0", "react-calendar-heatmap": "^1.9.0",
"react-confetti": "^6.1.0", "react-confetti": "^6.1.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-textarea-autosize": "^8.5.7",
"react-tooltip": "^5.28.0", "react-tooltip": "^5.28.0",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",

@ -113,6 +113,9 @@ importers:
react-dom: react-dom:
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1(react@18.3.1) version: 18.3.1(react@18.3.1)
react-textarea-autosize:
specifier: ^8.5.7
version: 8.5.7(@types/react@18.3.18)(react@18.3.1)
react-tooltip: react-tooltip:
specifier: ^5.28.0 specifier: ^5.28.0
version: 5.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 5.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -356,6 +359,10 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
'@babel/runtime@7.26.9':
resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==}
engines: {node: '>=6.9.0'}
'@babel/template@7.25.9': '@babel/template@7.25.9':
resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -2901,6 +2908,12 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
react-textarea-autosize@8.5.7:
resolution: {integrity: sha512-2MqJ3p0Jh69yt9ktFIaZmORHXw4c4bxSIhCeWiFwmJ9EYKgLmuNII3e9c9b2UO+ijl4StnpZdqpxNIhTdHvqtQ==}
engines: {node: '>=10'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-tooltip@5.28.0: react-tooltip@5.28.0:
resolution: {integrity: sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==} resolution: {integrity: sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==}
peerDependencies: peerDependencies:
@ -2924,6 +2937,9 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
regex-recursion@5.1.1: regex-recursion@5.1.1:
resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==}
@ -3304,6 +3320,33 @@ packages:
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>= 4.21.0'
use-composed-ref@1.4.0:
resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-isomorphic-layout-effect@1.2.0:
resolution: {integrity: sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-latest@1.3.0:
resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-sync-external-store@1.4.0: use-sync-external-store@1.4.0:
resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==}
peerDependencies: peerDependencies:
@ -3662,6 +3705,10 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@babel/runtime@7.26.9':
dependencies:
regenerator-runtime: 0.14.1
'@babel/template@7.25.9': '@babel/template@7.25.9':
dependencies: dependencies:
'@babel/code-frame': 7.26.2 '@babel/code-frame': 7.26.2
@ -6299,6 +6346,15 @@ snapshots:
react-refresh@0.14.2: {} react-refresh@0.14.2: {}
react-textarea-autosize@8.5.7(@types/react@18.3.18)(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.9
react: 18.3.1
use-composed-ref: 1.4.0(@types/react@18.3.18)(react@18.3.1)
use-latest: 1.3.0(@types/react@18.3.18)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
react-tooltip@5.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-tooltip@5.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@floating-ui/dom': 1.6.13 '@floating-ui/dom': 1.6.13
@ -6332,6 +6388,8 @@ snapshots:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
regenerator-runtime@0.14.1: {}
regex-recursion@5.1.1: regex-recursion@5.1.1:
dependencies: dependencies:
regex: 5.1.1 regex: 5.1.1
@ -6872,6 +6930,25 @@ snapshots:
escalade: 3.2.0 escalade: 3.2.0
picocolors: 1.1.1 picocolors: 1.1.1
use-composed-ref@1.4.0(@types/react@18.3.18)(react@18.3.1):
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
use-isomorphic-layout-effect@1.2.0(@types/react@18.3.18)(react@18.3.1):
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
use-latest@1.3.0(@types/react@18.3.18)(react@18.3.1):
dependencies:
react: 18.3.1
use-isomorphic-layout-effect: 1.2.0(@types/react@18.3.18)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.18
use-sync-external-store@1.4.0(react@18.3.1): use-sync-external-store@1.4.0(react@18.3.1):
dependencies: dependencies:
react: 18.3.1 react: 18.3.1

@ -19,6 +19,7 @@ import { markdownToHtml } from '../../lib/markdown';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { getAiCourseLimitOptions } from '../../queries/ai-course'; import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client'; import { queryClient } from '../../stores/query-client';
import TextareaAutosize from 'react-textarea-autosize';
export type AllowedAIChatRole = 'user' | 'assistant'; export type AllowedAIChatRole = 'user' | 'assistant';
export type AIChatHistoryType = { export type AIChatHistoryType = {
@ -198,11 +199,23 @@ export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) {
<div className="flex flex-col justify-end gap-2 px-3 py-2"> <div className="flex flex-col justify-end gap-2 px-3 py-2">
{courseAIChatHistory.map((chat, index) => { {courseAIChatHistory.map((chat, index) => {
return ( return (
<>
<AIChatCard <AIChatCard
key={index} key={index}
role={chat.role} role={chat.role}
content={chat.content} content={chat.content}
/> />
{chat.isDefault && (
<div className="mb-1 mt-0.5">
<div className="grid grid-cols-2 gap-2">
{capabilities.map((capability, index) => (
<CapabilityCard key={index} {...capability} />
))}
</div>
</div>
)}
</>
); );
})} })}
@ -219,7 +232,7 @@ export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) {
</div> </div>
<form <form
className="relative flex h-[41px] items-center border-t border-gray-200 text-sm" className="relative flex items-start border-t border-gray-200 text-sm"
onSubmit={handleChatSubmit} onSubmit={handleChatSubmit}
> >
{isLimitExceeded && ( {isLimitExceeded && (
@ -228,17 +241,22 @@ export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) {
<p>You have reached the AI usage limit for today.</p> <p>You have reached the AI usage limit for today.</p>
</div> </div>
)} )}
<input <TextareaAutosize
className="h-full grow bg-transparent px-4 py-2 focus:outline-none" className="h-full min-h-[41px] grow resize-none bg-transparent px-4 py-2 focus:outline-none"
placeholder="Ask AI anything about the lesson..." placeholder="Ask AI anything about the lesson..."
value={message} value={message}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
autoFocus={true} autoFocus={true}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
handleChatSubmit(e as unknown as FormEvent<HTMLFormElement>);
}
}}
/> />
<button <button
type="submit" type="submit"
disabled={isStreamingMessage || isLimitExceeded} disabled={isStreamingMessage || isLimitExceeded}
className="flex aspect-square h-full items-center justify-center text-zinc-500 hover:text-black" className="flex aspect-square size-[41px] items-center justify-center text-zinc-500 hover:text-black"
> >
<Send className="size-4 stroke-[2.5]" /> <Send className="size-4 stroke-[2.5]" />
</button> </button>
@ -285,3 +303,66 @@ function AIChatCard(props: AIChatCardProps) {
</div> </div>
); );
} }
type CapabilityCardProps = {
icon: React.ReactNode;
title: string;
description: string;
className?: string;
};
function CapabilityCard({
icon,
title,
description,
className,
}: CapabilityCardProps) {
return (
<div
className={cn(
'flex flex-col gap-2 rounded-lg bg-yellow-500/10 p-3',
className,
)}
>
<div className="flex items-center gap-2">
{icon}
<span className="text-[13px] font-medium leading-none text-black">
{title}
</span>
</div>
<p className="text-[12px] leading-normal text-gray-600">{description}</p>
</div>
);
}
const capabilities = [
{
icon: (
<HelpCircle
className="size-4 shrink-0 text-yellow-600"
strokeWidth={2.5}
/>
),
title: 'Clarify Concepts',
description: "If you don't understand a concept, ask me to clarify it",
},
{
icon: (
<BookOpen className="size-4 shrink-0 text-yellow-600" strokeWidth={2.5} />
),
title: 'More Details',
description: 'Get deeper insights about topics covered in the lesson',
},
{
icon: (
<Code className="size-4 shrink-0 text-yellow-600" strokeWidth={2.5} />
),
title: 'Code Help',
description: 'Share your code and ask me to help you debug it',
},
{
icon: <Bot className="size-4 shrink-0 text-yellow-600" strokeWidth={2.5} />,
title: 'Best Practices',
description: 'Share your code and ask me the best way to do something',
},
] as const;

Loading…
Cancel
Save