mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-25 00:21:28 +02:00
wip
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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
19
src/helper/shuffle.ts
Normal 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;
|
||||
}
|
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user