1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-03 06:12:53 +02:00

Final implementation

This commit is contained in:
Kamran Ahmed
2025-06-10 16:15:47 +01:00
parent 912bf4333d
commit 97b7f54c6f

View File

@@ -28,6 +28,8 @@ import { queryClient } from '../../stores/query-client';
import { RoadmapAIChatCard } from '../RoadmapAIChat/RoadmapAIChatCard';
import { UpdatePersonaModal } from '../UserPersona/UpdatePersonaModal';
import { CLOSE_TOPIC_DETAIL_EVENT } from '../TopicDetail/TopicDetail';
import { billingDetailsOptions } from '../../queries/billing';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
type ChatHeaderButtonProps = {
onClick?: () => void;
@@ -77,6 +79,72 @@ function ChatHeaderButton(props: ChatHeaderButtonProps) {
);
}
type UpgradeMessageProps = {
onUpgradeClick?: () => void;
};
function UpgradeMessage(props: UpgradeMessageProps) {
const { onUpgradeClick } = props;
return (
<div className="border-t border-gray-200 bg-black px-3 py-3">
<div className="flex items-center gap-2.5">
<Wand2 className="h-4 w-4 flex-shrink-0 text-white" />
<div className="flex-1 text-sm">
<p className="mb-1 font-medium text-white">
You've reached your AI usage limit
</p>
<p className="text-xs text-gray-300">
Upgrade to Pro for relaxed limits and advanced features
</p>
</div>
<button
className="flex-shrink-0 rounded-md bg-white px-3 py-1.5 text-xs font-medium text-black transition-colors hover:bg-gray-100"
onClick={onUpgradeClick}
>
Upgrade to Pro
</button>
</div>
</div>
);
}
type UsageButtonProps = {
percentageUsed: number;
onUpgradeClick?: () => void;
};
function UsageButton(props: UsageButtonProps) {
const { percentageUsed, onUpgradeClick } = props;
return (
<button
onClick={onUpgradeClick}
className="ml-2 flex items-center gap-2 rounded-md px-3 py-1.5 text-xs font-medium transition-all hover:bg-yellow-200"
>
<div className="flex items-center gap-1.5">
<div className="h-1.5 w-6 overflow-hidden rounded-full bg-gray-200">
<div
className={cn(
'h-full transition-all duration-300',
percentageUsed >= 90
? 'bg-red-500'
: percentageUsed >= 70
? 'bg-yellow-500'
: 'bg-green-500',
)}
style={{ width: `${Math.min(percentageUsed, 100)}%` }}
/>
</div>
<span className="text-yellow-700">{percentageUsed}% used</span>
</div>
<span className="font-semibold text-yellow-800 underline underline-offset-2">
Upgrade
</span>
</button>
);
}
type RoadmapChatProps = {
roadmapId: string;
};
@@ -111,6 +179,19 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
queryClient,
);
const { data: tokenUsage, isLoading: isTokenUsageLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0);
const percentageUsed = Math.round(
((tokenUsage?.used || 0) / (tokenUsage?.limit || 0)) * 100,
);
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const isPaidUser = userBillingDetails?.status === 'active';
const totalTopicCount = useMemo(() => {
const allowedTypes = ['topic', 'subtopic', 'todo'];
return (
@@ -222,51 +303,38 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
<div
className={cn(
'animate-fade-slide-up fixed bottom-5 left-1/2 z-91 max-h-[95vh] max-w-[968px] -translate-x-1/4 transform flex-row gap-1.5 overflow-hidden px-4 transition-all duration-300 sm:max-h-[49vh] lg:flex',
'animate-fade-slide-up fixed bottom-5 left-1/2 z-91 max-h-[95vh] max-w-[968px] -translate-x-1/4 transform flex-col gap-1.5 overflow-hidden px-4 transition-all duration-300 sm:max-h-[55vh] lg:flex',
isOpen ? 'w-full' : 'w-auto',
)}
>
{isOpen && (
<div className="flex h-full max-h-[95vh] w-full flex-col overflow-hidden rounded-lg bg-white shadow-lg sm:max-h-[49vh]">
<>
<div className="flex h-full max-h-[95vh] w-full flex-col overflow-hidden rounded-lg bg-white shadow-lg sm:max-h-[55vh]">
{/* Messages area */}
<div className="flex items-center justify-between px-3 py-2">
<div className="flex">
<ChatHeaderButton
className="hidden sm:flex mr-4"
icon={<BookOpen className="h-3.5 w-3.5" />}
className="mr-2 hidden text-sm sm:flex"
>
AI Tutor
</ChatHeaderButton>
<ChatHeaderButton
onClick={() => {
setIsPersonalizeOpen(true);
{!isPaidUser && (
<UsageButton
percentageUsed={percentageUsed}
onUpgradeClick={() => {
window.open('/premium', '_blank');
}}
icon={<PersonStanding className="h-3.5 w-3.5" />}
className="rounded-md bg-gray-200 py-1 pr-2 pl-1.5 text-gray-500 hover:bg-gray-300"
>
Personalize
</ChatHeaderButton>
/>
)}
</div>
<div className="flex items-center gap-2">
{hasMessages && (
<ChatHeaderButton
onClick={() => {
setInputValue('');
clearChat();
}}
icon={<Trash2 className="h-3.5 w-3.5" />}
className="mr-2 text-gray-500"
>
Clear
</ChatHeaderButton>
)}
<ChatHeaderButton
href={`/${roadmapId}/ai`}
target="_blank"
icon={<AppWindow className="h-3.5 w-3.5" />}
className="rounded-md hidden sm:flex bg-gray-200 py-1 pr-2 pl-1.5 text-gray-500 hover:bg-gray-300"
className="hidden rounded-md bg-gray-200 py-1 pr-2 pl-1.5 text-gray-500 hover:bg-gray-300 sm:flex"
>
Open in new tab
</ChatHeaderButton>
@@ -294,7 +362,8 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
/>
{/* Show default questions only when there's no chat history */}
{aiChatHistory.length === 0 && defaultQuestions.length > 0 && (
{aiChatHistory.length === 0 &&
defaultQuestions.length > 0 && (
<div className="mt-0.5 mb-1">
<p className="mb-2 text-xs font-normal text-gray-500">
Some questions you might have about this roadmap:
@@ -351,13 +420,46 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
</div>
{/* Input area */}
<div className="relative flex items-center border-t border-gray-200 text-sm">
{isLimitExceeded && (
<UpgradeMessage
onUpgradeClick={() => {
window.open('/premium', '_blank');
}}
/>
)}
{!isLimitExceeded && (
<>
<div className="flex flex-row gap-2 border-t border-gray-200 px-3 pt-2">
<ChatHeaderButton
onClick={() => {
setIsPersonalizeOpen(true);
}}
icon={<PersonStanding className="h-3.5 w-3.5" />}
className="rounded-md bg-gray-200 py-1 pr-2 pl-1.5 text-gray-500 hover:bg-gray-300"
>
Personalize
</ChatHeaderButton>
{hasMessages && (
<ChatHeaderButton
onClick={() => {
setInputValue('');
clearChat();
}}
icon={<Trash2 className="h-3.5 w-3.5" />}
className="rounded-md bg-gray-200 py-1 pr-2 pl-1.5 text-gray-500 hover:bg-gray-300"
>
Clear
</ChatHeaderButton>
)}
</div>
<div className="relative flex items-center text-sm">
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
autoFocus
disabled={isLimitExceeded}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
@@ -367,13 +469,20 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
submitInput();
}
}}
placeholder="Ask me anything about this roadmap..."
className="w-full resize-none p-3 outline-none"
placeholder={
isLimitExceeded
? 'You have reached the usage limit for today..'
: 'Ask me anything about this roadmap...'
}
className={cn(
'w-full resize-none px-3 py-4 outline-none',
isLimitExceeded && 'bg-gray-100 text-gray-400',
)}
/>
<button
className="absolute top-1/2 right-2 -translate-y-1/2 p-1 text-zinc-500 hover:text-black disabled:opacity-50"
disabled={isRoadmapDetailLoading}
disabled={isRoadmapDetailLoading || isLimitExceeded}
onClick={() => {
if (isStreamingMessage) {
handleAbort();
@@ -389,7 +498,10 @@ export function RoadmapFloatingChat(props: RoadmapChatProps) {
)}
</button>
</div>
</>
)}
</div>
</>
)}
{!isOpen && (