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 { AITutorLayout } from '../AITutor/AITutorLayout';
|
||||||
import { AIGuideContent } from './AIGuideContent';
|
import { AIGuideContent } from './AIGuideContent';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
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 { queryClient } from '../../stores/query-client';
|
||||||
import { GenerateAIGuide } from './GenerateAIGuide';
|
import { GenerateAIGuide } from './GenerateAIGuide';
|
||||||
import { AIGuideChat } from './AIGuideChat';
|
import { AIGuideChat } from './AIGuideChat';
|
||||||
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
|
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
|
||||||
|
import { isLoggedIn } from '../../lib/jwt';
|
||||||
|
import { shuffle } from '../../helper/shuffle';
|
||||||
|
|
||||||
type AIGuideProps = {
|
type AIGuideProps = {
|
||||||
guideSlug?: string;
|
guideSlug?: string;
|
||||||
@@ -21,6 +26,24 @@ export function AIGuide(props: AIGuideProps) {
|
|||||||
// only fetch the guide if the guideSlug is provided
|
// only fetch the guide if the guideSlug is provided
|
||||||
// otherwise we are still generating the guide
|
// otherwise we are still generating the guide
|
||||||
const { data: aiGuide } = useQuery(getAiGuideOptions(guideSlug), queryClient);
|
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 (
|
return (
|
||||||
<AITutorLayout
|
<AITutorLayout
|
||||||
@@ -34,12 +57,64 @@ export function AIGuide(props: AIGuideProps) {
|
|||||||
<div className="grow overflow-y-auto p-4 pt-0">
|
<div className="grow overflow-y-auto p-4 pt-0">
|
||||||
{guideSlug && <AIGuideContent html={aiGuide?.html || ''} />}
|
{guideSlug && <AIGuideContent html={aiGuide?.html || ''} />}
|
||||||
{!guideSlug && <GenerateAIGuide onGuideSlugChange={setGuideSlug} />}
|
{!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>
|
</div>
|
||||||
<AIGuideChat
|
<AIGuideChat
|
||||||
guideSlug={guideSlug}
|
guideSlug={guideSlug}
|
||||||
isGuideLoading={!aiGuide}
|
isGuideLoading={!aiGuide}
|
||||||
onUpgrade={() => setShowUpgradeModal(true)}
|
onUpgrade={() => setShowUpgradeModal(true)}
|
||||||
|
randomQuestions={randomQuestions}
|
||||||
/>
|
/>
|
||||||
</AITutorLayout>
|
</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;
|
guideSlug?: string;
|
||||||
isGuideLoading?: boolean;
|
isGuideLoading?: boolean;
|
||||||
onUpgrade?: () => void;
|
onUpgrade?: () => void;
|
||||||
|
randomQuestions?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AIGuideChat(props: AIGuideChatProps) {
|
export function AIGuideChat(props: AIGuideChatProps) {
|
||||||
const { guideSlug, isGuideLoading, onUpgrade } = props;
|
const { guideSlug, isGuideLoading, onUpgrade, randomQuestions } = props;
|
||||||
|
|
||||||
const scrollareaRef = useRef<HTMLDivElement>(null);
|
const scrollareaRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -47,6 +48,13 @@ export function AIGuideChat(props: AIGuideChatProps) {
|
|||||||
refetch: refetchBillingDetails,
|
refetch: refetchBillingDetails,
|
||||||
} = useQuery(billingDetailsOptions(), queryClient);
|
} = 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 isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
|
||||||
const isPaidUser = userBillingDetails?.status === 'active';
|
const isPaidUser = userBillingDetails?.status === 'active';
|
||||||
|
|
||||||
@@ -83,30 +91,34 @@ export function AIGuideChat(props: AIGuideChatProps) {
|
|||||||
const isStreamingMessage = status === 'streaming';
|
const isStreamingMessage = status === 'streaming';
|
||||||
const hasMessages = messages.length > 0;
|
const hasMessages = messages.length > 0;
|
||||||
|
|
||||||
const handleSubmitInput = useCallback(() => {
|
const handleSubmitInput = useCallback(
|
||||||
if (!isLoggedIn()) {
|
(defaultInputValue?: string) => {
|
||||||
showLoginPopup();
|
const message = defaultInputValue || inputValue;
|
||||||
return;
|
if (!isLoggedIn()) {
|
||||||
}
|
showLoginPopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isStreamingMessage) {
|
if (isStreamingMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newMessages: ChatMessage[] = [
|
const newMessages: ChatMessage[] = [
|
||||||
...messages,
|
...messages,
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: inputValue,
|
content: message,
|
||||||
html: markdownToHtml(inputValue),
|
html: markdownToHtml(message),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
setMessages(newMessages);
|
setMessages(newMessages);
|
||||||
});
|
});
|
||||||
sendMessages(newMessages);
|
sendMessages(newMessages);
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
}, [inputValue, isStreamingMessage, messages, sendMessages, setMessages]);
|
},
|
||||||
|
[inputValue, isStreamingMessage, messages, sendMessages, setMessages],
|
||||||
|
);
|
||||||
|
|
||||||
const checkScrollPosition = useCallback(() => {
|
const checkScrollPosition = useCallback(() => {
|
||||||
const scrollArea = scrollareaRef.current;
|
const scrollArea = scrollareaRef.current;
|
||||||
@@ -158,6 +170,28 @@ export function AIGuideChat(props: AIGuideChatProps) {
|
|||||||
html="Hello, how can I help you today?"
|
html="Hello, how can I help you today?"
|
||||||
isIntro
|
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) => {
|
{messages.map((chat, index) => {
|
||||||
return (
|
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(),
|
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