'use client' import { useEffect, useRef, useState } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { AlarmClock, X, Plus, Minus, Timer as TimerIcon } from 'lucide-react' interface Timer { id: string label: string startedAt: string endsAt: string agent?: 'cosmo' | 'lusya' } function formatRemaining(ms: number): { big: string; small?: string } { if (ms <= 0) return { big: '0:00' } const total = Math.ceil(ms / 1000) const h = Math.floor(total / 3600) const m = Math.floor((total % 3600) / 60) const s = total % 60 if (h > 0) { return { big: `${h}:${m.toString().padStart(2, '0')}`, small: `${s.toString().padStart(2, '0')}` } } return { big: `${m}:${s.toString().padStart(2, '0')}` } } async function callTimer(action: string, body: Record) { // Cookie авторизация (браузер планшета уже залогинен под PIN) return fetch('/api/voice/timer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, ...body }), credentials: 'include', }) } export default function TimerHomeWidget() { const [timers, setTimers] = useState([]) const [, setTick] = useState(0) const fetchTimers = async () => { try { const r = await fetch('/api/voice/timer') if (!r.ok) return const d = await r.json() setTimers(d.timers || []) } catch {} } // Подписка на SSE для live-обновлений useEffect(() => { let es: EventSource | null = null let retry: ReturnType | null = null let closed = false const connect = () => { es = new EventSource('/api/voice/stream') es.onmessage = (e) => { try { const evt = JSON.parse(e.data) if (evt.event === 'timer_start' || evt.event === 'timer_cancel') { fetchTimers() } } catch {} } es.onerror = () => { if (closed) return es?.close() retry = setTimeout(connect, 3000) } } fetchTimers() connect() return () => { closed = true if (retry) clearTimeout(retry) es?.close() } }, []) // Тик каждые 500мс для плавного отсчёта useEffect(() => { const t = setInterval(() => setTick(x => x + 1), 500) return () => clearInterval(t) }, []) const now = Date.now() const active = timers .filter(t => new Date(t.endsAt).getTime() > now - 60_000) // прячем просроченные > 1 мин .sort((a, b) => new Date(a.endsAt).getTime() - new Date(b.endsAt).getTime()) return (
Таймеры {active.length > 0 && ( {active.length} )}
{active.length === 0 ? (
Нет активных таймеров
Скажи «поставь таймер на 5 минут»
) : (
{active.map(t => { const end = new Date(t.endsAt).getTime() const start = new Date(t.startedAt).getTime() const total = Math.max(1, end - start) const remain = Math.max(0, end - now) const progress = Math.max(0, Math.min(1, 1 - remain / total)) const imminent = remain > 0 && remain < 10_000 const expired = remain <= 0 const time = formatRemaining(remain) const accent = expired ? '#f87171' : imminent ? '#fb923c' : '#818cf8' const accentBg = expired ? 'linear-gradient(135deg, rgba(239,68,68,0.12), rgba(239,68,68,0.06))' : imminent ? 'linear-gradient(135deg, rgba(251,146,60,0.12), rgba(251,146,60,0.05))' : 'var(--surface-2)' return ( {/* Progress fill as bottom bar */}
{/* Big countdown */}
{time.big}
{time.small && (
:{time.small}
)}
{/* Label */}
{t.label}
{expired ? 'прозвенел' : ''}
{/* Controls */}
) })}
)}
) }