chatbot66 / index.html
kritsanan's picture
Update index.html
49dde5d verified
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>กลุ่มม้าขาวก้าวใหม่ เบอร์ 2 - พูดแล้วทำ สำเร็จจริง</title>
<!-- React & ReactDOM -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Babel -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Thai:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Noto Sans Thai', sans-serif;
background-color: #f8fafc;
-webkit-tap-highlight-color: transparent;
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.fade-in { animation: fadeIn 0.5s ease-out; }
.slide-up { animation: slideUp 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.card-shadow { box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.1); }
.nav-active { color: #1e40af; font-weight: 600; }
.nav-item { color: #64748b; }
/* Floating Action Button Pulse */
@keyframes pulse-ring {
0% { box-shadow: 0 0 0 0 rgba(30, 64, 175, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(30, 64, 175, 0); }
100% { box-shadow: 0 0 0 0 rgba(30, 64, 175, 0); }
}
.fab-pulse { animation: pulse-ring 2s infinite; }
/* Loading Dots */
.typing-dot {
animation: typing 1.4s infinite ease-in-out both;
}
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
/* Audio Wave Animation */
.bar {
width: 3px;
background: #2563eb;
animation: sound 0ms -800ms linear infinite alternate;
}
@keyframes sound {
0% { opacity: .35; height: 3px; }
100% { opacity: 1; height: 12px; }
}
.playing .bar { animation-duration: 400ms; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
// --- Helper: PCM to WAV Converter ---
function pcmToWav(pcmData, sampleRate = 24000) {
const buffer = new ArrayBuffer(44 + pcmData.length * 2);
const view = new DataView(buffer);
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + pcmData.length * 2, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
writeString(view, 36, 'data');
view.setUint32(40, pcmData.length * 2, true);
for (let i = 0; i < pcmData.length; i++) {
view.setInt16(44 + i * 2, pcmData[i], true);
}
return new Blob([view], { type: 'audio/wav' });
}
function writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
function base64ToInt16Array(base64) {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new Int16Array(bytes.buffer);
}
// --- Icons ---
const Icon = ({ path, size = 24, className = "" }) => (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} dangerouslySetInnerHTML={{ __html: path }} />
);
const icons = {
home: '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline>',
users: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path>',
book: '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>',
message: '<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>',
sun: '<circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>',
video: '<polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>',
check: '<polyline points="20 6 9 17 4 12"></polyline>',
send: '<line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>',
bot: '<rect x="3" y="11" width="18" height="10" rx="2"></rect><circle cx="12" cy="5" r="2"></circle><path d="M12 7v4"></path><line x1="8" y1="16" x2="8" y2="16"></line><line x1="16" y1="16" x2="16" y2="16"></line>',
sparkle: '<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />',
road: '<path d="M18 20V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16"></path><path d="M2 20h20"></path><path d="M12 2v18"></path><path d="M12 12h4"></path><path d="M12 6h-4"></path><path d="M12 18h4"></path>',
volume: '<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>',
stop: '<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>',
trash: '<polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>',
music: '<path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle>',
heart: '<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>'
};
// --- Data: 13 Policies ---
const policies = [
{ id: 1, title: "ถนนเกษตร/ไฟส่องสว่าง", desc: "ถนนคอนกรีต-ลาดยางสู่ไร่นา พร้อมไฟส่องสว่างทั่วถึง", icon: "road", color: "bg-orange-100 text-orange-600" },
{ id: 2, title: "รถรับส่งฟรี/แอร์ศูนย์เด็ก", desc: "รถรับส่งนักเรียนฟรี ติดแอร์ศูนย์เด็กเล็กทุกแห่ง", icon: "users", color: "bg-pink-100 text-pink-600" },
{ id: 3, title: "CCTV ดูหลานทั่วโลก", desc: "ดูหลานผ่านมือถือได้ทุกที่ ทุกเวลา แม้อยู่นอกประเทศ", icon: "video", color: "bg-blue-100 text-blue-600" },
{ id: 4, title: "โดมยักษ์/ลานตากพืชผล", desc: "สร้างโดมลานกีฬาอเนกประสงค์ ใช้ตากข้าว/พืชผลได้", icon: "home", color: "bg-green-100 text-green-600" },
{ id: 5, title: "ส่งเสริมดนตรี/กีฬา", desc: "จัดหาอุปกรณ์ จัดประกวดทุก 3-6 เดือน เฟ้นหาตัวแทนตำบล", icon: "music", color: "bg-purple-100 text-purple-600" },
{ id: 6, title: "กำจัดขยะครบวงจร", desc: "เพิ่มรถเก็บขยะ สร้างเตาเผาขยะไร้ควัน", icon: "trash", color: "bg-slate-100 text-slate-600" },
{ id: 7, title: "รถตัดหญ้าเคลื่อนที่เร็ว", desc: "รถตัดหญ้าข้างทางขนาดใหญ่ สะอาด ปลอดภัย ทันใจ", icon: "road", color: "bg-emerald-100 text-emerald-600" },
{ id: 8, title: "รถกระเช้าเอนกประสงค์", desc: "ตัดกิ่งไม้ ซ่อมไฟทาง แก้ไขไฟฟ้าส่องสว่างทันท่วงที", icon: "check", color: "bg-yellow-100 text-yellow-600" },
{ id: 9, title: "โซล่าร์ฟาร์มชุมชน", desc: "ตั้งกลุ่มผลิตไฟเพื่อการเกษตรและครัวเรือน", icon: "sun", color: "bg-amber-100 text-amber-600" },
{ id: 10, title: "ศูนย์ดูแลผู้สูงอายุ", desc: "บำบัดฟื้นฟูสุขภาพกายและใจให้เบิกบาน แข็งแรง", icon: "heart", color: "bg-rose-100 text-rose-600" },
{ id: 11, title: "ฟื้นฟูแหล่งท่องเที่ยว", desc: "พัฒนาแหล่งท่องเที่ยวกลุ่มแพ เที่ยวได้ทุกฤดูกาล", icon: "sparkle", color: "bg-cyan-100 text-cyan-600" },
{ id: 12, title: "งานประเพณียิ่งใหญ่", desc: "สงกรานต์ บุญบั้งไฟ แห่เทียน ลอยกระทง จัดใหญ่ขึ้น", icon: "book", color: "bg-red-100 text-red-600" },
{ id: 13, title: "สานต่อสาธารณูปโภค", desc: "น้ำ ไฟ ถนน ทำต่อให้บรรลุวัตถุประสงค์ทุกโครงการ", icon: "check", color: "bg-indigo-100 text-indigo-600" }
];
// Updated Team Members
const teamMembers = [
{ name: "นายสุบรรณ พลรักษา", role: "ผู้สมัคร นายก อบต.", desc: "สานงานต่อ ก่องานใหม่ ผลงาน 4 ปีเป็นประกัน", img: "https://via.placeholder.com/150/2563eb/ffffff?text=นายกฯสุบรรณ" },
{ name: "นายปราโมทย์ อุ่นวงศ์", role: "ผู้สมัคร รองนายกฯ", desc: "ประธานกลุ่มม้าขาวฯ มุ่งมั่นพัฒนา", img: "https://via.placeholder.com/150/2563eb/ffffff?text=รองฯปราโมทย์" },
{ name: "นายสุวิทย์ บุญสาน", role: "ผู้สมัคร รองนายกฯ", desc: "รองนายกฯ คนขยัน เข้าถึงพึ่งได้", img: "https://via.placeholder.com/150/2563eb/ffffff?text=รองฯสุวิทย์" },
{ name: "นายต้อย ทรัพย์พิพิธ", role: "ผู้สมัคร เลขานุการฯ", desc: "เลขานุการคู่ใจ พร้อมรับใช้พี่น้อง", img: "https://via.placeholder.com/150/2563eb/ffffff?text=เลขาฯต้อย" }
];
// --- Gemini API for Villagers ---
const apiKey = "AIzaSyCrlKPfTLrm9rTsOlg0LgljRnWKrjPgVTM"; // Injected
const SYSTEM_PROMPT = `
คุณคือ 'พี่ม้าขาว' (Phi Ma Khao) ผู้ช่วยอัจฉริยะประจำกลุ่มม้าขาวก้าวใหม่ เบอร์ 2 ตำบลหนองกุงทับม้า
ข้อมูลสำคัญสำหรับการเลือกตั้ง:
- วันเลือกตั้ง: อาทิตย์ 28 ธ.ค. 68 เวลา 08.00-17.00 น.
- สโลแกน: "พูดแล้วทำ สำเร็จจริง สานงานต่อ ก่องานใหม่"
นโยบายหลัก 13 ข้อ (ต้องแม่นยำตามนี้):
1. สนับสนุนถนนคอนกรีต/ลาดยางสู่ไร่นา พร้อมไฟส่องสว่างทั่วถึง
2. รถรับ-ส่งนักเรียนฟรี และศูนย์เด็กเล็กติดแอร์ทุกแห่ง
3. CCTV ศูนย์เด็กเล็ก ดูผ่านมือถือได้ทุกที่ทั่วโลก
4. สร้างลานคอนกรีต/โดมยักษ์ทุกชุมชน (ใช้เป็นสนามกีฬา, ดนตรี, ลานตากพืชผล)
5. จัดหาอุปกรณ์กีฬา/นักร้องดนตรี จัดประกวดทุก 3-6 เดือน/1 ปี คัดตัวแทนตำบล
6. ระบบกำจัดขยะ: เพิ่มรถเก็บขยะ, สร้างเตาเผาขยะไร้ควัน
7. รถตัดหญ้าข้างทางเคลื่อนที่เร็วขนาดใหญ่ เพื่อความสะอาดปลอดภัย
8. รถเครนกระเช้า ตัดกิ่งไม้/ซ่อมไฟส่องสว่างทันที
9. สนับสนุนกลุ่มโซล่าฟาร์มเพื่อการเกษตรและครัวเรือน
10. ศูนย์ดูแลผู้สูงอายุ บำบัดฟื้นฟูกายใจให้เบิกบาน
11. ฟื้นฟูแหล่งท่องเที่ยวกลุ่มแพ ให้เที่ยวได้ทุกฤดูกาล
12. ส่งเสริมงานประเพณี (สงกรานต์/บั้งไฟ/แห่เทียน/ลอยกระทง) ให้ใหญ่ขึ้น
13. สานต่องานสาธารณูปโภคเดิม (น้ำ/ไฟ/ถนน) ให้สำเร็จทุกโครงการ
บุคลิกของคุณ:
- พี่ชายที่พึ่งพาได้ สุภาพ อบอุ่น มีความรู้จริง
- ใช้ภาษาไทยกลางที่สุภาพ นุ่มนวล
- เชิญชวนให้เลือก เบอร์ 2 กลุ่มม้าขาวก้าวใหม่
`;
const callGemini = async (prompt, customSystem = null) => {
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
systemInstruction: { parts: [{ text: customSystem || SYSTEM_PROMPT }] }
};
try {
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (!response.ok) throw new Error('API Error');
const data = await response.json();
return data.candidates?.[0]?.content?.parts?.[0]?.text || "ขออภัยครับ พี่ม้าขาวไม่สามารถตอบได้ในขณะนี้";
} catch (error) { return "เกิดข้อผิดพลาดในการเชื่อมต่อครับ"; }
};
const callGeminiTTS = async (text) => {
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`;
const payload = {
contents: [{ parts: [{ text: "พูดด้วยน้ำเสียงอบอุ่น มั่นใจ และเชิญชวน: " + text }] }],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: {
voiceName: "Fenrir"
}
}
}
}
};
try {
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (!response.ok) throw new Error('TTS API Error');
const data = await response.json();
const base64Audio = data.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
if (!base64Audio) throw new Error('No audio data');
return base64Audio;
} catch (error) {
console.error("TTS Error:", error);
return null;
}
};
// --- Components ---
const Nav = ({ activeTab, setTab }) => (
<div className="fixed bottom-0 w-full bg-white border-t border-gray-200 pb-safe z-50 flex justify-around items-center px-2 py-3 shadow-lg">
<button onClick={()=>setTab('home')} className={`flex flex-col items-center gap-1 ${activeTab==='home' ? 'nav-active' : 'nav-item'}`}>
<Icon path={icons.home} size={24} />
<span className="text-[10px]">หน้าหลัก</span>
</button>
<button onClick={()=>setTab('policy')} className={`flex flex-col items-center gap-1 ${activeTab==='policy' ? 'nav-active' : 'nav-item'}`}>
<Icon path={icons.book} size={24} />
<span className="text-[10px]">นโยบาย</span>
</button>
<div className="relative -top-5">
<button onClick={()=>setTab('chat')} className="bg-blue-800 text-white p-4 rounded-full shadow-lg fab-pulse flex items-center justify-center border-4 border-slate-50">
<Icon path={icons.bot} size={28} />
</button>
</div>
<button onClick={()=>setTab('team')} className={`flex flex-col items-center gap-1 ${activeTab==='team' ? 'nav-active' : 'nav-item'}`}>
<Icon path={icons.users} size={24} />
<span className="text-[10px]">ทีมงาน</span>
</button>
<button onClick={()=>setTab('contact')} className={`flex flex-col items-center gap-1 ${activeTab==='contact' ? 'nav-active' : 'nav-item'}`}>
<Icon path={icons.message} size={24} />
<span className="text-[10px]">แจ้งปัญหา</span>
</button>
</div>
);
const Header = () => (
<div className="bg-gradient-to-r from-blue-700 to-indigo-800 text-white p-6 rounded-b-[2rem] shadow-md relative overflow-hidden">
<div className="absolute top-0 right-0 opacity-10 transform translate-x-10 -translate-y-10">
<svg width="200" height="200" viewBox="0 0 200 200"><circle cx="100" cy="100" r="100" fill="white"/></svg>
</div>
<div className="relative z-10 text-center">
<div className="inline-block bg-white text-blue-800 px-3 py-1 rounded-full text-xs font-bold mb-2 shadow-sm">
เลือกตั้ง 28 ธ.ค. 68 (08.00-17.00)
</div>
<h1 className="text-2xl font-bold mb-1">กลุ่มม้าขาวก้าวใหม่</h1>
<p className="text-blue-200 text-sm mb-4">พูดแล้วทำ สำเร็จจริง</p>
<div className="bg-white/20 backdrop-blur-sm rounded-xl p-3 inline-flex items-center gap-4 border border-white/30">
<div className="text-right">
<p className="text-[10px] text-blue-100">นายกฯ สุบรรณ</p>
<p className="font-bold text-lg">เบอร์</p>
</div>
<div className="bg-white text-blue-800 w-12 h-12 rounded-full flex items-center justify-center text-3xl font-bold shadow-lg">
2
</div>
</div>
</div>
</div>
);
const PolicyCard = ({ p }) => {
const [expanded, setExpanded] = useState(false);
const [explanation, setExplanation] = useState("");
const [loading, setLoading] = useState(false);
const [audioLoading, setAudioLoading] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
const audioRef = useRef(null);
const handleExplain = async () => {
if (explanation) {
setExpanded(!expanded);
return;
}
setLoading(true);
setExpanded(true);
const prompt = `ในฐานะพี่ม้าขาว ช่วยอธิบายขยายนโยบาย "${p.title}: ${p.desc}" ให้ชาวบ้านฟังด้วยความจริงใจ สุภาพ และเห็นภาพชัดเจน ว่าสิ่งนี้จะช่วยยกระดับคุณภาพชีวิตของพวกเขาได้อย่างไร`;
const text = await callGemini(prompt);
setExplanation(text);
setLoading(false);
};
const handlePlayAudio = async (e) => {
e.stopPropagation();
if (isPlaying) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
setIsPlaying(false);
return;
}
setAudioLoading(true);
const textToSpeak = `นโยบายข้อนี้ครับ. ${p.desc}. พี่ม้าขาวตั้งใจจะทำเพื่อให้พี่น้องชาวหนองกุงทับม้ามีชีวิตที่ดีขึ้นครับ`;
const base64 = await callGeminiTTS(textToSpeak);
if (base64) {
const pcmData = base64ToInt16Array(base64);
const wavBlob = pcmToWav(pcmData);
const audioUrl = URL.createObjectURL(wavBlob);
if (audioRef.current) {
audioRef.current.src = audioUrl;
audioRef.current.play();
setIsPlaying(true);
audioRef.current.onended = () => setIsPlaying(false);
}
}
setAudioLoading(false);
};
return (
<div className="bg-white p-4 rounded-xl shadow-sm border border-slate-100 mb-3 slide-up transition-all">
<div className="flex items-start gap-4">
<div className={`p-3 rounded-full shrink-0 ${p.color}`} dangerouslySetInnerHTML={{ __html: icons[p.icon] || icons.check }}></div>
<div className="flex-1">
<h3 className="font-bold text-gray-800 text-lg">{p.title}</h3>
<p className="text-gray-500 text-sm leading-relaxed">{p.desc}</p>
<div className="flex gap-2 mt-3">
<button
onClick={handleExplain}
className="text-xs bg-slate-100 text-slate-700 px-3 py-1.5 rounded-full font-bold flex items-center gap-1 hover:bg-slate-200 transition-colors"
>
<span dangerouslySetInnerHTML={{__html: icons.sparkle}} className="w-3 h-3"></span>
{loading ? 'กำลังคิด...' : 'อ่านเพิ่มเติม'}
</button>
<button
onClick={handlePlayAudio}
disabled={audioLoading}
className={`text-xs px-3 py-1.5 rounded-full font-bold flex items-center gap-2 transition-colors ${isPlaying ? 'bg-red-100 text-red-600' : 'bg-blue-100 text-blue-600'}`}
>
{audioLoading ? (
<div className="w-3 h-3 border-2 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
) : (
<>
<span dangerouslySetInnerHTML={{__html: isPlaying ? icons.stop : icons.volume}} className="w-3 h-3"></span>
{isPlaying ? 'หยุดเสียง' : 'ฟังพี่ม้าขาวเล่า'}
</>
)}
{isPlaying && (
<div className="flex items-end gap-[2px] h-3 playing">
<div className="bar"></div>
<div className="bar" style={{animationDelay: '-200ms'}}></div>
<div className="bar" style={{animationDelay: '-400ms'}}></div>
</div>
)}
</button>
</div>
</div>
</div>
{expanded && (
<div className="mt-4 bg-slate-50 p-4 rounded-lg border border-slate-200 text-sm text-gray-700 leading-relaxed animate-fade-in relative">
{loading ? (
<div className="flex space-x-1 h-5 items-center justify-center">
<div className="w-2 h-2 bg-slate-400 rounded-full typing-dot"></div>
<div className="w-2 h-2 bg-slate-400 rounded-full typing-dot"></div>
<div className="w-2 h-2 bg-slate-400 rounded-full typing-dot"></div>
</div>
) : (
<>
<div className="font-bold text-blue-800 mb-2 flex items-center gap-2 border-b border-slate-200 pb-2">
<span className="text-lg">🎙️</span> จากใจพี่ม้าขาว:
</div>
{explanation}
</>
)}
</div>
)}
<audio ref={audioRef} className="hidden" />
</div>
);
};
const ChatInterface = () => {
const [messages, setMessages] = useState([
{ sender: 'bot', text: 'สวัสดีครับ พี่ม้าขาวรายงานตัวครับ! 🤝 \nผมพร้อมให้ข้อมูลนโยบายทั้ง 13 ข้อ และแนะนำทีมบริหารชุดใหม่ สอบถามมาได้เลย!' }
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const scrollRef = useRef(null);
useEffect(() => scrollRef.current?.scrollIntoView({ behavior: "smooth" }), [messages]);
const send = async () => {
if(!input.trim()) return;
const txt = input;
setInput('');
setMessages(prev => [...prev, { sender: 'user', text: txt }]);
setIsLoading(true);
const reply = await callGemini(txt);
setMessages(prev => [...prev, { sender: 'bot', text: reply }]);
setIsLoading(false);
};
return (
<div className="flex flex-col h-[calc(100vh-140px)]">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((m, i) => (
<div key={i} className={`flex ${m.sender==='user'?'justify-end':'justify-start'}`}>
<div className={`max-w-[80%] p-3 rounded-2xl text-sm ${
m.sender==='user'
? 'bg-blue-700 text-white rounded-br-none'
: 'bg-white text-gray-800 border border-gray-200 rounded-bl-none shadow-sm'
}`}>
<p className="whitespace-pre-line">{m.text}</p>
</div>
</div>
))}
{isLoading && <div className="text-xs text-gray-400 text-center animate-pulse">พี่ม้าขาวกำลังพิมพ์...</div>}
<div ref={scrollRef}></div>
</div>
<div className="p-3 bg-white border-t">
<div className="flex gap-2">
<input
value={input}
onChange={e=>setInput(e.target.value)}
onKeyPress={e=>e.key==='Enter' && send()}
placeholder="พิมพ์คำถาม..."
className="flex-1 border rounded-full px-4 py-2 text-sm focus:outline-none focus:border-blue-500 bg-gray-50"
/>
<button onClick={send} className="bg-blue-700 text-white p-2 rounded-full w-10 h-10 flex items-center justify-center hover:bg-blue-800 transition-colors">
<Icon path={icons.send} size={18} />
</button>
</div>
</div>
</div>
);
};
const ContactPage = ({ setTab }) => {
const [problem, setProblem] = useState("");
const [aiDraft, setAiDraft] = useState("");
const [loading, setLoading] = useState(false);
const handleAnalyze = async () => {
if(!problem.trim()) return;
setLoading(true);
const prompt = `ชาวบ้านแจ้งปัญหามาว่า: "${problem}"
ในฐานะพี่ม้าขาว ช่วยร่างหนังสือร้องเรียนแบบเป็นทางการให้หน่อย ระบุหมวดหมู่ปัญหา และใช้ภาษาที่แสดงถึงความกระตือรือร้นที่จะช่วยเหลือ`;
const draft = await callGemini(prompt);
setAiDraft(draft);
setLoading(false);
};
return (
<div className="pb-24 p-4 animate-fade-in">
<h2 className="text-2xl font-bold text-gray-800 mb-4">แจ้งปัญหา/ร้องเรียน</h2>
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100 mb-4">
<label className="block text-sm font-bold text-gray-700 mb-2">มีปัญหาเรื่องอะไร บอกพี่ม้าขาวได้เลยครับ</label>
<textarea
value={problem}
onChange={(e) => setProblem(e.target.value)}
placeholder="เช่น น้ำประปาไม่ไหล, ไฟทางดับ..."
className="w-full p-3 border rounded-xl text-sm h-24 focus:outline-none focus:ring-2 focus:ring-blue-500 bg-gray-50 mb-3"
></textarea>
<button
onClick={handleAnalyze}
disabled={loading || !problem}
className="w-full bg-indigo-700 text-white py-2 rounded-lg font-bold shadow-sm hover:bg-indigo-800 transition-colors flex justify-center items-center gap-2"
>
{loading ? 'กำลังร่างหนังสือ...' : <><span dangerouslySetInnerHTML={{__html: icons.sparkle}} className="w-4 h-4"></span> ให้พี่ม้าขาวช่วยร่างคำร้อง</>}
</button>
</div>
{aiDraft && (
<div className="bg-green-50 p-4 rounded-xl border border-green-200 slide-up">
<h3 className="font-bold text-green-800 mb-2 flex items-center gap-2">
✅ ร่างหนังสือร้องเรียน (โดยพี่ม้าขาว)
</h3>
<div className="bg-white p-3 rounded-lg border border-green-100 text-sm text-gray-700 whitespace-pre-line mb-3 shadow-inner">
{aiDraft}
</div>
<button className="w-full bg-green-600 text-white py-3 rounded-xl font-bold shadow-lg hover:bg-green-700 transition-colors" onClick={() => alert('ส่งเรื่องเรียบร้อย! ทีมงานได้รับข้อมูลแล้วครับ')}>
ยืนยันส่งเรื่องทันที
</button>
</div>
)}
<div className="text-center mt-6">
<a href="tel:0000000000" className="inline-block mt-2 bg-slate-200 text-slate-600 px-6 py-2 rounded-full font-bold hover:bg-slate-300 transition-colors">
📞 โทรหาทีมงาน
</a>
</div>
</div>
);
};
const App = () => {
const [activeTab, setTab] = useState('home');
const renderContent = () => {
switch(activeTab) {
case 'home':
return (
<div className="pb-24 animate-fade-in">
<Header />
<div className="p-4">
<div className="mb-6">
<h2 className="font-bold text-gray-700 text-lg mb-3 flex items-center gap-2">
<Icon path={icons.sun} className="text-orange-500"/> ไฮไลท์นโยบาย
</h2>
<div className="grid grid-cols-2 gap-3">
<div className="bg-white p-3 rounded-xl shadow-sm text-center border border-gray-100" onClick={()=>setTab('policy')}>
<div className="text-4xl mb-2">🛣️</div>
<div className="font-bold text-gray-800 text-sm">ถนนลงนา</div>
</div>
<div className="bg-white p-3 rounded-xl shadow-sm text-center border border-gray-100" onClick={()=>setTab('policy')}>
<div className="text-4xl mb-2">📱</div>
<div className="font-bold text-gray-800 text-sm">กล้องดูหลาน</div>
</div>
</div>
</div>
<div className="bg-blue-50 rounded-xl p-4 border border-blue-100 flex items-center gap-4 cursor-pointer" onClick={()=>setTab('chat')}>
<div className="bg-white p-3 rounded-full shadow-sm text-2xl">🤖</div>
<div>
<h3 className="font-bold text-blue-800">ปรึกษาพี่ม้าขาว</h3>
<p className="text-xs text-blue-600">คุยได้ทุกเรื่อง ตลอด 24 ชม.</p>
</div>
<div className="ml-auto text-blue-400"></div>
</div>
</div>
</div>
);
case 'policy':
return (
<div className="pb-24 p-4 animate-fade-in">
<h2 className="text-2xl font-bold text-gray-800 mb-4">นโยบายทำจริง <span className="text-blue-700">13 ด้าน</span></h2>
{policies.map(p => <PolicyCard key={p.id} p={p} />)}
</div>
);
case 'team':
return (
<div className="pb-24 p-4 animate-fade-in">
<h2 className="text-2xl font-bold text-gray-800 mb-6 text-center">ทีมงาน<span className="text-blue-700">คุณภาพ</span></h2>
<div className="space-y-6">
{teamMembers.map((m, i) => (
<div key={i} className="bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-100 text-center pb-4">
<div className="h-24 bg-gradient-to-b from-blue-700 to-blue-800"></div>
<div className="relative -mt-12 mb-3">
<div className="w-24 h-24 bg-gray-200 rounded-full border-4 border-white mx-auto flex items-center justify-center text-4xl overflow-hidden">
👤
</div>
</div>
<h3 className="font-bold text-lg text-gray-800">{m.name}</h3>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full font-bold">{m.role}</span>
<p className="text-gray-500 text-sm mt-2 px-4">"{m.desc}"</p>
</div>
))}
</div>
</div>
);
case 'chat':
return (
<div className="pb-24 bg-gray-50 h-full animate-fade-in">
<div className="bg-white p-4 shadow-sm border-b flex items-center gap-3 sticky top-0 z-10">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-xl">🤖</div>
<div>
<h3 className="font-bold text-gray-800">พี่ม้าขาว (AI)</h3>
<p className="text-xs text-green-600 flex items-center gap-1"><span className="w-2 h-2 bg-green-500 rounded-full block"></span> พร้อมช่วยเหลือ</p>
</div>
</div>
<ChatInterface />
</div>
);
case 'contact':
return <ContactPage setTab={setTab} />;
default: return null;
}
};
return (
<div className="bg-slate-50 min-h-screen">
{renderContent()}
<Nav activeTab={activeTab} setTab={setTab} />
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>