Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI-Powered Reminder System</title> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/luxon@3.3.0/build/global/luxon.min.js"></script> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> | |
| <style> | |
| :root { | |
| --primary: #4361ee; | |
| --secondary: #3f37c9; | |
| --success: #4cc9f0; | |
| --danger: #f72585; | |
| --warning: #f8961e; | |
| --light: #f8f9fa; | |
| --dark: #212529; | |
| } | |
| body { | |
| background-color: #f0f2f5; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| .card { | |
| border-radius: 12px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
| transition: transform 0.2s; | |
| } | |
| .card:hover { | |
| transform: translateY(-5px); | |
| } | |
| .priority-low { border-left: 4px solid #6c757d; } | |
| .priority-medium { border-left: 4px solid #ffc107; } | |
| .priority-high { border-left: 4px solid #dc3545; } | |
| .voice-recorder { | |
| background: #e9ecef; | |
| border-radius: 8px; | |
| padding: 15px; | |
| margin: 10px 0; | |
| } | |
| #app { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| .sidebar { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 20px; | |
| height: fit-content; | |
| } | |
| .calendar-day { | |
| border: 1px solid #dee2e6; | |
| min-height: 120px; | |
| padding: 10px; | |
| cursor: pointer; | |
| } | |
| .calendar-day.active { | |
| background-color: #e7f5ff; | |
| border-color: #4361ee; | |
| } | |
| .notification-badge { | |
| position: absolute; | |
| top: -8px; | |
| right: -8px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <!-- Header --> | |
| <header class="py-4 mb-4 border-bottom"> | |
| <div class="container-fluid"> | |
| <div class="row align-items-center"> | |
| <div class="col-md-6"> | |
| <h1 class="display-4 fw-bold"> | |
| <i class="bi bi-alarm"></i> AI-Powered Reminder System | |
| </h1> | |
| </div> | |
| <div class="col-md-6 text-end"> | |
| <button class="btn btn-primary me-2" @click="showNotifications = !showNotifications"> | |
| <i class="bi bi-bell"></i> | |
| <span v-if="notifications.length > 0" class="badge bg-danger notification-badge"> | |
| {{ notifications.length }} | |
| </span> | |
| </button> | |
| <button class="btn btn-outline-primary" @click="showSettings = !showSettings"> | |
| <i class="bi bi-gear"></i> Settings | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="container-fluid"> | |
| <div class="row"> | |
| <!-- Sidebar --> | |
| <div class="col-lg-4 mb-4"> | |
| <div class="sidebar sticky-top"> | |
| <h3 class="mb-4"><i class="bi bi-plus-circle"></i> Add New Reminder</h3> | |
| <form @submit.prevent="createReminder"> | |
| <div class="mb-3"> | |
| <label class="form-label">Title *</label> | |
| <input v-model="newReminder.title" type="text" class="form-control" required> | |
| </div> | |
| <div class="mb-3"> | |
| <label class="form-label">Description</label> | |
| <textarea v-model="newReminder.description" class="form-control" rows="2"></textarea> | |
| </div> | |
| <div class="row mb-3"> | |
| <div class="col-md-6"> | |
| <label class="form-label">Date *</label> | |
| <input v-model="newReminder.due_date" type="date" class="form-control" required> | |
| </div> | |
| <div class="col-md-6"> | |
| <label class="form-label">Time *</label> | |
| <input v-model="newReminder.due_time" type="time" class="form-control" required> | |
| </div> | |
| </div> | |
| <div class="mb-3"> | |
| <label class="form-label">Priority</label> | |
| <select v-model="newReminder.priority" class="form-select"> | |
| <option value="Low">Low</option> | |
| <option value="Medium">Medium</option> | |
| <option value="High">High</option> | |
| </select> | |
| </div> | |
| <div class="mb-3"> | |
| <label class="form-check-label"> | |
| <input type="checkbox" v-model="showVoiceRecorder" class="form-check-input"> | |
| Add Voice Reminder | |
| </label> | |
| </div> | |
| <div v-if="showVoiceRecorder" class="voice-recorder mb-3"> | |
| <div class="d-flex justify-content-between align-items-center mb-2"> | |
| <div> | |
| <button class="btn btn-sm btn-danger" @click="toggleRecording" :disabled="isRecording"> | |
| <i class="bi bi-record-circle"></i> | |
| {{ isRecording ? 'Stop Recording' : 'Start Recording' }} | |
| </button> | |
| <button v-if="audioUrl" class="btn btn-sm btn-outline-secondary ms-2" @click="playRecordedAudio"> | |
| <i class="bi bi-play"></i> Play | |
| </button> | |
| </div> | |
| <audio ref="audioPlayer" :src="audioUrl" hidden></audio> | |
| </div> | |
| <div v-if="recordingStatus" class="text-muted small">{{ recordingStatus }}</div> | |
| </div> | |
| <button type="submit" class="btn btn-primary w-100"> | |
| <i class="bi bi-check-circle"></i> Create Reminder | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="col-lg-8"> | |
| <!-- Dashboard Stats --> | |
| <div class="row mb-4"> | |
| <div class="col-md-4 mb-3"> | |
| <div class="card h-100"> | |
| <div class="card-body text-center"> | |
| <h5 class="card-title">Total Reminders</h5> | |
| <h2 class="text-primary">{{ reminders.length }}</h2> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-4 mb-3"> | |
| <div class="card h-100"> | |
| <div class="card-body text-center"> | |
| <h5 class="card-title">Pending Tasks</h5> | |
| <h2 class="text-warning">{{ pendingReminders }}</h2> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-4 mb-3"> | |
| <div class="card h-100"> | |
| <div class="card-body text-center"> | |
| <h5 class="card-title">High Priority</h5> | |
| <h2 class="text-danger">{{ highPriorityReminders }}</h2> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Calendar View --> | |
| <div class="card mb-4"> | |
| <div class="card-header bg-white"> | |
| <h4 class="mb-0"><i class="bi bi-calendar"></i> Calendar View</h4> | |
| </div> | |
| <div class="card-body"> | |
| <div class="d-flex justify-content-between mb-3"> | |
| <button class="btn btn-sm btn-outline-secondary" @click="prevMonth"> | |
| <i class="bi bi-chevron-left"></i> | |
| </button> | |
| <h5 class="mb-0">{{ currentMonth }}</h5> | |
| <button class="btn btn-sm btn-outline-secondary" @click="nextMonth"> | |
| <i class="bi bi-chevron-right"></i> | |
| </button> | |
| </div> | |
| <div class="calendar-grid"> | |
| <div class="row text-center fw-bold border-bottom mb-2"> | |
| <div class="col p-2">Sun</div> | |
| <div class="col p-2">Mon</div> | |
| <div class="col p-2">Tue</div> | |
| <div class="col p-2">Wed</div> | |
| <div class="col p-2">Thu</div> | |
| <div class="col p-2">Fri</div> | |
| <div class="col p-2">Sat</div> | |
| </div> | |
| <div v-for="week in calendar" :key="week[0].date" class="row"> | |
| <div v-for="day in week" :key="day.date" | |
| class="col calendar-day" | |
| :class="{ | |
| 'active': isToday(day.date), | |
| 'text-muted': day.month !== currentMonthNum | |
| }" | |
| @click="selectedDate = day.date"> | |
| <div class="d-flex justify-content-between"> | |
| <span>{{ day.day }}</span> | |
| <span v-if="countRemindersForDate(day.date) > 0" | |
| class="badge bg-primary rounded-pill"> | |
| {{ countRemindersForDate(day.date) }} | |
| </span> | |
| </div> | |
| <div v-if="isToday(day.date)" class="small text-success">Today</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Reminders for Selected Date --> | |
| <div class="card"> | |
| <div class="card-header bg-white d-flex justify-content-between align-items-center"> | |
| <h4 class="mb-0"><i class="bi bi-list-check"></i> Reminders for {{ selectedDate }}</h4> | |
| <div> | |
| <button class="btn btn-sm btn-outline-primary" @click="getAIInsightsForDate"> | |
| <i class="bi bi-robot"></i> AI Insights | |
| </button> | |
| </div> | |
| </div> | |
| <div class="card-body"> | |
| <div v-if="aiInsights" class="alert alert-info mb-4"> | |
| <h5><i class="bi bi-lightbulb"></i> AI Insights</h5> | |
| <div v-html="aiInsights"></div> | |
| </div> | |
| <div v-if="filteredReminders.length === 0" class="text-center py-5"> | |
| <i class="bi bi-check2-circle" style="font-size: 3rem;"></i> | |
| <h5 class="mt-3">No reminders for this date</h5> | |
| <p class="text-muted">Add a new reminder using the form on the left</p> | |
| </div> | |
| <div v-for="reminder in filteredReminders" :key="reminder.id" class="mb-3"> | |
| <div class="card" :class="'priority-' + reminder.priority.toLowerCase()"> | |
| <div class="card-body"> | |
| <div class="d-flex justify-content-between"> | |
| <div class="form-check form-switch"> | |
| <input class="form-check-input" type="checkbox" | |
| v-model="reminder.completed" | |
| @change="updateReminder(reminder)"> | |
| <label class="form-check-label"> | |
| <h5 class="mb-0">{{ reminder.title }}</h5> | |
| </label> | |
| </div> | |
| <div> | |
| <button class="btn btn-sm btn-outline-secondary" | |
| @click="showAIInsights(reminder)"> | |
| <i class="bi bi-robot"></i> | |
| </button> | |
| <button class="btn btn-sm btn-outline-danger ms-1" | |
| @click="deleteReminder(reminder.id)"> | |
| <i class="bi bi-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-2"> | |
| <div class="text-muted"> | |
| <i class="bi bi-clock"></i> {{ reminder.due_time }} | | |
| <span :class="{ | |
| 'text-danger': reminder.priority === 'High', | |
| 'text-warning': reminder.priority === 'Medium', | |
| 'text-muted': reminder.priority === 'Low' | |
| }"> | |
| {{ reminder.priority }} priority | |
| </span> | |
| </div> | |
| <p v-if="reminder.description" class="mt-2">{{ reminder.description }}</p> | |
| <div v-if="reminder.voice_note" class="mt-2"> | |
| <audio controls :src="reminder.voice_note" class="w-100"></audio> | |
| </div> | |
| <div v-if="reminder.ai_insights" class="mt-3"> | |
| <div class="alert alert-light"> | |
| <h6><i class="bi bi-lightbulb"></i> AI Insights</h6> | |
| <div v-html="reminder.ai_insights"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Notifications Modal --> | |
| <div v-if="showNotifications" class="modal fade show" style="display: block; background: rgba(0,0,0,0.5)"> | |
| <div class="modal-dialog modal-dialog-centered"> | |
| <div class="modal-content"> | |
| <div class="modal-header bg-primary text-white"> | |
| <h5 class="modal-title"><i class="bi bi-bell"></i> Active Notifications</h5> | |
| <button type="button" class="btn-close btn-close-white" @click="showNotifications = false"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <div v-if="notifications.length === 0" class="text-center py-4"> | |
| <i class="bi bi-check2-circle" style="font-size: 3rem;"></i> | |
| <h5 class="mt-3">No active notifications</h5> | |
| </div> | |
| <div v-for="notification in notifications" :key="notification.id" class="alert alert-warning"> | |
| <div class="d-flex justify-content-between align-items-center"> | |
| <div> | |
| <strong>{{ notification.title }}</strong> is due now! | |
| </div> | |
| <button class="btn btn-sm btn-outline-secondary" @click="dismissNotification(notification.id)"> | |
| Dismiss | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const { createApp, ref, computed, onMounted } = Vue; | |
| createApp({ | |
| setup() { | |
| // Application state | |
| const reminders = ref([]); | |
| const notifications = ref([]); | |
| const newReminder = ref({ | |
| title: '', | |
| description: '', | |
| due_date: luxon.DateTime.local().toISODate(), | |
| due_time: luxon.DateTime.local().toFormat('HH:mm'), | |
| priority: 'Medium', | |
| voice_note: '' | |
| }); | |
| const selectedDate = ref(luxon.DateTime.local().toISODate()); | |
| const currentDate = ref(luxon.DateTime.local()); | |
| const showVoiceRecorder = ref(false); | |
| const isRecording = ref(false); | |
| const audioUrl = ref(null); | |
| const audioBlob = ref(null); | |
| const recordingStatus = ref(''); | |
| const showNotifications = ref(false); | |
| const showSettings = ref(false); | |
| const aiInsights = ref(''); | |
| const mediaRecorder = ref(null); | |
| const audioChunks = ref([]); | |
| const audioContext = ref(null); | |
| const audioPlayer = ref(null); | |
| // Computed properties | |
| const pendingReminders = computed(() => { | |
| return reminders.value.filter(r => !r.completed).length; | |
| }); | |
| const highPriorityReminders = computed(() => { | |
| return reminders.value.filter(r => r.priority === 'High' && !r.completed).length; | |
| }); | |
| const filteredReminders = computed(() => { | |
| return reminders.value | |
| .filter(r => r.due_date === selectedDate.value) | |
| .sort((a, b) => { | |
| if (a.completed !== b.completed) return a.completed ? 1 : -1; | |
| if (a.priority !== b.priority) { | |
| const priorityOrder = { 'High': 1, 'Medium': 2, 'Low': 3 }; | |
| return priorityOrder[a.priority] - priorityOrder[b.priority]; | |
| } | |
| return a.due_time.localeCompare(b.due_time); | |
| }); | |
| }); | |
| const currentMonth = computed(() => { | |
| return currentDate.value.toFormat('MMMM yyyy'); | |
| }); | |
| const currentMonthNum = computed(() => { | |
| return currentDate.value.month; | |
| }); | |
| const calendar = computed(() => { | |
| const startOfMonth = currentDate.value.startOf('month'); | |
| const startDate = startOfMonth.startOf('week'); | |
| const endDate = currentDate.value.endOf('month').endOf('week'); | |
| const weeks = []; | |
| let currentDay = startDate; | |
| while (currentDay <= endDate) { | |
| const week = []; | |
| for (let i = 0; i < 7; i++) { | |
| week.push({ | |
| date: currentDay.toISODate(), | |
| day: currentDay.day, | |
| month: currentDay.month | |
| }); | |
| currentDay = currentDay.plus({ days: 1 }); | |
| } | |
| weeks.push(week); | |
| } | |
| return weeks; | |
| }); | |
| // Methods | |
| const loadReminders = async () => { | |
| try { | |
| const response = await axios.get('/api/reminders'); | |
| reminders.value = response.data; | |
| checkForNotifications(); | |
| } catch (error) { | |
| console.error('Error loading reminders:', error); | |
| } | |
| }; | |
| const createReminder = async () => { | |
| try { | |
| const response = await axios.post('/api/reminders', { | |
| ...newReminder.value, | |
| voice_note: audioUrl.value | |
| }); | |
| reminders.value.push(response.data); | |
| newReminder.value = { | |
| title: '', | |
| description: '', | |
| due_date: luxon.DateTime.local().toISODate(), | |
| due_time: luxon.DateTime.local().toFormat('HH:mm'), | |
| priority: 'Medium', | |
| voice_note: '' | |
| }; | |
| audioUrl.value = null; | |
| audioBlob.value = null; | |
| recordingStatus.value = ''; | |
| showVoiceRecorder.value = false; | |
| alert('Reminder created successfully!'); | |
| } catch (error) { | |
| console.error('Error creating reminder:', error); | |
| alert('Failed to create reminder'); | |
| } | |
| }; | |
| const updateReminder = async (reminder) => { | |
| try { | |
| await axios.put(`/api/reminders/${reminder.id}`, reminder); | |
| checkForNotifications(); | |
| } catch (error) { | |
| console.error('Error updating reminder:', error); | |
| } | |
| }; | |
| const deleteReminder = async (id) => { | |
| if (!confirm('Are you sure you want to delete this reminder?')) return; | |
| try { | |
| await axios.delete(`/api/reminders/${id}`); | |
| reminders.value = reminders.value.filter(r => r.id !== id); | |
| checkForNotifications(); | |
| } catch (error) { | |
| console.error('Error deleting reminder:', error); | |
| } | |
| }; | |
| const startRecording = async () => { | |
| try { | |
| audioChunks.value = []; | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| mediaRecorder.value = new MediaRecorder(stream); | |
| mediaRecorder.value.ondataavailable = event => { | |
| audioChunks.value.push(event.data); | |
| }; | |
| mediaRecorder.value.onstop = async () => { | |
| const audioBlob = new Blob(audioChunks.value, { type: 'audio/wav' }); | |
| audioUrl.value = URL.createObjectURL(audioBlob); | |
| // Convert to base64 for saving | |
| const reader = new FileReader(); | |
| reader.readAsDataURL(audioBlob); | |
| reader.onloadend = async () => { | |
| const base64Audio = reader.result; | |
| try { | |
| const response = await axios.post('/api/save-voice-note', { | |
| audio_data: base64Audio | |
| }); | |
| newReminder.value.voice_note = response.data.path; | |
| } catch (error) { | |
| console.error('Error saving voice note:', error); | |
| } | |
| }; | |
| }; | |
| mediaRecorder.value.start(); | |
| isRecording.value = true; | |
| recordingStatus.value = 'Recording... Click stop when finished'; | |
| } catch (error) { | |
| console.error('Error starting recording:', error); | |
| alert('Microphone access denied. Please enable microphone permissions.'); | |
| } | |
| }; | |
| const stopRecording = () => { | |
| if (mediaRecorder.value) { | |
| mediaRecorder.value.stop(); | |
| mediaRecorder.value.stream.getTracks().forEach(track => track.stop()); | |
| isRecording.value = false; | |
| recordingStatus.value = 'Recording saved. Click play to review'; | |
| } | |
| }; | |
| const toggleRecording = () => { | |
| if (isRecording.value) { | |
| stopRecording(); | |
| } else { | |
| startRecording(); | |
| } | |
| }; | |
| const playRecordedAudio = () => { | |
| if (audioPlayer.value && audioUrl.value) { | |
| audioPlayer.value.play(); | |
| } | |
| }; | |
| const countRemindersForDate = (date) => { | |
| return reminders.value.filter(r => r.due_date === date && !r.completed).length; | |
| }; | |
| const isToday = (date) => { | |
| return date === luxon.DateTime.local().toISODate(); | |
| }; | |
| const nextMonth = () => { | |
| currentDate.value = currentDate.value.plus({ months: 1 }); | |
| }; | |
| const prevMonth = () => { | |
| currentDate.value = currentDate.value.minus({ months: 1 }); | |
| }; | |
| const checkForNotifications = () => { | |
| const now = luxon.DateTime.local(); | |
| notifications.value = reminders.value | |
| .filter(r => !r.completed) | |
| .filter(r => { | |
| const reminderDate = luxon.DateTime.fromISO(r.due_date); | |
| const reminderTime = luxon.DateTime.fromISO(`${r.due_date}T${r.due_time}`); | |
| // Check if reminder is due within the last minute | |
| return reminderDate.hasSame(now, 'day') && | |
| Math.abs(reminderTime.diff(now, 'minutes').minutes) < 1; | |
| }) | |
| .map(r => ({ | |
| id: r.id, | |
| title: r.title, | |
| time: r.due_time | |
| })); | |
| if (notifications.value.length > 0) { | |
| showNotifications.value = true; | |
| } | |
| }; | |
| const dismissNotification = (id) => { | |
| notifications.value = notifications.value.filter(n => n.id !== id); | |
| }; | |
| const showAIInsights = async (reminder) => { | |
| try { | |
| const response = await axios.post('/api/ai-insights', { | |
| title: reminder.title, | |
| description: reminder.description | |
| }); | |
| reminder.ai_insights = response.data.insights; | |
| await updateReminder(reminder); | |
| } catch (error) { | |
| console.error('Error getting AI insights:', error); | |
| } | |
| }; | |
| const getAIInsightsForDate = async () => { | |
| const dateReminders = reminders.value.filter(r => r.due_date === selectedDate.value); | |
| if (dateReminders.length === 0) { | |
| aiInsights.value = 'No reminders to analyze for this date'; | |
| return; | |
| } | |
| try { | |
| const response = await axios.post('/api/ai-insights', { | |
| title: `Reminders for ${selectedDate.value}`, | |
| description: `You have ${dateReminders.length} reminders scheduled for this date. | |
| ${dateReminders.filter(r => r.priority === 'High').length} are high priority.` | |
| }); | |
| aiInsights.value = response.data.insights; | |
| } catch (error) { | |
| console.error('Error getting AI insights:', error); | |
| } | |
| }; | |
| // Lifecycle hooks | |
| onMounted(() => { | |
| loadReminders(); | |
| // Check for notifications every minute | |
| setInterval(() => { | |
| checkForNotifications(); | |
| }, 60000); | |
| }); | |
| return { | |
| reminders, | |
| notifications, | |
| newReminder, | |
| selectedDate, | |
| currentDate, | |
| showVoiceRecorder, | |
| isRecording, | |
| audioUrl, | |
| recordingStatus, | |
| showNotifications, | |
| showSettings, | |
| aiInsights, | |
| audioPlayer, | |
| pendingReminders, | |
| highPriorityReminders, | |
| filteredReminders, | |
| currentMonth, | |
| currentMonthNum, | |
| calendar, | |
| createReminder, | |
| updateReminder, | |
| deleteReminder, | |
| toggleRecording, | |
| playRecordedAudio, | |
| countRemindersForDate, | |
| isToday, | |
| nextMonth, | |
| prevMonth, | |
| dismissNotification, | |
| showAIInsights, | |
| getAIInsightsForDate | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> |