1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-25 00:21:28 +02:00
This commit is contained in:
Arik Chakma
2025-06-13 23:10:29 +06:00
parent c382177277
commit f1dd448222
4 changed files with 171 additions and 25 deletions

View File

@@ -1,12 +1,17 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { AITutorLayout } from '../AITutor/AITutorLayout';
import { AIGuideContent } from './AIGuideContent';
import { useQuery } from '@tanstack/react-query';
import { getAiGuideOptions } from '../../queries/ai-guide';
import {
aiGuideSuggestionsOptions,
getAiGuideOptions,
} from '../../queries/ai-guide';
import { queryClient } from '../../stores/query-client';
import { GenerateAIGuide } from './GenerateAIGuide';
import { AIGuideChat } from './AIGuideChat';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { isLoggedIn } from '../../lib/jwt';
import { shuffle } from '../../helper/shuffle';
type AIGuideProps = {
guideSlug?: string;
@@ -21,6 +26,24 @@ export function AIGuide(props: AIGuideProps) {
// only fetch the guide if the guideSlug is provided
// otherwise we are still generating the guide
const { data: aiGuide } = useQuery(getAiGuideOptions(guideSlug), queryClient);
const { data: aiGuideSuggestions, isLoading: isAiGuideSuggestionsLoading } =
useQuery(
{
...aiGuideSuggestionsOptions(guideSlug),
enabled: !!guideSlug && !!isLoggedIn(),
},
queryClient,
);
const randomQuestions = useMemo(() => {
return shuffle(aiGuideSuggestions?.questions || []).slice(0, 4);
}, [aiGuideSuggestions]);
const relatedTopics = useMemo(() => {
return shuffle(aiGuideSuggestions?.relatedTopics || []).slice(0, 2);
}, [aiGuideSuggestions]);
const deepDiveTopics = useMemo(() => {
return shuffle(aiGuideSuggestions?.deepDiveTopics || []).slice(0, 2);
}, [aiGuideSuggestions]);
return (
<AITutorLayout
@@ -34,12 +57,64 @@ export function AIGuide(props: AIGuideProps) {
<div className="grow overflow-y-auto p-4 pt-0">
{guideSlug && <AIGuideContent html={aiGuide?.html || ''} />}
{!guideSlug && <GenerateAIGuide onGuideSlugChange={setGuideSlug} />}
{!isAiGuideSuggestionsLoading && aiGuide && (
<div className="mt-4 grid grid-cols-2 divide-x divide-gray-200 rounded-lg border border-gray-200 bg-white">
<ListSuggestions
title="Related Topics"
suggestions={relatedTopics}
depth="essentials"
/>
<ListSuggestions
title="Dive Deeper"
suggestions={deepDiveTopics}
depth="detailed"
/>
</div>
)}
</div>
<AIGuideChat
guideSlug={guideSlug}
isGuideLoading={!aiGuide}
onUpgrade={() => setShowUpgradeModal(true)}
randomQuestions={randomQuestions}
/>
</AITutorLayout>
);
}
type ListSuggestionsProps = {
title: string;
suggestions: string[];
depth: string;
};
export function ListSuggestions(props: ListSuggestionsProps) {
const { title, suggestions, depth } = props;
return (
<div className="flex flex-col">
<h2 className="border-b border-gray-200 p-2 text-sm text-gray-500">
{title}
</h2>
<ul className="flex flex-col gap-1 p-1">
{suggestions?.map((topic) => {
const url = `/ai/guide?term=${encodeURIComponent(topic)}&depth=${depth}&id=&format=guide`;
return (
<li key={topic} className="w-full">
<a
href={url}
target="_blank"
className="block truncate rounded-md px-2 py-1 text-sm hover:bg-gray-100"
>
{topic}
</a>
</li>
);
})}
</ul>
</div>
);
}

View File

@@ -24,10 +24,11 @@ type AIGuideChatProps = {
guideSlug?: string;
isGuideLoading?: boolean;
onUpgrade?: () => void;
randomQuestions?: string[];
};
export function AIGuideChat(props: AIGuideChatProps) {
const { guideSlug, isGuideLoading, onUpgrade } = props;
const { guideSlug, isGuideLoading, onUpgrade, randomQuestions } = props;
const scrollareaRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
@@ -47,6 +48,13 @@ export function AIGuideChat(props: AIGuideChatProps) {
refetch: refetchBillingDetails,
} = useQuery(billingDetailsOptions(), queryClient);
// const {suggestions}
// const randomAiGuideSuggestions = useMemo(() => {
// return aiGuideSuggestions?.relatedTopics[
// Math.floor(Math.random() * aiGuideSuggestions.relatedTopics.length)
// ];
// }, [aiGuideSuggestions]);
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const isPaidUser = userBillingDetails?.status === 'active';
@@ -83,30 +91,34 @@ export function AIGuideChat(props: AIGuideChatProps) {
const isStreamingMessage = status === 'streaming';
const hasMessages = messages.length > 0;
const handleSubmitInput = useCallback(() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
const handleSubmitInput = useCallback(
(defaultInputValue?: string) => {
const message = defaultInputValue || inputValue;
if (!isLoggedIn()) {
showLoginPopup();
return;
}
if (isStreamingMessage) {
return;
}
if (isStreamingMessage) {
return;
}
const newMessages: ChatMessage[] = [
...messages,
{
role: 'user',
content: inputValue,
html: markdownToHtml(inputValue),
},
];
flushSync(() => {
setMessages(newMessages);
});
sendMessages(newMessages);
setInputValue('');
}, [inputValue, isStreamingMessage, messages, sendMessages, setMessages]);
const newMessages: ChatMessage[] = [
...messages,
{
role: 'user',
content: message,
html: markdownToHtml(message),
},
];
flushSync(() => {
setMessages(newMessages);
});
sendMessages(newMessages);
setInputValue('');
},
[inputValue, isStreamingMessage, messages, sendMessages, setMessages],
);
const checkScrollPosition = useCallback(() => {
const scrollArea = scrollareaRef.current;
@@ -158,6 +170,28 @@ export function AIGuideChat(props: AIGuideChatProps) {
html="Hello, how can I help you today?"
isIntro
/>
{randomQuestions &&
randomQuestions.length > 0 &&
messages.length === 0 && (
<>
<ul className="flex flex-col gap-1">
{randomQuestions?.map((question) => {
return (
<li key={`chat-${question}`}>
<button
className="w-fit rounded-lg border border-gray-200 bg-white p-2 text-left text-sm text-balance hover:bg-white/40"
onClick={() => {
handleSubmitInput(question);
}}
>
<p className="text-gray-500">{question}</p>
</button>
</li>
);
})}
</ul>
</>
)}
{messages.map((chat, index) => {
return (

19
src/helper/shuffle.ts Normal file
View File

@@ -0,0 +1,19 @@
export function shuffle<T = any>(array: T[]): T[] {
let currentIndex = array.length;
const result = [...array];
// While there remain elements to shuffle...
while (currentIndex != 0) {
// Pick a remaining element...
let randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[result[currentIndex], result[randomIndex]] = [
result[randomIndex],
result[currentIndex],
];
}
return result;
}

View File

@@ -67,3 +67,21 @@ export function listUserAiDocumentsOptions(
enabled: !!isLoggedIn(),
};
}
type AIGuideSuggestionsResponse = {
relatedTopics: string[];
deepDiveTopics: string[];
questions: string[];
};
export function aiGuideSuggestionsOptions(guideSlug?: string) {
return queryOptions({
queryKey: ['ai-guide-suggestions', guideSlug],
queryFn: () => {
return httpGet<AIGuideSuggestionsResponse>(
`/v1-ai-guide-suggestions/${guideSlug}`,
);
},
enabled: !!guideSlug && !!isLoggedIn(),
});
}