import { Head } from '@inertiajs/react'; import { BookOpen, Bot, MessageSquare, Plus, Send, Sparkles, Trash2, UserRound, X, } from 'lucide-react'; import type { CSSProperties, FormEvent, KeyboardEvent } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { LoadingIndicator } from '@/components/ui/loading-indicator'; import { Spinner } from '@/components/ui/spinner'; import type { ChatMessageResponse, ChatSessionResponse, CourseResponse, } from '@/lib/rag'; import { createLocalUserMessage } from '@/lib/rag'; import type { AuthUser } from '@/lib/rag-client'; import { createChatSessionEmbed, deleteChatSessionEmbed, getChatHistoryEmbed, listChatSessionsEmbed, listCoursesEmbed, loginWithEmbedApiKey, sendRestChatMessage, } from '@/lib/rag-client'; type EmbedConfig = { background: string; backgroundProfile: string; border: string; courseId?: string | null; danger: string; primary: string; primaryGradient: string; radius: string; subtitle: string; textPrimary: string; textSecondary: string; title: string; userName: string; }; const COLOR_DEFAULTS = { background: '#ffffff', backgroundProfile: '#ffffff', border: '#739af1', danger: '#ef4444', primary: '#2764eb', primaryGradient: '#ccdcff', textPrimary: '#0a0a0a', textSecondary: '#737373', } as const; const EMBED_TOKEN_KEY = 'sevima_raghub_embed_token'; const EMBED_USER_KEY = 'sevima_raghub_embed_user'; type EmbedTokenResult = { token: string; user?: AuthUser; }; function parseColor(value: string | null, fallback: string): string { if (!value) { return fallback; } const normalized = value.startsWith('#') ? value : `#${value}`; return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(normalized) ? normalized : fallback; } function parseRadius(value: string | null): string { if (!value) { return '20px'; } return /^\d{1,2}(?:px|rem)$/.test(value) ? value : '20px'; } function parseEmbedConfig(params: URLSearchParams): EmbedConfig { return { background: parseColor(params.get('background'), COLOR_DEFAULTS.background), backgroundProfile: parseColor(params.get('background_profile'), COLOR_DEFAULTS.backgroundProfile), border: parseColor(params.get('border'), COLOR_DEFAULTS.border), courseId: params.get('course_id') ?? params.get('courseId') ?? undefined, danger: parseColor(params.get('danger'), COLOR_DEFAULTS.danger), primary: parseColor(params.get('primary'), COLOR_DEFAULTS.primary), primaryGradient: parseColor(params.get('primary_gradient'), COLOR_DEFAULTS.primaryGradient), radius: parseRadius(params.get('radius')), subtitle: params.get('subtitle') ?? 'Pilih mata kuliah, lalu kirim pertanyaan pertama untuk membuat sesi baru', textPrimary: parseColor(params.get('text_primary'), COLOR_DEFAULTS.textPrimary), textSecondary: parseColor(params.get('text_secondary'), COLOR_DEFAULTS.textSecondary), title: params.get('title') ?? 'RAG Hub', userName: params.get('user_name') ?? 'User', }; } const COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days function setEmbedCookie(name: string, value: string): void { const secure = window.location.protocol === 'https:'; const parts = [ `${name}=${encodeURIComponent(value)}`, 'path=/', `max-age=${COOKIE_MAX_AGE}`, 'SameSite=None', ]; if (secure) { parts.push('Secure'); } document.cookie = parts.join('; '); } function getEmbedCookie(name: string): string | null { const match = document.cookie.match( new RegExp(`(?:^|;\\s*)${name}=([^;]*)`), ); return match ? decodeURIComponent(match[1]) : null; } function getStoredEmbedToken(): string | null { return getEmbedCookie(EMBED_TOKEN_KEY); } function getStoredEmbedUser(): AuthUser | null { try { const raw = getEmbedCookie(EMBED_USER_KEY); return raw ? (JSON.parse(raw) as AuthUser) : null; } catch { return null; } } function storeEmbedSession(token: string, user?: AuthUser): void { setEmbedCookie(EMBED_TOKEN_KEY, token); if (user) { setEmbedCookie(EMBED_USER_KEY, JSON.stringify(user)); } } async function resolveEmbedToken( params: URLSearchParams, ): Promise { const stored = getStoredEmbedToken(); if (stored) { return { token: stored, user: getStoredEmbedUser() ?? undefined }; } const apiKey = params.get('api_key'); if (apiKey) { try { const { token, user } = await loginWithEmbedApiKey(apiKey); storeEmbedSession(token, user); return { token, user }; } catch { return null; } } return null; } function appendUniqueMessages( current: ChatMessageResponse[], next: ChatMessageResponse[], ): ChatMessageResponse[] { const ids = new Set(current.map((m) => m.uuid_id)); return [...current, ...next.filter((m) => !ids.has(m.uuid_id))]; } function isUserMessage(role: string): boolean { return ['human', 'student', 'user'].includes(role.toLowerCase()); } function formatMessageTime(value?: string): string { if (!value) { return ''; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit', }).format(date); } function TypingText({ text }: { text: string }) { const [visibleLength, setVisibleLength] = useState(0); useEffect(() => { const intervalId = window.setInterval(() => { setVisibleLength((currentLength) => { if (currentLength >= text.length) { window.clearInterval(intervalId); return currentLength; } return currentLength + 1; }); }, 14); return () => window.clearInterval(intervalId); }, [text]); return ( <> {text.slice(0, visibleLength)} {visibleLength < text.length && ( )} ); } function EmbedMessage({ message, shouldAnimateTyping, }: { message: ChatMessageResponse; shouldAnimateTyping?: boolean; }) { const isUser = isUserMessage(message.role); return (
{!isUser && ( )}

{!isUser && shouldAnimateTyping ? ( ) : ( message.content )}

{formatMessageTime(message.created_at)}
{isUser && ( )}
); } function EmbedComposer({ courses, isProcessing, onCourseChange, onQuestionChange, onSubmit, question, selectedCourseId, }: { courses: CourseResponse[]; isProcessing: boolean; onCourseChange: (courseId: string) => void; onQuestionChange: (question: string) => void; onSubmit: (event: FormEvent) => void; question: string; selectedCourseId: string; }) { const canSubmit = !isProcessing && selectedCourseId.length > 0 && question.trim().length > 0; function handleQuestionKeyDown( event: KeyboardEvent, ): void { if ( event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing ) { return; } event.preventDefault(); if (canSubmit) { event.currentTarget.form?.requestSubmit(); } } return (
Mata Kuliah: