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:
@@ -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 && (
|
||||
|
Reference in New Issue
Block a user