import { useCallback, useEffect, useRef, useState } from "react"; import { Bot, User, Send, Loader2, Mic, MicOff, CheckCircle2, RotateCcw, AlertTriangle, RefreshCw, ArrowRight } from "lucide-react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { Components } from "react-markdown"; import AudioRecorder from "./AudioRecorder"; import { getInterviewResult, type InterviewResult } from "../../../services/interviewApi"; import { useInterviewSession, type InterviewMessage, type InterviewMode, } from "../../../hooks/useInterviewSession"; const markdownComponents: Components = { p: ({ children }) => (

{children}

), ul: ({ children }) => ( ), ol: ({ children }) => (
    {children}
), li: ({ children }) =>
  • {children}
  • , strong: ({ children }) => {children}, em: ({ children }) => {children}, }; interface InterviewPanelProps { roomId: string; userId: string; onComplete: () => void; onResultReady?: (result: InterviewResult) => void; } export default function InterviewPanel({ roomId, userId, onComplete, onResultReady }: InterviewPanelProps) { const { status, messages, isSending, isStarting, startError, interviewResult, isLoaded, startSession, sendTextMessage, connectAudio, disconnectAudio, sendAudioChunk, sendEndUtterance, switchMode, resetSession, } = useInterviewSession(roomId, userId); const [input, setInput] = useState(""); const [audioMode, setAudioMode] = useState("text"); const [audioStreamingText, setAudioStreamingText] = useState(""); const [isAudioConnected, setIsAudioConnected] = useState(false); const messagesEndRef = useRef(null); const audioContextRef = useRef(null); const inputRef = useRef(null); // Auto-scroll to bottom useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, audioStreamingText]); // Auto-start hanya setelah localStorage selesai dimuat dan status benar-benar idle useEffect(() => { if (isLoaded && status === "idle") { startSession("text"); } }, [isLoaded, status]); // eslint-disable-line react-hooks/exhaustive-deps // Notify parent when result is ready (from hook or re-fetched) useEffect(() => { if (status === "completed" && interviewResult) { onResultReady?.(interviewResult); } else if (status === "completed" && !interviewResult) { getInterviewResult(roomId) .then((r) => onResultReady?.(r)) .catch(() => {}); } }, [status, interviewResult]); // eslint-disable-line react-hooks/exhaustive-deps // Setup audio WebSocket when audio mode is active useEffect(() => { if (audioMode !== "audio" || status !== "active") return; if (isAudioConnected) return; setIsAudioConnected(true); connectAudio( (token) => setAudioStreamingText((prev) => prev + token), (fullText) => { setAudioStreamingText(""); // The hook handles adding the message internally, but for audio we add it here // since audio messages arrive differently void fullText; // handled via useInterviewSession internal state if needed }, async (audioBuf) => { // Play TTS audio try { if (!audioContextRef.current || audioContextRef.current.state === "closed") { audioContextRef.current = new AudioContext(); } const ctx = audioContextRef.current; const decoded = await ctx.decodeAudioData(audioBuf.slice(0)); const source = ctx.createBufferSource(); source.buffer = decoded; source.connect(ctx.destination); source.start(); } catch { // ignore playback errors } }, () => { setIsAudioConnected(false); onComplete(); } ); return () => { disconnectAudio(); setIsAudioConnected(false); }; }, [audioMode, status]); // eslint-disable-line react-hooks/exhaustive-deps const handleSendText = useCallback(async () => { const text = input.trim(); if (!text || isSending || status !== "active") return; setInput(""); await sendTextMessage(text); inputRef.current?.focus(); }, [input, isSending, status, sendTextMessage]); const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendText(); } }; const handleModeToggle = () => { const next: InterviewMode = audioMode === "text" ? "audio" : "text"; setAudioMode(next); switchMode(next); setIsAudioConnected(false); setAudioStreamingText(""); }; const handleRestart = () => { setAudioMode("text"); setIsAudioConnected(false); setAudioStreamingText(""); resetSession(); startSession("text"); }; // Loading state saat memanggil API untuk membuat sesi if (isStarting) { return (

    Memulai sesi interview…

    ); } // Error state — backend tidak bisa dijangkau if (startError && status === "idle") { return (

    Tidak dapat terhubung ke server interview

    Pastikan backend interview berjalan di localhost:8080

    ); } return (
    {/* Mode toggle bar */}

    {status === "completed" ? "Interview selesai — lihat hasil di bawah" : "Jawab pertanyaan untuk membantu analisis data Anda"}

    {status === "completed" && ( )} {status === "active" && ( )}
    {/* Messages */}
    {messages.map((msg) => ( ))} {/* Live audio streaming text */} {audioStreamingText && (

    {audioStreamingText}

    )} {/* Completed state */} {status === "completed" && (
    Interview selesai!
    )}
    {/* CTA setelah interview selesai */} {status === "completed" && (
    )} {/* Input area */} {status === "active" && (
    {audioMode === "audio" ? (

    Tahan tombol mikrofon lalu bicara

    ) : (