(0 comments) (0 reactions) (0 assignees)JavaScript (95 forks)auto 404
hacktoberfesthacktoberfest-acceptedinvalid

Repository metrics

Stars
 (553 stars)
PR merge metrics
 (PR metrics pending)

Description

<nav class="bg-blue-600 text-white p-4 shadow-lg sticky top-0 z-50">
    <div class="container mx-auto flex justify-between items-center">
        <div class="flex items-center gap-3">
            <i class="fa-solid fa-school text-2xl"></i>
            <h1 class="text-xl font-bold">School AI Time</h1>
        </div>
        <div class="flex items-center gap-4">
            <div id="realtime-clock" class="text-2xl font-bold clock-font tracking-wider">00:00:00</div>
        </div>
    </div>
</nav>

<div class="container mx-auto p-4 grid grid-cols-1 lg:grid-cols-3 gap-6">
    
    <div class="lg:col-span-2 space-y-6">
        <div class="bg-white rounded-xl shadow-lg p-8 text-center border-l-8 border-blue-500">
            <h2 class="text-gray-500 text-lg mb-2">สถานะปัจจุบัน</h2>
            <div id="current-status-text" class="text-3xl font-bold text-blue-600 mb-4">รอการประกาศ</div>
            
            <div class="bg-blue-50 rounded-lg p-4">
                <p class="text-sm text-gray-600 mb-1">เสียงถัดไป</p>
                <div class="flex justify-between items-center">
                    <span id="next-schedule-name" class="font-semibold">-</span>
                    <span id="next-schedule-time" class="text-blue-600 font-bold">--:--</span>
                </div>
            </div>
        </div>

        <div class="bg-white rounded-xl shadow-lg p-6">
            <h3 class="text-xl font-bold mb-4 border-b pb-2 flex items-center gap-2">
                <i class="fa-solid fa-microphone-lines text-blue-500"></i> 
                <span>ประกาศด้วยข้อความฉุกเฉิน (Text-to-Speech)</span>
            </h3>
            
            <div class="flex flex-wrap gap-2 mb-4">
                <button onclick="setTemplate('ประกาศจากห้องวิชาการ ขอเชิญคณะครูประชุมด่วนที่ห้องโสตทัศนศึกษาค่ะ')" class="text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 py-1 px-3 rounded-full transition">เรียกประชุมครู</button>
                <button onclick="setTemplate('ประกาศจากฝ่ายกิจการนักเรียน ขอให้นักเรียนทุกคนลงมาเข้าแถวเคารพธงชาติที่ลานกิจกรรมในเวลานี้ค่ะ')" class="text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 py-1 px-3 rounded-full transition">เรียกเข้าแถว</button>
                <button onclick="setTemplate('โรงเรียนสุมณฑาศึกษา ยินดีต้อนรับคณะผู้บริหารและแขกผู้มีเกียรติทุกท่านค่ะ')" class="text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 py-1 px-3 rounded-full transition">กล่าวต้อนรับ</button>
            </div>

            <div class="flex flex-col md:flex-row gap-4">
                <input type="text" id="tts-input" class="flex-1 border p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="พิมพ์ข้อความที่ต้องการประกาศที่นี่...">
                <button onclick="speakText()" class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition font-bold shadow-md">
                    <i class="fa-solid fa-volume-high"></i> พูดทันที
                </button>
            </div>
        </div>
    </div>

    <div class="lg:col-span-1">
        <div class="bg-white rounded-xl shadow-lg p-6 sticky top-24">
            <div class="flex justify-between items-center border-b pb-2 mb-4">
                <h3 class="text-xl font-bold flex items-center gap-2">
                    <i class="fa-solid fa-gear text-gray-500"></i> จัดการตารางเวลา
                </h3>
                <label class="flex items-center gap-2 text-sm text-gray-600 bg-gray-50 px-2 py-1 rounded border">
                    <input type="checkbox" id="workday-only" onchange="saveSettings()" checked> 
                    เล่นเฉพาะ จ.-ศ.
                </label>
            </div>

            <div class="overflow-y-auto max-h-[500px] pr-2 custom-scrollbar">
                <div id="schedule-container" class="space-y-3">
                    </div>
            </div>
        </div>
    </div>
</div>

<audio id="main-audio-player"></audio>

<script>
    // --- State Management ---
    const STORAGE_KEY = 'school_ai_time_schedule';
    const SETTINGS_KEY = 'school_ai_time_settings';
    let schedule = []; 
    let lastPlayedTime = ""; // Prevent double play in same minute

    // Default Presets for a standard school day
    const defaultSchedule = [
        { id: 0, time: '07:50', text: 'เตรียมตัวเข้าแถวเคารพธงชาติ' },
        { id: 1, time: '08:30', text: 'หมดเวลาโฮมรูม เริ่มเรียนคาบที่ 1' },
        { id: 2, time: '12:00', text: 'พักรับประทานอาหารกลางวัน' },
        { id: 3, time: '16:00', text: 'หมดเวลาเรียน ขอให้นักเรียนเดินทางกลับบ้านโดยสวัสดิภาพ' }
    ];

    document.addEventListener('DOMContentLoaded', () => {
        loadSettings();
        renderSchedule();
        setInterval(updateClock, 1000);
        setInterval(checkSchedule, 1000);
        updateClock(); 
    });

    function updateClock() {
        const now = new Date();
        const timeString = now.toLocaleTimeString('en-GB', { hour12: false });
        document.getElementById('realtime-clock').innerText = timeString;
        updateNextScheduleDisplay(now);
    }

    // --- TTS Functions ---
    function setTemplate(text) {
        document.getElementById('tts-input').value = text;
    }

    function speakText(textToSpeak = null) {
        const text = textToSpeak || document.getElementById('tts-input').value;
        if (!text) return alert("กรุณาพิมพ์ข้อความ");

        const utterance = new SpeechSynthesisUtterance(text);
        utterance.lang = 'th-TH'; // บังคับภาษาไทย
        utterance.rate = 0.9; // ปรับให้พูดช้าลงนิดหน่อยเพื่อให้ฟังชัดเจนเหมือนประกาศโรงเรียน
        
        // แสดงสถานะ
        document.getElementById('current-status-text').innerText = "กำลังประกาศข้อความ...";
        
        utterance.onend = () => {
            document.getElementById('current-status-text').innerText = "รอการประกาศ";
        };

        window.speechSynthesis.speak(utterance);
    }

    // --- Schedule & Persistence Logic ---
    function loadSettings() {
        const savedSchedule = localStorage.getItem(STORAGE_KEY);
        const savedSettings = localStorage.getItem(SETTINGS_KEY);

        if (savedSchedule) {
            schedule = JSON.parse(savedSchedule);
        } else {
            // Initialize with 10 blank slots + defaults if empty
            schedule = Array.from({length: 10}, (_, i) => {
                const defaultSlot = defaultSchedule.find(d => d.id === i);
                return defaultSlot ? { time: defaultSlot.time, text: defaultSlot.text } : { time: '', text: '' };
            });
        }

        if(savedSettings) {
            const settings = JSON.parse(savedSettings);
            document.getElementById('workday-only').checked = settings.workdayOnly;
        }
    }

    function saveSettings() {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(schedule));
        localStorage.setItem(SETTINGS_KEY, JSON.stringify({
            workdayOnly: document.getElementById('workday-only').checked
        }));
        renderSchedule();
    }

    function renderSchedule() {
        const container = document.getElementById('schedule-container');
        container.innerHTML = '';

        schedule.forEach((slot, index) => {
            const div = document.createElement('div');
            div.className = "flex flex-col gap-2 bg-gray-50 p-3 rounded-lg border border-gray-200 focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500 transition shadow-sm";
            
            div.innerHTML = `
                <div class="flex items-center gap-2">
                    <div class="w-6 font-bold text-gray-400 text-xs">#${index+1}</div>
                    <input type="time" value="${slot.time}" onchange="updateSlot(${index}, 'time', this.value)" class="border rounded p-1 text-sm w-24 outline-none">
                    <input type="text" value="${slot.text}" onchange="updateSlot(${index}, 'text', this.value)" placeholder="ข้อความที่ให้ AI อ่าน..." class="flex-1 border rounded p-1 text-sm outline-none w-full">
                    <button onclick="clearSlot(${index})" class="text-red-400 hover:text-red-600 px-1" title="ล้างข้อมูล"><i class="fa-solid fa-xmark"></i></button>
                </div>
            `;
            container.appendChild(div);
        });
    }

    function updateSlot(index, field, value) {
        schedule[index][field] = value;
        saveSettings();
    }

    function clearSlot(index) {
        schedule[index].time = '';
        schedule[index].text = '';
        saveSettings();
    }

    // --- Automation Core ---
    function checkSchedule() {
        const now = new Date();
        
        // ตรวจสอบวันทำงาน (จันทร์-ศุกร์ คือ 1-5)
        const isWorkdayOnly = document.getElementById('workday-only').checked;
        const currentDay = now.getDay();
        if (isWorkdayOnly && (currentDay === 0 || currentDay === 6)) {
            return; // ข้ามการทำงานถ้านี่คือวันหยุด
        }

        const currentTimeStr = now.toTimeString().substring(0, 5);
        const currentSeconds = now.getSeconds();

        if (currentSeconds !== 0) return; // ทำงานแค่ตอนวินาทีที่ 00

        // ป้องกันการทริกเกอร์ซ้ำในนาทีเดียวกัน
        if (lastPlayedTime === currentTimeStr) return;

        const activeSlot = schedule.find(slot => slot.time === currentTimeStr && slot.text.trim() !== "");

        if (activeSlot) {
            lastPlayedTime = currentTimeStr;
            speakText(activeSlot.text);
        }
    }

    function updateNextScheduleDisplay(now) {
        const currentTimeMin = now.getHours() * 60 + now.getMinutes();
        let nextEvent = null;
        let minDiff = 1440; 

        schedule.forEach(slot => {
            if (!slot.time || !slot.text) return;
            const [h, m] = slot.time.split(':').map(Number);
            const slotTimeMin = h * 60 + m;
            
            let diff = slotTimeMin - currentTimeMin;
            if (diff <= 0) diff += 1440; 

            if (diff > 0 && diff < minDiff) {
                minDiff = diff;
                nextEvent = slot;
            }
        });

        if (nextEvent) {
            document.getElementById('next-schedule-name').innerText = nextEvent.text;
            document.getElementById('next-schedule-time').innerText = nextEvent.time;
        } else {
            document.getElementById('next-schedule-name').innerText = "ไม่มีกำหนดการ";
            document.getElementById('next-schedule-time').innerText = "--:--";
        }
    }
</script>

Contributor guide