mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-07 15:50:40 +02:00
Merge branch 'feat/topic-chat' of github.com:kamranahmedse/developer-roadmap into feat/topic-chat
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
Fragment,
|
Fragment,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { billingDetailsOptions } from '../../queries/billing';
|
import { billingDetailsOptions } from '../../queries/billing';
|
||||||
import { getAiCourseLimitOptions } from '../../queries/ai-course';
|
import { getAiCourseLimitOptions } from '../../queries/ai-course';
|
||||||
@@ -18,7 +19,7 @@ import {
|
|||||||
Loader2Icon,
|
Loader2Icon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
SendIcon,
|
SendIcon,
|
||||||
Trash2
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { showLoginPopup } from '../../lib/popup';
|
import { showLoginPopup } from '../../lib/popup';
|
||||||
import { cn } from '../../lib/classname';
|
import { cn } from '../../lib/classname';
|
||||||
@@ -60,6 +61,7 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const scrollareaRef = useRef<HTMLDivElement>(null);
|
const scrollareaRef = useRef<HTMLDivElement>(null);
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
const sanitizedTopicId = topicId?.includes('@')
|
const sanitizedTopicId = topicId?.includes('@')
|
||||||
? topicId?.split('@')?.[1]
|
? topicId?.split('@')?.[1]
|
||||||
@@ -95,10 +97,9 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
|
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
|
||||||
const isPaidUser = userBillingDetails?.status === 'active';
|
const isPaidUser = userBillingDetails?.status === 'active';
|
||||||
|
|
||||||
const handleChatSubmit = (e: FormEvent<HTMLFormElement>) => {
|
const handleChatSubmit = (overrideMessage?: string) => {
|
||||||
e.preventDefault();
|
const trimmedMessage = (overrideMessage ?? message).trim();
|
||||||
|
|
||||||
const trimmedMessage = message.trim();
|
|
||||||
if (
|
if (
|
||||||
!trimmedMessage ||
|
!trimmedMessage ||
|
||||||
isStreamingMessage ||
|
isStreamingMessage ||
|
||||||
@@ -228,6 +229,27 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
roadmapTreeMapping?.subjects && roadmapTreeMapping?.subjects?.length > 0;
|
roadmapTreeMapping?.subjects && roadmapTreeMapping?.subjects?.length > 0;
|
||||||
const hasChatHistory = aiChatHistory.length > 1;
|
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 (
|
return (
|
||||||
<div className="relative mt-4 flex grow flex-col overflow-hidden rounded-lg border border-gray-200">
|
<div className="relative mt-4 flex grow flex-col overflow-hidden rounded-lg border border-gray-200">
|
||||||
{isDataLoading && (
|
{isDataLoading && (
|
||||||
@@ -332,6 +354,24 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="scrollbar-thumb-gray-300 scrollbar-track-transparent scrollbar-thin relative grow overflow-y-auto"
|
className="scrollbar-thumb-gray-300 scrollbar-track-transparent scrollbar-thin relative grow overflow-y-auto"
|
||||||
ref={scrollareaRef}
|
ref={scrollareaRef}
|
||||||
@@ -340,11 +380,24 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
<div className="relative flex grow flex-col justify-end">
|
<div className="relative flex grow flex-col justify-end">
|
||||||
<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">
|
||||||
{aiChatHistory.map((chat, index) => {
|
{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';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={`chat-${index}`}>
|
<Fragment key={`chat-${index}`}>
|
||||||
<AIChatCard
|
<AIChatCard
|
||||||
role={chat.role}
|
role={chat.role}
|
||||||
content={chat.content}
|
content={content}
|
||||||
html={chat.html}
|
html={chat.html}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -364,8 +417,12 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
|
ref={formRef}
|
||||||
className="relative flex items-start border-t border-gray-200 text-sm"
|
className="relative flex items-start border-t border-gray-200 text-sm"
|
||||||
onSubmit={handleChatSubmit}
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleChatSubmit();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isLimitExceeded && isLoggedIn() && (
|
{isLimitExceeded && isLoggedIn() && (
|
||||||
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
|
<div className="absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black text-white">
|
||||||
@@ -416,7 +473,8 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
handleChatSubmit(e as unknown as FormEvent<HTMLFormElement>);
|
e.preventDefault();
|
||||||
|
handleChatSubmit();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
@@ -432,3 +490,22 @@ export function TopicDetailAI(props: TopicDetailAIProps) {
|
|||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user