mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-25 16:39:02 +02:00
feat: add tab navigation for chat and topic details
This commit is contained in:
@@ -65,6 +65,8 @@ export type RoamdapAIChatHistoryType = {
|
|||||||
jsx?: React.ReactNode;
|
jsx?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RoadmapAIChatTab = 'chat' | 'topic';
|
||||||
|
|
||||||
type RoadmapAIChatProps = {
|
type RoadmapAIChatProps = {
|
||||||
roadmapId: string;
|
roadmapId: string;
|
||||||
};
|
};
|
||||||
@@ -82,6 +84,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
const [selectedTopicTitle, setSelectedTopicTitle] = useState<string | null>(
|
const [selectedTopicTitle, setSelectedTopicTitle] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [activeTab, setActiveTab] = useState<RoadmapAIChatTab>('chat');
|
||||||
|
|
||||||
const [aiChatHistory, setAiChatHistory] = useState<
|
const [aiChatHistory, setAiChatHistory] = useState<
|
||||||
RoamdapAIChatHistoryType[]
|
RoamdapAIChatHistoryType[]
|
||||||
@@ -178,6 +181,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
setSelectedTopicId(topicId);
|
setSelectedTopicId(topicId);
|
||||||
setSelectedTopicTitle(topicTitle);
|
setSelectedTopicTitle(topicTitle);
|
||||||
|
setActiveTab('topic');
|
||||||
});
|
});
|
||||||
|
|
||||||
const topicWithSlug = slugify(topicTitle) + '@' + topicId;
|
const topicWithSlug = slugify(topicTitle) + '@' + topicId;
|
||||||
@@ -389,41 +393,6 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex h-full max-w-[40%] flex-grow flex-col border-l border-gray-200 bg-white">
|
<div className="relative flex h-full max-w-[40%] flex-grow flex-col border-l border-gray-200 bg-white">
|
||||||
{selectedTopicId && (
|
|
||||||
<>
|
|
||||||
<TopicDetail
|
|
||||||
resourceId={selectedTopicId}
|
|
||||||
resourceType="roadmap"
|
|
||||||
renderer="editor"
|
|
||||||
canSubmitContribution={false}
|
|
||||||
wrapperClassName="static mx-auto sm:pt-14 pt-14"
|
|
||||||
overlayClassName="hidden"
|
|
||||||
onClose={() => setSelectedTopicId(null)}
|
|
||||||
shouldCloseOnBackdropClick={false}
|
|
||||||
shouldCloseOnEscape={false}
|
|
||||||
/>
|
|
||||||
<div className="absolute top-0 left-0 z-99 flex w-full items-center justify-between gap-2 bg-gray-100 p-1">
|
|
||||||
<div className="flex items-center gap-2 px-4 py-2 text-sm">
|
|
||||||
AI Tutor{' '}
|
|
||||||
<ChevronRightIcon className="size-4 shrink-0 stroke-[2.5] text-gray-500" />{' '}
|
|
||||||
{selectedTopicTitle}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="flex cursor-pointer items-center gap-2 rounded-lg p-2 text-black hover:bg-gray-200"
|
|
||||||
onClick={() => setSelectedTopicId(null)}
|
|
||||||
>
|
|
||||||
<XIcon className="size-3 shrink-0" strokeWidth={2.5} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!selectedTopicId && (
|
|
||||||
<>
|
|
||||||
{showUpgradeModal && (
|
|
||||||
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<RoadmapAIChatHeader
|
<RoadmapAIChatHeader
|
||||||
isLoading={isDataLoading}
|
isLoading={isDataLoading}
|
||||||
hasChatHistory={hasChatHistory}
|
hasChatHistory={hasChatHistory}
|
||||||
@@ -434,8 +403,46 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
onUpgrade={() => {
|
onUpgrade={() => {
|
||||||
setShowUpgradeModal(true);
|
setShowUpgradeModal(true);
|
||||||
}}
|
}}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={(tab) => {
|
||||||
|
setActiveTab(tab);
|
||||||
|
if (tab === 'topic' && selectedTopicId && selectedTopicTitle) {
|
||||||
|
handleSelectTopic(selectedTopicId, selectedTopicTitle);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCloseTopic={() => {
|
||||||
|
setSelectedTopicId(null);
|
||||||
|
setSelectedTopicTitle(null);
|
||||||
|
setActiveTab('chat');
|
||||||
|
}}
|
||||||
|
selectedTopicId={selectedTopicId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{activeTab === 'topic' && selectedTopicId && (
|
||||||
|
<TopicDetail
|
||||||
|
resourceId={selectedTopicId}
|
||||||
|
resourceType="roadmap"
|
||||||
|
renderer="editor"
|
||||||
|
canSubmitContribution={false}
|
||||||
|
wrapperClassName="static mx-auto h-auto grow"
|
||||||
|
overlayClassName="hidden"
|
||||||
|
closeButtonClassName="hidden"
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedTopicId(null);
|
||||||
|
setSelectedTopicTitle(null);
|
||||||
|
setActiveTab('chat');
|
||||||
|
}}
|
||||||
|
shouldCloseOnBackdropClick={false}
|
||||||
|
shouldCloseOnEscape={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'chat' && (
|
||||||
|
<>
|
||||||
|
{showUpgradeModal && (
|
||||||
|
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="relative grow overflow-y-auto" ref={scrollareaRef}>
|
<div className="relative grow overflow-y-auto" ref={scrollareaRef}>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute inset-0 flex h-full w-full items-center justify-center">
|
<div className="absolute inset-0 flex h-full w-full items-center justify-center">
|
||||||
|
@@ -3,12 +3,16 @@ import { getAiCourseLimitOptions } from '../../queries/ai-course';
|
|||||||
import { queryClient } from '../../stores/query-client';
|
import { queryClient } from '../../stores/query-client';
|
||||||
import { billingDetailsOptions } from '../../queries/billing';
|
import { billingDetailsOptions } from '../../queries/billing';
|
||||||
import { isLoggedIn } from '../../lib/jwt';
|
import { isLoggedIn } from '../../lib/jwt';
|
||||||
import { BotIcon, GiftIcon, Trash2Icon } from 'lucide-react';
|
import { BookIcon, BotIcon, GiftIcon, Trash2Icon, XIcon } from 'lucide-react';
|
||||||
import type { RoamdapAIChatHistoryType } from './RoadmapAIChat';
|
import type {
|
||||||
|
RoadmapAIChatTab,
|
||||||
|
RoamdapAIChatHistoryType,
|
||||||
|
} from './RoadmapAIChat';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useToast } from '../../hooks/use-toast';
|
import { useToast } from '../../hooks/use-toast';
|
||||||
import { getPercentage } from '../../lib/number';
|
import { getPercentage } from '../../lib/number';
|
||||||
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
|
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
|
||||||
|
import { cn } from '../../lib/classname';
|
||||||
|
|
||||||
type RoadmapAIChatHeaderProps = {
|
type RoadmapAIChatHeaderProps = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -18,6 +22,11 @@ type RoadmapAIChatHeaderProps = {
|
|||||||
|
|
||||||
onLogin: () => void;
|
onLogin: () => void;
|
||||||
onUpgrade: () => void;
|
onUpgrade: () => void;
|
||||||
|
|
||||||
|
activeTab: RoadmapAIChatTab;
|
||||||
|
onTabChange: (tab: RoadmapAIChatTab) => void;
|
||||||
|
onCloseTopic: () => void;
|
||||||
|
selectedTopicId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) {
|
export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) {
|
||||||
@@ -27,6 +36,11 @@ export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) {
|
|||||||
onLogin,
|
onLogin,
|
||||||
onUpgrade,
|
onUpgrade,
|
||||||
isLoading: isDataLoading,
|
isLoading: isDataLoading,
|
||||||
|
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
|
onCloseTopic,
|
||||||
|
selectedTopicId,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -58,11 +72,43 @@ export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex min-h-[46px] items-center justify-between gap-2 border-b border-gray-200 px-3 py-2 text-sm">
|
<div className="flex h-[46px] items-center justify-between gap-2 border-b border-gray-200 text-sm">
|
||||||
<span className="flex items-center gap-2 text-sm">
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-2 py-3 text-sm',
|
||||||
|
activeTab === 'chat' && selectedTopicId && 'bg-gray-100',
|
||||||
|
)}
|
||||||
|
onClick={() => onTabChange('chat')}
|
||||||
|
>
|
||||||
<BotIcon className="size-4 shrink-0 text-black" />
|
<BotIcon className="size-4 shrink-0 text-black" />
|
||||||
<span>AI Chat</span>
|
<span>AI Chat</span>
|
||||||
</span>
|
</button>
|
||||||
|
|
||||||
|
{(activeTab === 'topic' || selectedTopicId) && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1.5 border-l border-gray-200 px-2 py-3 text-sm',
|
||||||
|
activeTab === 'topic' && selectedTopicId && 'bg-gray-100',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => onTabChange('topic')}
|
||||||
|
>
|
||||||
|
<BookIcon className="size-4 shrink-0 text-black" />
|
||||||
|
<span>Topic Details</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="ml-auto rounded-lg p-1 text-gray-500 hover:bg-gray-200"
|
||||||
|
onClick={onCloseTopic}
|
||||||
|
>
|
||||||
|
<XIcon className="size-4 shrink-0" strokeWidth={2.5} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{!isDataLoading && (
|
{!isDataLoading && (
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
|
@@ -93,6 +93,7 @@ type TopicDetailProps = {
|
|||||||
|
|
||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
overlayClassName?: string;
|
overlayClassName?: string;
|
||||||
|
closeButtonClassName?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
shouldCloseOnBackdropClick?: boolean;
|
shouldCloseOnBackdropClick?: boolean;
|
||||||
shouldCloseOnEscape?: boolean;
|
shouldCloseOnEscape?: boolean;
|
||||||
@@ -106,6 +107,7 @@ export function TopicDetail(props: TopicDetailProps) {
|
|||||||
renderer = 'balsamiq',
|
renderer = 'balsamiq',
|
||||||
wrapperClassName,
|
wrapperClassName,
|
||||||
overlayClassName,
|
overlayClassName,
|
||||||
|
closeButtonClassName,
|
||||||
onClose,
|
onClose,
|
||||||
shouldCloseOnBackdropClick = true,
|
shouldCloseOnBackdropClick = true,
|
||||||
shouldCloseOnEscape = true,
|
shouldCloseOnEscape = true,
|
||||||
@@ -449,7 +451,10 @@ export function TopicDetail(props: TopicDetailProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="close-topic"
|
id="close-topic"
|
||||||
className="flex items-center gap-1.5 rounded-lg bg-gray-200 px-1.5 py-1 text-xs text-black hover:bg-gray-300 hover:text-gray-900"
|
className={cn(
|
||||||
|
'flex items-center gap-1.5 rounded-lg bg-gray-200 px-1.5 py-1 text-xs text-black hover:bg-gray-300 hover:text-gray-900',
|
||||||
|
closeButtonClassName,
|
||||||
|
)}
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
<X className="size-4" />
|
<X className="size-4" />
|
||||||
|
Reference in New Issue
Block a user