'use client'; import { useEffect, useRef, useState } from 'react'; import { Send, ChevronRight, Shield, X, ExternalLink, BookOpen, Scale, RotateCcw } from 'lucide-react'; import { useNavigation } from '@/lib/navigation'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { usePersona } from '@/components/onboarding/usePersona'; interface Citation { index: number; type: 'section' | 'judgment'; id: number; label: string; ref: string; snippet: string; url: string; } interface ChatMessage { role: 'user' | 'assistant'; content: string; citations?: Citation[]; retrievedChunks?: number; error?: boolean; /** Stable unique id for keying — uses timestamp + role + sequence */ uid: string; } const SAMPLE_QUESTIONS = [ 'นายจ้างเลิกจ้างโดยไม่เตือนล่วงหน้า ลูกจ้างมีสิทธิอะไรบ้าง?', 'ค่าจ้างล่วงเวลาคำนวณอย่างไร?', 'ลูกจ้างมีบุริมสิทธิ์เมื่อนายจ้างล้มละลายหรือไม่?', 'การเลิกจ้างเพราะลูกจ้างอายุ 60 ปีถือเป็นการเลิกจ้างที่ชอบหรือไม่?', 'นายจ้างหยุดกิจการ ต้องจ่ายค่าชดเชยอย่างไร?', ]; /** Returns persona-specific sample questions, falling back to defaults */ function useSampleQuestions() { const { persona } = usePersona(); if (persona) return persona.sampleQuestions; return SAMPLE_QUESTIONS; } /** Resolve mascot image source based on UI state (extracted from nested ternary). */ function getMascotSrc(opts: { showEasterEgg: boolean; loading: boolean; openCitation: boolean; thinkingIdx: number; thinkingImgs: readonly string[]; }): string { if (opts.showEasterEgg) return '/mascot/mascot-back.png'; if (opts.loading) return opts.thinkingImgs[opts.thinkingIdx % opts.thinkingImgs.length]; if (opts.openCitation) return '/mascot/mascot-right.png'; return '/mascot/mascot-front.png'; } /** Resolve message bubble className based on role/error (extracted from nested ternary). */ function getBubbleClass(isUser: boolean, isError: boolean): string { if (isUser) return 'bg-gradient-to-br from-gold to-gold/80 text-navy rounded-br-md'; if (isError) return 'card-premium border-destructive/30 rounded-bl-md'; return 'card-premium rounded-bl-md'; } /** Resolve agent step dot className based on done/active state (extracted from nested ternary). */ function getStepDotClass(done: boolean, active: boolean): string { if (done) return 'bg-green-600'; if (active) return 'border-2 border-gold animate-spin'; return 'border border-border/60'; } /** Resolve agent step opacity based on state (extracted from nested ternary). */ function getStepOpacity(opts: { pending: boolean; loading: boolean; idx: number; activeStepIdx: number }): number { if (opts.pending) return 0.35; if (opts.loading && opts.idx > opts.activeStepIdx) return 0.35; return 1; } /** Agent workflow steps — animated while AI is processing */ const AGENT_STEPS = [ { id: 1, label: 'ทำความเข้าใจคำถาม', detail: 'วิเคราะห์ประเด็นและเจตนาของผู้ถาม' }, { id: 2, label: 'ค้นในฐานกฎหมาย', detail: 'FTS5 search ในมาตรา + อนุบัญญัติ' }, { id: 3, label: 'ค้นคำพิพากษาฎีกา', detail: 'ฎีกาแรงงาน + ฎีกาอาญา/แพ่ง' }, { id: 4, label: 'ประเมินความเสี่ยงฝั่งนายจ้าง', detail: 'Risk Matrix + แนวป้องกัน' }, { id: 5, label: 'เรียบเรียงคำตอบพร้อมอ้างอิง', detail: 'Citations [1] [2] [3]...' }, ]; export function AskView() { const { navigate } = useNavigation(); // Restore chat state from sessionStorage (saved before navigating to section/judgment) const [messages, setMessages] = useState(() => { if (typeof window === 'undefined') return []; try { const saved = sessionStorage.getItem('panya_chat_messages'); if (saved) { sessionStorage.removeItem('panya_chat_messages'); return JSON.parse(saved); } } catch {} return []; }); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); // Persona — drives default laborOnly + sample questions + AI prompt customization const { persona, personaId } = usePersona(); // Default laborOnly from persona (HR/Owner = true, Legal = false). User can still override. const [laborOnly, setLaborOnly] = useState(true); // Sync laborOnly when persona changes useEffect(() => { if (persona) setLaborOnly(persona.laborOnly); }, [persona]); const sampleQuestions = useSampleQuestions(); const [inputFocused, setInputFocused] = useState(false); const [activeStepIdx, setActiveStepIdx] = useState(-1); const [openCitation, setOpenCitation] = useState(null); const messagesEndRef = useRef(null); const lastAiMsgRef = useRef(null); const prevLoadingRef = useRef(false); const stepTimerRef = useRef(null); // === MASCOT FEATURES === // 1. Easter egg: 3 clicks on mascot → back view + funny message (4s) const [mascotClicks, setMascotClicks] = useState(0); const [showEasterEgg, setShowEasterEgg] = useState(false); const easterEggTimerRef = useRef(null); // 2. Thinking mascot: cycle front/left/right during loading const [thinkingIdx, setThinkingIdx] = useState(0); const thinkingImgs = ['/mascot/mascot-front.png', '/mascot/mascot-left.png', '/mascot/mascot-right.png']; // 3. Direction-aware: front by default, right when citation panel open, back on easter egg const mascotSrc = getMascotSrc({ showEasterEgg, loading, openCitation: !!openCitation, thinkingIdx, thinkingImgs }); const handleMascotClick = () => { const n = mascotClicks + 1; setMascotClicks(n); if (n >= 3) { setShowEasterEgg(true); setMascotClicks(0); if (easterEggTimerRef.current) clearTimeout(easterEggTimerRef.current); easterEggTimerRef.current = setTimeout(() => setShowEasterEgg(false), 4000); } else { if (easterEggTimerRef.current) clearTimeout(easterEggTimerRef.current); easterEggTimerRef.current = setTimeout(() => setMascotClicks(0), 1500); } }; // === END MASCOT === // Smart scroll: when user sends → scroll to bottom (show loading); // when AI responds → scroll to TOP of AI message (so user reads from beginning, not end) useEffect(() => { const lastMsg = messages.at(-1); const wasLoading = prevLoadingRef.current; if (loading) { // Loading started — scroll to bottom to show AgentRunning animation messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); } else if (wasLoading && lastMsg?.role === 'assistant') { // AI just responded — scroll to TOP of AI message so user reads from start setTimeout(() => { lastAiMsgRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 150); } else if (lastMsg?.role === 'user') { // User sent message — scroll to bottom messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); } prevLoadingRef.current = loading; }, [messages, loading]); // Animate agent steps while loading useEffect(() => { if (loading) { setActiveStepIdx(0); let i = 0; let mi = 0; stepTimerRef.current = setInterval(() => { i++; mi++; setThinkingIdx(mi); if (i >= AGENT_STEPS.length) { // Hold on last step until response arrives setActiveStepIdx(AGENT_STEPS.length - 1); // Note: AGENT_STEPS.length - 1 is the last index — SonarCloud S7755 // prefers .at(-1) but we need the numeric index here, not the element. } else { setActiveStepIdx(i); } }, 800); } else { if (stepTimerRef.current) { clearInterval(stepTimerRef.current); stepTimerRef.current = null; } setActiveStepIdx(-1); } return () => { if (stepTimerRef.current) clearInterval(stepTimerRef.current); }; }, [loading]); const ask = async (question: string) => { if (!question.trim() || loading) return; const userMsg: ChatMessage = { role: 'user', content: question, uid: `u-${Date.now()}` }; setMessages(prev => [...prev, userMsg]); setInput(''); setLoading(true); const history = messages.slice(-4).map(m => ({ role: m.role, content: m.content })); try { const res = await fetch('/api/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, history, laborOnly, persona: personaId }), }); const data = await res.json(); if (data.error) { setMessages(prev => [ ...prev, { role: 'assistant', content: `ขออภัย เกิดข้อผิดพลาด: ${data.error}\n\n${data.message || ''}\n\nอย่างไรก็ตาม ผมค้นพบ ${data.retrievedChunks || 0} มาตรา/ฎีกาที่เกี่ยวข้อง คุณสามารถดู citations ด้านล่างเพื่ออ่านด้วยตัวเอง`, citations: data.citations || [], retrievedChunks: data.retrievedChunks, error: true, uid: `a-err-${Date.now()}`, }, ]); } else { setMessages(prev => [ ...prev, { role: 'assistant', content: data.answer, citations: data.citations || [], retrievedChunks: data.retrievedChunks, uid: `a-${Date.now()}`, }, ]); } } catch (e: any) { setMessages(prev => [ ...prev, { role: 'assistant', content: `เกิดข้อผิดพลาดในการเชื่อมต่อ: ${e.message || 'Unknown error'}`, error: true, uid: `a-throw-${Date.now()}`, }, ]); } finally { setLoading(false); } }; const onSubmit = (e: React.SyntheticEvent) => { e.preventDefault(); ask(input); }; const handleCitationClick = (url: string, cit?: Citation) => { if (cit) setOpenCitation(cit); const params = new URLSearchParams(url.split('?')[1] || ''); const view = params.get('view'); const id = params.get('id'); // Save chat state to sessionStorage before navigating away if (messages.length > 0) { try { sessionStorage.setItem('panya_chat_messages', JSON.stringify(messages)); sessionStorage.setItem('panya_chat_input', input); } catch {} } if (view === 'section' && id) navigate({ name: 'section', sectionId: Number.parseInt(id, 10) }); else if (view === 'judgment' && id) navigate({ name: 'judgment', judgmentId: Number.parseInt(id, 10) }); }; return (
{/* MAIN chat column */}
{/* Header */}

ปรึกษา Panya-AI

Employer Mode

AI ตอบโดยอ้างอิงกฎหมายและคำพิพากษาจริง · ตรวจสอบต้นทางได้ทุกคำตอบ

{/* Labor-only toggle */}
{laborOnly ? 'ค้นหาเฉพาะมาตรา/ฎีกาแรงงาน' : 'ค้นหาในทุกกฎหมาย'}
{/* Messages */}
{messages.length === 0 && !loading && (

สวัสดีครับ ผมปัญญา

ที่ปรึกษากฎหมายไทยฝั่งนายจ้าง · ผมค้นและตอบคำถามได้จากกฎหมาย 78 ฉบับ, มาตรา 12,936, คำพิพากษาฎีกา 502 คดี

{persona && (
👤 บทบาท: {persona.label} · {persona.labelEn}
)}
{sampleQuestions.map((q) => ( ))}
)}
{messages.map((msg, i) => { const isLastAi = i === messages.length - 1 && msg.role === 'assistant'; // Note: SonarCloud S7755 wants .at(-1) here too, but we can't use it // because we need the INDEX comparison, not the element itself. // This is a false positive — the pattern is correct for this use case. return (
); })} {loading && }
{/* Input */}