1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-25 08:35:42 +02:00
This commit is contained in:
Arik Chakma
2025-06-20 18:32:22 +06:00
parent 2364eb9725
commit ea1df049a5
2 changed files with 103 additions and 67 deletions

View File

@@ -4,7 +4,7 @@ import {
SparklesIcon, SparklesIcon,
type LucideIcon, type LucideIcon,
} from 'lucide-react'; } from 'lucide-react';
import { useEffect, useId, useState, type FormEvent } from 'react'; import { useEffect, useId, useState } from 'react';
import { FormatItem } from './FormatItem'; import { FormatItem } from './FormatItem';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup'; import { showLoginPopup } from '../../lib/popup';
@@ -57,8 +57,7 @@ export function ContentGenerator() {
}, },
]; ];
const handleSubmit = (e: FormEvent<HTMLFormElement>) => { const handleSubmit = () => {
e.preventDefault();
if (!isLoggedIn()) { if (!isLoggedIn()) {
showLoginPopup(); showLoginPopup();
return; return;
@@ -113,7 +112,13 @@ export function ContentGenerator() {
</p> </p>
</div> </div>
<form className="mt-10 space-y-4" onSubmit={handleSubmit}> <form
className="mt-10 space-y-4"
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor={titleFieldId} className="inline-block text-gray-500"> <label htmlFor={titleFieldId} className="inline-block text-gray-500">
What can I help you learn? What can I help you learn?
@@ -184,6 +189,9 @@ export function ContentGenerator() {
format={selectedFormat} format={selectedFormat}
questionAnswerChatMessages={questionAnswerChatMessages} questionAnswerChatMessages={questionAnswerChatMessages}
setQuestionAnswerChatMessages={setQuestionAnswerChatMessages} setQuestionAnswerChatMessages={setQuestionAnswerChatMessages}
onGenerateNow={() => {
handleSubmit();
}}
/> />
)} )}

View File

@@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client'; import { queryClient } from '../../stores/query-client';
import { aiQuestionSuggestionsOptions } from '../../queries/user-ai-session'; import { aiQuestionSuggestionsOptions } from '../../queries/user-ai-session';
import type { AllowedFormat } from './ContentGenerator'; import type { AllowedFormat } from './ContentGenerator';
import { Loader2Icon, SendIcon } from 'lucide-react'; import { Loader2Icon, RotateCwIcon, SendIcon } from 'lucide-react';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';
@@ -23,6 +23,7 @@ type QuestionAnswerChatProps = {
setQuestionAnswerChatMessages: ( setQuestionAnswerChatMessages: (
messages: QuestionAnswerChatMessage[], messages: QuestionAnswerChatMessage[],
) => void; ) => void;
onGenerateNow: () => void;
}; };
export function QuestionAnswerChat(props: QuestionAnswerChatProps) { export function QuestionAnswerChat(props: QuestionAnswerChatProps) {
@@ -31,6 +32,7 @@ export function QuestionAnswerChat(props: QuestionAnswerChatProps) {
format, format,
questionAnswerChatMessages, questionAnswerChatMessages,
setQuestionAnswerChatMessages, setQuestionAnswerChatMessages,
onGenerateNow,
} = props; } = props;
const [activeMessageIndex, setActiveMessageIndex] = useState(0); const [activeMessageIndex, setActiveMessageIndex] = useState(0);
@@ -94,11 +96,18 @@ export function QuestionAnswerChat(props: QuestionAnswerChatProps) {
const canGenerateNow = const canGenerateNow =
// user can generate after answering 5 questions -> 5 * 2 messages (user and assistant) // user can generate after answering 5 questions -> 5 * 2 messages (user and assistant)
!isLoadingAiQuestionSuggestions && questionAnswerChatMessages.length > 10; !isLoadingAiQuestionSuggestions && questionAnswerChatMessages.length >= 10;
const canReset = questionAnswerChatMessages.length >= 2;
const handleReset = () => {
setQuestionAnswerChatMessages([]);
setActiveMessageIndex(0);
setStatus('answering');
};
return ( return (
<> <>
<div className="relative h-[300px] w-full overflow-hidden rounded-xl border border-gray-200 bg-white"> <div className="relative h-[420px] w-full overflow-hidden rounded-xl border border-gray-200 bg-white">
{isLoadingAiQuestionSuggestions && ( {isLoadingAiQuestionSuggestions && (
<div className="absolute inset-0 flex items-center justify-center bg-white"> <div className="absolute inset-0 flex items-center justify-center bg-white">
<div className="flex animate-pulse items-center gap-2 rounded-full border border-gray-200 bg-gray-50 p-2 px-4 text-sm"> <div className="flex animate-pulse items-center gap-2 rounded-full border border-gray-200 bg-gray-50 p-2 px-4 text-sm">
@@ -121,35 +130,53 @@ export function QuestionAnswerChat(props: QuestionAnswerChatProps) {
)} )}
{!isLoadingAiQuestionSuggestions && status === 'answering' && ( {!isLoadingAiQuestionSuggestions && status === 'answering' && (
<div className="flex h-full w-full flex-col bg-white"> <>
<div {canReset && (
ref={scrollAreaRef} <div className="absolute top-2 left-2 z-10">
className="relative h-full w-full grow overflow-y-auto" <button
> className="flex cursor-pointer items-center gap-1.5 rounded-lg bg-gray-50 px-2 py-1 text-xs text-gray-500 hover:bg-gray-200 focus:outline-none"
<div className="absolute inset-0 flex flex-col"> onClick={handleReset}
<div className="flex w-full grow flex-col justify-end gap-2 p-2"> >
{questionAnswerChatMessages.map((message, index) => ( <RotateCwIcon className="size-3" />
Reset and Restart Asking
</button>
</div>
)}
<div className="flex h-full w-full flex-col bg-white">
<div
ref={scrollAreaRef}
className="relative h-full w-full grow overflow-y-auto"
>
<div className="absolute inset-0 flex flex-col">
<div className="flex w-full grow flex-col justify-end gap-2 p-2">
{questionAnswerChatMessages.map((message, index) => (
<QuestionAnswerChatMessage
key={index}
role={message.role}
question={
message.role === 'assistant'
? message.question
: undefined
}
answer={
message.role === 'user' ? message.answer : undefined
}
/>
))}
<QuestionAnswerChatMessage <QuestionAnswerChatMessage
key={index} role="assistant"
role={message.role} question={activeMessage?.question ?? ''}
question={
message.role === 'assistant'
? message.question
: undefined
}
answer={
message.role === 'user' ? message.answer : undefined
}
/> />
))} </div>
</div>
<QuestionAnswerChatMessage </div>
role="assistant"
question={activeMessage?.question ?? ''}
/>
<div className="p-2">
<div className="rounded-lg border border-gray-200 bg-white">
{activeMessage && ( {activeMessage && (
<div> <div className="border-b border-gray-200 p-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Pick an answer from these or write it below Pick an answer from these or write it below
</p> </p>
@@ -158,7 +185,7 @@ export function QuestionAnswerChat(props: QuestionAnswerChatProps) {
<button <button
type="button" type="button"
key={answer} key={answer}
className="cursor-pointer rounded-lg bg-gray-100 p-1 px-2 hover:bg-gray-200" className="cursor-pointer rounded-lg bg-gray-100 p-1 px-2 hover:bg-gray-200 focus:outline-none"
onClick={() => { onClick={() => {
handleAnswerSelect(answer); handleAnswerSelect(answer);
}} }}
@@ -169,42 +196,40 @@ export function QuestionAnswerChat(props: QuestionAnswerChatProps) {
</div> </div>
</div> </div>
)} )}
<div
className="flex w-full items-center justify-between gap-2 p-2"
onSubmit={(e) => {
e.preventDefault();
handleAnswerSelect(message);
}}
>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full bg-transparent text-sm focus:outline-none"
placeholder="Write your answer here..."
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleAnswerSelect(message);
setMessage('');
}
}}
/>
<button
type="button"
className="flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-gray-100 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
>
<SendIcon className="size-4" />
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</>
<div className="p-2">
<div
className="flex w-full items-center justify-between gap-2 rounded-lg border border-gray-200 bg-white p-2"
onSubmit={(e) => {
e.preventDefault();
handleAnswerSelect(message);
}}
>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full bg-transparent text-sm focus:outline-none"
placeholder="Write your answer here..."
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleAnswerSelect(message);
setMessage('');
}
}}
/>
<button
type="button"
className="flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
>
<SendIcon className="size-4" />
</button>
</div>
</div>
</div>
)} )}
</div> </div>
@@ -212,7 +237,10 @@ export function QuestionAnswerChat(props: QuestionAnswerChatProps) {
<div className="flex w-full items-center rounded-lg border border-gray-200 bg-white p-2"> <div className="flex w-full items-center rounded-lg border border-gray-200 bg-white p-2">
<p className="text-sm"> <p className="text-sm">
Keep answering for better output or{' '} Keep answering for better output or{' '}
<button className="text-blue-500 underline underline-offset-2 hover:no-underline focus:outline-none"> <button
className="text-blue-500 underline underline-offset-2 hover:no-underline focus:outline-none"
onClick={onGenerateNow}
>
Generate now. Generate now.
</button> </button>
</p> </p>