diff --git a/package.json b/package.json index ae7bed2a5..70cce2884 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "react-calendar-heatmap": "^1.9.0", "react-confetti": "^6.1.0", "react-dom": "^18.3.1", + "react-textarea-autosize": "^8.5.7", "react-tooltip": "^5.28.0", "reactflow": "^11.11.4", "rehype-external-links": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 378361fba..6d1855787 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ importers: react-dom: specifier: ^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: specifier: ^5.28.0 version: 5.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -356,6 +359,10 @@ packages: peerDependencies: '@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': resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} @@ -2901,6 +2908,12 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} 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: resolution: {integrity: sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==} peerDependencies: @@ -2924,6 +2937,9 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regex-recursion@5.1.1: resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} @@ -3304,6 +3320,33 @@ packages: peerDependencies: 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: resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} peerDependencies: @@ -3662,6 +3705,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.26.9': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/template@7.25.9': dependencies: '@babel/code-frame': 7.26.2 @@ -6299,6 +6346,15 @@ snapshots: 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): dependencies: '@floating-ui/dom': 1.6.13 @@ -6332,6 +6388,8 @@ snapshots: dependencies: picomatch: 2.3.1 + regenerator-runtime@0.14.1: {} + regex-recursion@5.1.1: dependencies: regex: 5.1.1 @@ -6872,6 +6930,25 @@ snapshots: escalade: 3.2.0 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): dependencies: react: 18.3.1 diff --git a/src/components/GenerateCourse/AICourseFollowUpPopover.tsx b/src/components/GenerateCourse/AICourseFollowUpPopover.tsx index a39097574..41028ec68 100644 --- a/src/components/GenerateCourse/AICourseFollowUpPopover.tsx +++ b/src/components/GenerateCourse/AICourseFollowUpPopover.tsx @@ -19,6 +19,7 @@ import { markdownToHtml } from '../../lib/markdown'; import { cn } from '../../lib/classname'; import { getAiCourseLimitOptions } from '../../queries/ai-course'; import { queryClient } from '../../stores/query-client'; +import TextareaAutosize from 'react-textarea-autosize'; export type AllowedAIChatRole = 'user' | 'assistant'; 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"> {courseAIChatHistory.map((chat, index) => { return ( - <AIChatCard - key={index} - role={chat.role} - content={chat.content} - /> + <> + <AIChatCard + key={index} + role={chat.role} + 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> <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} > {isLimitExceeded && ( @@ -228,17 +241,22 @@ export function AICourseFollowUpPopover(props: AICourseFollowUpPopoverProps) { <p>You have reached the AI usage limit for today.</p> </div> )} - <input - className="h-full grow bg-transparent px-4 py-2 focus:outline-none" + <TextareaAutosize + 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..." value={message} onChange={(e) => setMessage(e.target.value)} autoFocus={true} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + handleChatSubmit(e as unknown as FormEvent<HTMLFormElement>); + } + }} /> <button type="submit" 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]" /> </button> @@ -285,3 +303,66 @@ function AIChatCard(props: AIChatCardProps) { </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;