mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-25 00:21:28 +02:00
Refactor actions logic
This commit is contained in:
@@ -2,18 +2,18 @@ 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 PredefinedMessageType,
|
||||
PredefinedActionButton,
|
||||
} from './PredefinedActions';
|
||||
|
||||
type PredefinedMessageGroupProps = {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
messages: Omit<PredefinedMessage, 'messages'>[];
|
||||
onSelect: (message: Omit<PredefinedMessage, 'messages'>) => void;
|
||||
messages: PredefinedMessageType[];
|
||||
onSelect: (message: PredefinedMessageType) => void;
|
||||
};
|
||||
|
||||
export function PredefinedMessageGroup(props: PredefinedMessageGroupProps) {
|
||||
export function PredefinedActionGroup(props: PredefinedMessageGroupProps) {
|
||||
const { label, icon: Icon, messages, onSelect } = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -25,7 +25,7 @@ export function PredefinedMessageGroup(props: PredefinedMessageGroupProps) {
|
||||
|
||||
return (
|
||||
<div className="relative" ref={containerRef}>
|
||||
<PredefinedMessageButton
|
||||
<PredefinedActionButton
|
||||
label={label}
|
||||
icon={Icon}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
@@ -36,7 +36,7 @@ export function PredefinedMessageGroup(props: PredefinedMessageGroupProps) {
|
||||
<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
|
||||
<PredefinedActionButton
|
||||
key={m.message}
|
||||
{...m}
|
||||
className="h-7 w-full rounded-none bg-transparent hover:bg-gray-200"
|
115
src/components/TopicDetail/PredefinedActions.tsx
Normal file
115
src/components/TopicDetail/PredefinedActions.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
BabyIcon,
|
||||
BookOpenTextIcon,
|
||||
BrainIcon,
|
||||
ChevronDownIcon,
|
||||
NotebookPenIcon,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { PredefinedActionGroup } from './PredefinedActionGroup';
|
||||
|
||||
export type PredefinedActionType = {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
prompt?: string;
|
||||
children?: PredefinedActionType[];
|
||||
};
|
||||
|
||||
export const actions: PredefinedActionType[] = [
|
||||
{
|
||||
icon: BookOpenTextIcon,
|
||||
label: 'Explain',
|
||||
children: [
|
||||
{
|
||||
icon: NotebookPenIcon,
|
||||
label: 'Explain Topic',
|
||||
prompt: 'Explain this topic in detail and include examples',
|
||||
},
|
||||
{
|
||||
icon: BabyIcon,
|
||||
label: 'Explain like I am five',
|
||||
prompt: 'Explain this topic like I am a 5 years old',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: BrainIcon,
|
||||
label: 'Test my Knowledge',
|
||||
prompt:
|
||||
"Act as an interviewer and test my understanding of this topic. Ask me a single question at a time and evaluate my answer. Question number should be bold. After evaluating my answer, immediately proceed to the next question without asking if I'm ready or want another question. Continue asking questions until I explicitly tell you to stop.",
|
||||
},
|
||||
];
|
||||
|
||||
export const promptLabelMapping = actions.reduce(
|
||||
(acc, action) => {
|
||||
if (action.prompt) {
|
||||
acc[action.prompt] = action.label;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
type PredefinedActionsProps = {
|
||||
onSelect: (action: PredefinedActionType) => void;
|
||||
};
|
||||
|
||||
export function PredefinedActions(props: PredefinedActionsProps) {
|
||||
const { onSelect } = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 border-gray-200 px-3 py-1 text-sm">
|
||||
{actions.map((action) => {
|
||||
if (!action.children) {
|
||||
return (
|
||||
<PredefinedActionButton
|
||||
key={action.label}
|
||||
icon={action.icon}
|
||||
label={action.label}
|
||||
onClick={() => {
|
||||
onSelect(action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PredefinedActionGroup
|
||||
key={action.label}
|
||||
label={action.label}
|
||||
icon={action.icon}
|
||||
messages={action.children}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type PredefinedActionButtonProps = {
|
||||
label: string;
|
||||
icon?: LucideIcon;
|
||||
onClick: () => void;
|
||||
isGroup?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function PredefinedActionButton(props: PredefinedActionButtonProps) {
|
||||
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>
|
||||
);
|
||||
}
|
@@ -1,120 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
@@ -1,14 +1,6 @@
|
||||
import '../GenerateCourse/AICourseLessonChat.css';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
useState,
|
||||
type FormEvent,
|
||||
useRef,
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { useState, useRef, Fragment, useCallback, useEffect } from 'react';
|
||||
import { billingDetailsOptions } from '../../queries/billing';
|
||||
import { getAiCourseLimitOptions } from '../../queries/ai-course';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
@@ -37,11 +29,7 @@ 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';
|
||||
import { PredefinedActions, promptLabelMapping } from './PredefinedActions';
|
||||
|
||||
type TopicDetailAIProps = {
|
||||
resourceId: string;
|
||||
@@ -338,15 +326,15 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PredefinedMessages
|
||||
onSelect={(m) => {
|
||||
if (!m?.message) {
|
||||
<PredefinedActions
|
||||
onSelect={(action) => {
|
||||
if (!action?.prompt) {
|
||||
toast.error('Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage(m.message);
|
||||
handleChatSubmit(m.message);
|
||||
setMessage(action.prompt);
|
||||
handleChatSubmit(action.prompt);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -358,17 +346,10 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
||||
<div className="relative flex grow flex-col justify-end">
|
||||
<div className="flex flex-col justify-end gap-2 px-3 py-2">
|
||||
{aiChatHistory.map((chat, index) => {
|
||||
const isTextMyKnowledgePrompt =
|
||||
chat.role === 'user' &&
|
||||
chat.content === testMyKnowledgePrompt;
|
||||
const isTextExplainTopicPrompt =
|
||||
chat.role === 'user' && chat.content === explainTopicPrompt;
|
||||
|
||||
let content = chat.content;
|
||||
if (isTextMyKnowledgePrompt) {
|
||||
content = 'Starting Interview';
|
||||
} else if (isTextExplainTopicPrompt) {
|
||||
content = 'Explain Topic';
|
||||
|
||||
if (chat.role === 'user' && promptLabelMapping[chat.content]) {
|
||||
content = promptLabelMapping[chat.content];
|
||||
}
|
||||
|
||||
return (
|
||||
|
Reference in New Issue
Block a user