diff --git a/app/[locale]/dashboard/student/courses/[courseId]/page.tsx b/app/[locale]/dashboard/student/courses/[courseId]/page.tsx index ffd41c65..086fc4e9 100644 --- a/app/[locale]/dashboard/student/courses/[courseId]/page.tsx +++ b/app/[locale]/dashboard/student/courses/[courseId]/page.tsx @@ -1,4 +1,4 @@ -import { CheckCircle, Clock, Dumbbell, FileText } from 'lucide-react' +import { CheckCircle, Clock, Clock10, Dumbbell, FileText } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import { redirect } from 'next/navigation' @@ -17,9 +17,9 @@ const ExerciseCard = ({ title, description, difficulty, type, status, courseId, {title} - - {status === 'Completed' ? : } - {status === 'Completed' ? t('dashboard.student.CourseStudentPage.completed') : t('dashboard.student.CourseStudentPage.notStarted')} + + {status === 'Completed' ? : status === 'In Progress' ? : } + {status === 'Completed' ? t('dashboard.student.CourseStudentPage.completed') : status === 'In Progress' ? t('dashboard.student.CourseStudentPage.inProgress') : t('dashboard.student.CourseStudentPage.notStarted')} {type} - {difficulty} @@ -29,12 +29,15 @@ const ExerciseCard = ({ title, description, difficulty, type, status, courseId, @@ -211,7 +214,8 @@ export default async function CourseStudentPage({ ) ), exercises(*, - exercise_completions(id) + exercise_completions(id), + exercise_messages(id) ) ` ) @@ -220,6 +224,7 @@ export default async function CourseStudentPage({ .eq('lessons.lesson_completions.user_id', userData.data.user.id) .eq('exams.exam_submissions.student_id', userData.data.user.id) .eq('exercises.exercise_completions.user_id', userData.data.user.id) + .eq('exercises.exercise_messages.user_id', userData.data.user.id) .single() if (courseData.error != null) { @@ -294,19 +299,24 @@ export default async function CourseStudentPage({ value="exercises" > {courseData.data.exercises - .map((exercise) => ( - 0 ? 'Completed' : 'Not Started'} - courseId={courseData.data.course_id} - exerciseId={exercise.id} - t={t} - /> - ))} + .map((exercise) => { + // if exercise has a completion, it is completed, else if it has a message, it is in progress else not started + const status = exercise.exercise_completions?.length > 0 ? 'Completed' : exercise.exercise_messages?.length > 0 ? 'In Progress' : 'Not Started' + + return ( + + ) + })} { +export async function POST(request: NextRequest) { try { const reqBody: RequestBody = await request.json() - const { message, instructions } = reqBody const model = google('gemini-1.5-flash') - const result = await generateText({ + const result = await streamText({ model, - system: `Act like a teacher. Always provide responses in plain text without using any markdown or special formatting like **, _ or other font styles. Respond naturally and clearly. ${instructions}`, - prompt: message, + messages: convertToCoreMessages(reqBody.messages), temperature: 0.7, }) - const responseText = result.text - - return new NextResponse(responseText) + return result.toDataStreamResponse() } catch (error) { console.error('Error al generar la respuesta:', error) return NextResponse.json( diff --git a/app/locales/en/components.ts b/app/locales/en/components.ts index e34b0ac3..239800ba 100644 --- a/app/locales/en/components.ts +++ b/app/locales/en/components.ts @@ -534,5 +534,23 @@ If you have doubts, try saying: "Could you give me a fake scenario to practice m vibrantCommunity: 'Join our vibrant learning community', certificates: 'Certificates', earnCertificates: 'Earn certificates upon course completion', + }, + ChatBox: { + closeChat: 'Close chat', + openChat: 'Open chat', + chatAssistant: 'Chat Assistant', + minimize: 'Minimize', + expand: 'Expand', + greeting: "Hi! I'm your AI assistant. How can I help you today?", + quickAccess: { + productQuestions: 'Product & General questions', + shareFeedback: 'Share feedback', + loggingIn: 'Logging in', + reset2FA: 'Reset 2FA', + abuseReport: 'Abuse report', + contactingSales: 'Contacting Sales & Partnerships', + }, + typeMessage: 'Type your message...', + sendMessage: 'Send message', } } as const diff --git a/app/locales/en/views.ts b/app/locales/en/views.ts index adb74d3f..9354bff3 100644 --- a/app/locales/en/views.ts +++ b/app/locales/en/views.ts @@ -144,6 +144,8 @@ export default { startExercise: 'Start Exercise', exercises: 'Exercises', exercisesCompleted: 'Exercises Completed', + inProgress: 'In Progress', + continue: 'Continue', }, LessonPage: { description: 'View and track your progress through the course lessons.', diff --git a/app/locales/es/components.ts b/app/locales/es/components.ts index f72638a5..dfbc08ca 100644 --- a/app/locales/es/components.ts +++ b/app/locales/es/components.ts @@ -531,5 +531,23 @@ Si tienes dudas, intenta diciendo: "Could you give me a fake scenario to practic vibrantCommunity: 'Únete a nuestra vibrante comunidad de aprendizaje', certificates: 'Certificados', earnCertificates: 'Obtén certificados al completar los cursos', + }, + ChatBox: { + closeChat: 'Cerrar chat', + openChat: 'Abrir chat', + chatAssistant: 'Asistente de Chat', + minimize: 'Minimizar', + expand: 'Expandir', + greeting: '¡Hola! Soy tu asistente de IA. ¿Cómo puedo ayudarte hoy?', + quickAccess: { + productQuestions: 'Preguntas sobre productos y generales', + shareFeedback: 'Compartir comentarios', + loggingIn: 'Iniciar sesión', + reset2FA: 'Restablecer 2FA', + abuseReport: 'Informe de abuso', + contactingSales: 'Contactar con Ventas y Asociaciones', + }, + typeMessage: 'Escribe tu mensaje...', + sendMessage: 'Enviar mensaje', } } as const diff --git a/app/locales/es/views.ts b/app/locales/es/views.ts index c0f680ce..41552d0f 100644 --- a/app/locales/es/views.ts +++ b/app/locales/es/views.ts @@ -145,6 +145,8 @@ export default { viewExercise: 'Ver ejercicio', exercises: 'Ejercicios', exercisesCompleted: 'Ejercicios completados', + inProgress: 'En progreso', + continue: 'Continuar', }, LessonPage: { description: 'Ver y realizar un seguimiento de tu progreso a través de la lección.', diff --git a/components/ScrollToTopButton.tsx b/components/ScrollToTopButton.tsx index f8c312db..05b3d0fe 100644 --- a/components/ScrollToTopButton.tsx +++ b/components/ScrollToTopButton.tsx @@ -35,7 +35,7 @@ function ScrollToTopButton () { diff --git a/components/chatbox/ChatBox.tsx b/components/chatbox/ChatBox.tsx index d616933b..d2b70990 100644 --- a/components/chatbox/ChatBox.tsx +++ b/components/chatbox/ChatBox.tsx @@ -1,8 +1,20 @@ 'use client' + +import { generateId } from 'ai' import { useChat } from 'ai/react' -import { Maximize2, MessageSquare, Minimize2, Trash2, X } from 'lucide-react' +import { AnimatePresence, motion } from 'framer-motion' +import { + ChevronDown, + ChevronUp, + Loader2, + MessageSquare, + Send, + X, +} from 'lucide-react' import { useEffect, useRef, useState } from 'react' +import { useScopedI18n } from '@/app/locales/client' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Button } from '@/components/ui/button' import { Card, @@ -14,45 +26,49 @@ import { import { Input } from '@/components/ui/input' import { ScrollArea } from '@/components/ui/scroll-area' +import ChatLoadingSkeleton from '../dashboards/chat/ChatLoadingSkeleton' +import ViewMarkdown from '../ui/markdown/ViewMarkdown' + interface ChatBoxProps { instructions: string } +const quickAccessButtons = [ + { label: 'productQuestions', icon: '📦' }, + { label: 'shareFeedback', icon: '📝' }, + { label: 'loggingIn', icon: '🔐' }, + { label: 'reset2FA', icon: '🔑' }, + { label: 'abuseReport', icon: '🚫' }, + { label: 'contactingSales', icon: '💼' }, +] + export default function ChatBox({ instructions }: ChatBoxProps) { const [isChatOpen, setIsChatOpen] = useState(false) const [isExpanded, setIsExpanded] = useState(false) const scrollAreaRef = useRef(null) const chatBoxRef = useRef(null) + const t = useScopedI18n('ChatBox') const { messages, input, handleInputChange, handleSubmit, - setMessages, isLoading, + append, } = useChat({ - api: '/api/chatbox-ai', // La API personalizada - streamProtocol: 'text', // Protocolo de comunicación - keepLastMessageOnError: true, + api: '/api/chatbox-ai', + initialMessages: [ + { + id: generateId(), + content: instructions, + role: 'system', + }, + ], }) - const toggleChat = () => { - if (isChatOpen) { - setIsChatOpen(false) - setIsExpanded(false) - } else { - setIsChatOpen(true) - } - } - - const clearChat = () => { - setMessages([]) - } - - const toggleExpand = () => { - setIsExpanded((prev) => !prev) - } + const toggleChat = () => setIsChatOpen((prev) => !prev) + const toggleExpand = () => setIsExpanded((prev) => !prev) const scrollToBottom = () => { if (scrollAreaRef.current) { @@ -88,160 +104,199 @@ export default function ChatBox({ instructions }: ChatBoxProps) { } }, []) + const handleQuickAccess = (question: string) => { + append({ + content: t(`quickAccess.${question}`), + role: 'user', + }) + } + return ( <> - {/* Botón para abrir/cerrar el chat */} - {/* Contenedor del chat */} - {isChatOpen && ( -
- - {/* Cabecera del chat */} - - - Chat - -
- {/* Botón para limpiar el chat */} - - {/* Botón para minimizar/maximizar el chat */} - - {/* Botón para cerrar el chat */} - + +
+
+ + + - - Close chat - -
-
- - {/* Contenido del chat */} - - - {messages.length === 0 ? ( -

- No messages yet. -

- ) : ( - messages.map((message) => ( -
- {/* Remitente del mensaje */} -
- {message.role === 'user' - ? 'User' - : 'Bot'} -
- {/* Burbuja del mensaje */} -
- {message.content} -
-
- )) - )} -
-
- - {/* Pie de página del chat con el input y el botón de enviar */} - -
- handleSubmit(e, { - body: { - message: input, // Mensaje del usuario - instructions, // Instrucciones pasadas como props - }, - }) - } - > - - + ) + )} + + + ) : ( + messages.map((message) => { + if (message.role === 'system') { + return null + } + + return ( +
+ + + + {message.role === + 'user' + ? 'U' + : 'A'} + + +
+ +
+
+ ) + }) + )} + {isLoading && } + + + + + + + handleSubmit(e, { + body: { + message: input, + instructions, + }, + }) + } > - {isLoading ? 'Loading...' : 'Send'} - - - - - - )} + + + +
+ + + )} + ) } diff --git a/components/dashboards/student/course/exercises/exerciseChat.tsx b/components/dashboards/student/course/exercises/exerciseChat.tsx index db732b6e..39ecdd80 100644 --- a/components/dashboards/student/course/exercises/exerciseChat.tsx +++ b/components/dashboards/student/course/exercises/exerciseChat.tsx @@ -4,9 +4,11 @@ import { ToolInvocation } from 'ai' import { Message, useChat } from 'ai/react' import dayjs from 'dayjs' import { Paperclip, Send } from 'lucide-react' +import { useRouter } from 'next/navigation' import { useEffect, useRef, useState } from 'react' import { useScopedI18n } from '@/app/locales/client' +import ChatLoadingSkeleton from '@/components/dashboards/chat/ChatLoadingSkeleton' import { SuccessMessage } from '@/components/dashboards/Common/chat/chat' import MarkdownEditor from '@/components/dashboards/Common/chat/MarkdownEditor' import MarkdownEditorTour from '@/components/dashboards/Common/tour/MarkdownEditorTour' @@ -42,6 +44,10 @@ export default function ExerciseChat({ const chatContainerRef = useRef(null) const [isCompleted, setIsCompleted] = useState(isExerciseCompleted) + // Create a ref for the scroll anchor + const scrollAnchorRef = useRef(null) + const router = useRouter() + const { messages, input, handleInputChange, handleSubmit, isLoading, stop, append } = useChat({ api: apiEndpoint ?? '/api/chat/exercises', @@ -55,16 +61,17 @@ export default function ExerciseChat({ console.log('Tool call:', toolCall) if (toolCall.toolName === 'makeUserAssigmentCompleted') { setIsCompleted(true) + router.refresh() } }, }) useEffect(() => { - if (chatContainerRef.current) { - chatContainerRef.current.scrollTop = - chatContainerRef.current.scrollHeight + // Skip the initial render + if (messages.length > initialMessages.length && scrollAnchorRef.current) { + scrollAnchorRef.current.scrollIntoView({ behavior: 'smooth' }) } - }, [messages]) + }, [messages, initialMessages.length]) const handleFileChange = (event: React.ChangeEvent) => { if (event.target.files) { @@ -82,70 +89,76 @@ export default function ExerciseChat({ return ( - - {messages.map((m: Message) => { - if (m.role === 'system') return null + +
+ {messages.map((m: Message) => { + if (m.role === 'system') return null - return ( -
-
- - - - {m.role === 'user' ? ( - profile.full_name[0] - ) : ( - 'A' - )} - - -
-
- +
+ + -
- {dayjs(m.createdAt).format( - 'MMM D, YYYY [at] h:mm A' - )} + + {m.role === 'user' + ? profile.full_name[0] + : 'A'} + + +
+
+ +
+ {dayjs(m.createdAt).format( + 'MMM D, YYYY [at] h:mm A' + )} +
-
- {m.toolInvocations?.map( - ( - toolInvocation: ToolInvocation - ) => { - console.log(toolInvocation) - if ( - toolInvocation.toolName === - 'makeUserAssigmentCompleted' && - 'result' in toolInvocation - ) { - return ( -
- {toolInvocation.result} -
- ) + {m.toolInvocations?.map( + ( + toolInvocation: ToolInvocation + ) => { + console.log(toolInvocation) + if ( + toolInvocation.toolName === + 'makeUserAssigmentCompleted' && + 'result' in + toolInvocation + ) { + return ( +
+ { + toolInvocation.result + } +
+ ) + } + return null } - return null - } - )} - + )} + +
-
- ) - })} + ) + })} + {isLoading && } + {/* Scroll Anchor */} +
+
{isCompleted ? ( <>