1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-06 23:30:42 +02:00
This commit is contained in:
Arik Chakma
2025-05-22 20:08:16 +06:00
parent 85fec7a6af
commit fc250a0a08
5 changed files with 109 additions and 54 deletions

View File

@@ -2,8 +2,9 @@ import type { FormEvent } from 'react';
import { useId, useState } from 'react';
import { httpPost } from '../../lib/http';
import {
COURSE_PURCHASE_PARAM, FIRST_LOGIN_PARAM,
setAuthToken
COURSE_PURCHASE_PARAM,
FIRST_LOGIN_PARAM,
setAuthToken,
} from '../../lib/jwt';
type EmailLoginFormProps = {

View File

@@ -6,7 +6,7 @@ import { AuthenticationForm } from './AuthenticationForm';
<Popup id='login-popup' title='' subtitle=''>
<div class='mb-7 text-center'>
<p class='mb-3 text-2xl font-semibold leading-5 text-slate-900'>
<p class='mb-3 text-2xl leading-5 font-semibold text-slate-900'>
Login or Signup
</p>
<p class='mt-2 text-sm leading-4 text-slate-600'>

View File

@@ -1,9 +1,7 @@
import './RoadmapAIChat.css';
import { useQuery } from '@tanstack/react-query';
import {
roadmapJSONOptions
} from '../../queries/roadmap';
import { roadmapJSONOptions } from '../../queries/roadmap';
import { queryClient } from '../../stores/query-client';
import {
Fragment,
@@ -13,7 +11,13 @@ import {
useRef,
useState,
} from 'react';
import { BotIcon, Frown, Loader2Icon, PauseCircleIcon, SendIcon } from 'lucide-react';
import {
BotIcon,
Frown,
Loader2Icon,
PauseCircleIcon,
SendIcon,
} from 'lucide-react';
import { ChatEditor } from '../ChatEditor/ChatEditor';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import { type AllowedAIChatRole } from '../GenerateCourse/AICourseLessonChat';
@@ -352,24 +356,17 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
)}
</div>
<form
className="relative flex items-start border-t border-gray-200 text-sm"
onSubmit={(e) => {
e.preventDefault();
if (isStreamingMessage && abortControllerRef.current) {
handleAbort();
return;
}
handleChatSubmit(editorRef.current?.getJSON() || {});
}}
>
<div className="relative flex items-start border-t border-gray-200 text-sm">
<ChatEditor
editorRef={editorRef}
roadmapId={roadmapId}
onSubmit={(content) => {
if (isStreamingMessage && abortControllerRef.current) {
handleAbort();
if (isStreamingMessage || abortControllerRef.current) {
return;
}
if (isEmptyContent(content)) {
toast.error('Please enter a message');
return;
}
@@ -378,8 +375,15 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
/>
<button
type="submit"
className="flex aspect-square size-[36px] items-center justify-center p-2 text-zinc-500 hover:text-black disabled:cursor-not-allowed disabled:opacity-50"
onClick={(e) => {
if (isStreamingMessage || abortControllerRef.current) {
handleAbort();
return;
}
handleChatSubmit(editorRef.current?.getJSON() || {});
}}
>
{isStreamingMessage ? (
<PauseCircleIcon className="size-4 stroke-[2.5]" />
@@ -387,12 +391,29 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
<SendIcon className="size-4 stroke-[2.5]" />
)}
</button>
</form>
</div>
</div>
</div>
);
}
function isEmptyContent(content: JSONContent) {
if (!content) {
return true;
}
// because they wrap the content in type doc
const firstContent = content.content?.[0];
if (!firstContent) {
return true;
}
return (
firstContent.type === 'paragraph' &&
(!firstContent?.content || firstContent?.content?.length === 0)
);
}
export function htmlFromTiptapJSON(json: JSONContent) {
const content = json.content;

View File

@@ -136,6 +136,8 @@ export function UserProgressActionList(props: UserProgressActionListProps) {
? progressItemWithText
: progressItemWithText.slice(0, itemCountToShow);
const hasMoreItemsToShow = progressItemWithText.length > itemCountToShow;
return (
<div className="relative my-6 overflow-hidden rounded-lg border border-gray-200 bg-white p-2 first:mt-0 last:mb-0">
<div className="relative flex flex-col gap-2">
@@ -151,7 +153,7 @@ export function UserProgressActionList(props: UserProgressActionListProps) {
/>
))}
{progressItemWithText.length > itemCountToShow && (
{hasMoreItemsToShow && (
<div className="absolute inset-x-0 right-0 bottom-0.5 translate-y-1/2">
<div className="flex items-center justify-center gap-2">
<button
@@ -167,37 +169,41 @@ export function UserProgressActionList(props: UserProgressActionListProps) {
)}
</div>
<div className="absolute top-0 right-0">
<button
className="flex items-center gap-1 rounded-b-md bg-green-100 px-2 py-1 text-[10px] leading-none font-medium text-green-600"
disabled={isBulkUpdating}
onClick={() => {
const done = updateUserProgress
.filter((item) => item.action === 'done')
.map((item) => item.id);
const learning = updateUserProgress
.filter((item) => item.action === 'learning')
.map((item) => item.id);
const skipped = updateUserProgress
.filter((item) => item.action === 'skipped')
.map((item) => item.id);
const pending = updateUserProgress
.filter((item) => item.action === 'pending')
.map((item) => item.id);
{hasMoreItemsToShow && (
<div className="absolute top-0 right-0">
<button
className="flex items-center gap-1 rounded-b-md bg-green-100 px-2 py-1 text-[10px] leading-none font-medium text-green-600"
disabled={isBulkUpdating}
onClick={() => {
const done = updateUserProgress
.filter((item) => item.action === 'done')
.map((item) => item.id);
const learning = updateUserProgress
.filter((item) => item.action === 'learning')
.map((item) => item.id);
const skipped = updateUserProgress
.filter((item) => item.action === 'skipped')
.map((item) => item.id);
const pending = updateUserProgress
.filter((item) => item.action === 'pending')
.map((item) => item.id);
bulkUpdateResourceProgress({
done,
learning,
skipped,
pending,
});
}}
>
{isBulkUpdating && <Loader2Icon className="size-2.5 animate-spin" />}
{!isBulkUpdating && <CheckIcon additionalClasses="size-2.5" />}
Apply All
</button>
</div>
bulkUpdateResourceProgress({
done,
learning,
skipped,
pending,
});
}}
>
{isBulkUpdating && (
<Loader2Icon className="size-2.5 animate-spin" />
)}
{!isBulkUpdating && <CheckIcon additionalClasses="size-2.5" />}
Apply All
</button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,27 @@
---
import { CheckSubscriptionVerification } from '../../../components/Billing/CheckSubscriptionVerification';
import { RoadmapAIChat } from '../../../components/RoadmapAIChat/RoadmapAIChat';
import SkeletonLayout from '../../../layouts/SkeletonLayout.astro';
import { AITutorLayout } from '../../../components/AITutor/AITutorLayout';
type Props = {
roadmapId: string;
};
const { roadmapId } = Astro.params as Props;
---
<SkeletonLayout
title='Roadmap AI Chat'
noIndex={true}
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.'
>
<AITutorLayout
activeTab='chat'
wrapperClassName='flex-row p-0 lg:p-0 overflow-hidden'
client:load
>
<h1>Roadmap AI Chat</h1>
<CheckSubscriptionVerification client:load />
</AITutorLayout>
</SkeletonLayout>