feat: predefined message group

feat/topic-chat
Arik Chakma 7 days ago
parent d0f73264af
commit 6f7eb7dd5d
  1. 54
      src/components/TopicDetail/PredefinedMessageGroup.tsx
  2. 120
      src/components/TopicDetail/PredefinedMessages.tsx
  3. 75
      src/components/TopicDetail/TopicDetailAI.tsx

@ -0,0 +1,54 @@
import type { LucideIcon } from 'lucide-react';
import { useState, useRef } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import {
type PredefinedMessage,
PredefinedMessageButton,
} from './PredefinedMessages';
type PredefinedMessageGroupProps = {
label: string;
icon: LucideIcon;
messages: Omit<PredefinedMessage, 'messages'>[];
onSelect: (message: Omit<PredefinedMessage, 'messages'>) => void;
};
export function PredefinedMessageGroup(props: PredefinedMessageGroupProps) {
const { label, icon: Icon, messages, onSelect } = props;
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useOutsideClick(containerRef, () => {
setIsOpen(false);
});
return (
<div className="relative" ref={containerRef}>
<PredefinedMessageButton
label={label}
icon={Icon}
onClick={() => setIsOpen(!isOpen)}
isGroup={true}
/>
{isOpen && (
<div className="absolute top-full left-0 z-20 mt-1 divide-y overflow-hidden rounded-md border border-gray-200 bg-white p-0">
{messages.map((m) => {
return (
<PredefinedMessageButton
key={m.message}
{...m}
className="h-7 w-full rounded-none bg-transparent hover:bg-gray-200"
onClick={() => {
onSelect(m);
setIsOpen(false);
}}
/>
);
})}
</div>
)}
</div>
);
}

@ -0,0 +1,120 @@
import {
BabyIcon,
BookOpenTextIcon,
BrainIcon,
ChevronDownIcon,
NotebookPenIcon,
type LucideIcon,
} from 'lucide-react';
import { useMemo } from 'react';
import { cn } from '../../lib/classname';
import { PredefinedMessageGroup } from './PredefinedMessageGroup';
export const testMyKnowledgePrompt =
'Act as an interviewer and test my understanding of this topic';
export const explainTopicPrompt = 'Explain this topic in detail';
export type PredefinedMessage = {
icon: LucideIcon;
label: string;
} & (
| {
message?: never;
messages: PredefinedMessage[];
}
| {
message: string;
messages?: never;
}
);
type PredefinedMessagesProps = {
onSelect: (message: Omit<PredefinedMessage, 'messages'>) => void;
};
export function PredefinedMessages(props: PredefinedMessagesProps) {
const { onSelect } = props;
const predefinedMessages: PredefinedMessage[] = useMemo(
() => [
{
icon: BookOpenTextIcon,
label: 'Explain',
messages: [
{
icon: NotebookPenIcon,
label: 'Explain Topic',
message: 'Explain this topic in detail and include examples',
},
{
icon: BabyIcon,
label: 'Explain like I am five',
message: 'Explain this topic like I am a 5 years old',
},
],
},
{
icon: BrainIcon,
label: 'Test my Knowledge',
message: testMyKnowledgePrompt,
},
],
[],
);
return (
<div className="flex items-center gap-2 border-gray-200 px-3 py-1 text-sm">
{predefinedMessages.map((m) => {
const isGroup = 'messages' in m && Array.isArray(m?.messages);
if (!isGroup) {
return (
<PredefinedMessageButton
key={m.message}
icon={m.icon}
label={m.label}
onClick={() => {
onSelect(m);
}}
/>
);
}
return (
<PredefinedMessageGroup
key={m.label}
label={m.label}
icon={m.icon}
messages={m.messages}
onSelect={onSelect}
/>
);
})}
</div>
);
}
type PredefinedMessageButtonProps = {
label: string;
icon?: LucideIcon;
onClick: () => void;
isGroup?: boolean;
className?: string;
};
export function PredefinedMessageButton(props: PredefinedMessageButtonProps) {
const { label, icon: Icon, onClick, isGroup = false, className } = props;
return (
<button
className={cn(
'flex shrink-0 items-center gap-1.5 rounded-md bg-gray-200 px-2 py-1 text-sm whitespace-nowrap hover:bg-gray-300',
className,
)}
onClick={onClick}
>
{Icon && <Icon className="size-3.5" />}
{label}
{isGroup && <ChevronDownIcon className="size-3.5" />}
</button>
);
}

@ -37,6 +37,11 @@ import { getPercentage } from '../../lib/number';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import { defaultChatHistory } from './TopicDetail';
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
import {
explainTopicPrompt,
PredefinedMessages,
testMyKnowledgePrompt,
} from './PredefinedMessages';
type TopicDetailAIProps = {
resourceId: string;
@ -229,27 +234,6 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
roadmapTreeMapping?.subjects && roadmapTreeMapping?.subjects?.length > 0;
const hasChatHistory = aiChatHistory.length > 1;
const testMyKnowledgePrompt =
'Act as an interviewer and test my understanding of this topic';
const explainTopicPrompt = 'Explain this topic in detail';
const predefinedMessages = useMemo(
() => [
{
label: 'Explain like I am five',
message: 'Explain this topic like I am a 5 years old',
},
{
label: 'Test my Knowledge',
message: testMyKnowledgePrompt,
},
{
label: 'Explain Topic',
message: explainTopicPrompt,
},
],
[],
);
return (
<div className="relative mt-4 flex grow flex-col overflow-hidden rounded-lg border border-gray-200">
{isDataLoading && (
@ -333,7 +317,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
{!isPaidUser && (
<>
<button
className="hidden sm:block rounded-md bg-gray-200 px-2 py-1 text-sm hover:bg-gray-300"
className="hidden rounded-md bg-gray-200 px-2 py-1 text-sm hover:bg-gray-300 sm:block"
onClick={() => {
setShowAILimitsPopup(true);
}}
@ -354,23 +338,17 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
)}
</div>
<div
className={cn(
'scrollbar-thumb-gray-300 scrollbar-track-transparent scrollbar-thin flex items-center gap-2 overflow-x-auto border-gray-200 px-3 py-1 text-sm',
)}
>
{predefinedMessages.map((m) => (
<PredefinedMessageButton
key={m.message}
label={m.label}
message={m.message}
onClick={() => {
setMessage(m.message);
handleChatSubmit(m.message);
}}
/>
))}
</div>
<PredefinedMessages
onSelect={(m) => {
if (!m?.message) {
toast.error('Something went wrong');
return;
}
setMessage(m.message);
handleChatSubmit(m.message);
}}
/>
<div
className="scrollbar-thumb-gray-300 scrollbar-track-transparent scrollbar-thin relative grow overflow-y-auto"
@ -490,22 +468,3 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
</div>
);
}
type PredefinedMessageButtonProps = {
label: string;
message: string;
onClick: () => void;
};
function PredefinedMessageButton(props: PredefinedMessageButtonProps) {
const { label, message, onClick } = props;
return (
<button
className="shrink-0 rounded-md bg-gray-200 px-2 py-1 text-sm whitespace-nowrap hover:bg-gray-300"
onClick={onClick}
>
{label}
</button>
);
}

Loading…
Cancel
Save