// ======================================================================= // Notes Application - Feedback & Dev Bug Tracker Widget (Native ES Module) // ChipDip-style floating chat for instant bug reporting & feature requests // ======================================================================= import { api } from './api.js'; import { escapeHtml, showToast } from './ui.js'; let activeCategory = 'bug'; let isWidgetOpen = false; function getCurrentContext() { const hash = window.location.hash || '#dictionary'; const width = window.innerWidth; const height = window.innerHeight; const isMobile = width <= 768; let friendlyName = 'Главная'; if (hash.startsWith('#reader/book/')) { const titleEl = document.getElementById('reader-book-title'); const bookTitle = titleEl ? titleEl.textContent : 'Книга'; friendlyName = `Читалка: ${bookTitle}`; } else if (hash.startsWith('#reader')) { friendlyName = 'Полка книг'; } else if (hash.startsWith('#srs')) { friendlyName = 'SRS Тренировка'; } else if (hash.startsWith('#grammar')) { friendlyName = 'Грамматика'; } else if (hash.startsWith('#cefr')) { friendlyName = 'CEFR Тестирование'; } else if (hash.startsWith('#lesson')) { friendlyName = 'Урок'; } else if (hash.startsWith('#dictionary')) { friendlyName = 'Словарь & Поиск'; } return { url: hash, friendlyName: friendlyName, data: { screen: `${width}×${height}`, device: isMobile ? 'Mobile' : 'Desktop', userAgent: navigator.userAgent.slice(0, 150), timestamp: new Date().toISOString() } }; } function updateContextDisplay() { const indicator = document.getElementById('feedback-context-text'); if (!indicator) return; const ctx = getCurrentContext(); indicator.textContent = `${ctx.friendlyName} • ${ctx.data.device} (${ctx.data.screen})`; } function formatRelativeTime(isoString) { if (!isoString) return ''; try { const date = new Date(isoString); return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); } catch { return isoString; } } function createReportCard(r) { const card = document.createElement('div'); card.className = 'feedback-msg-card'; card.dataset.reportId = r.id; const catLabels = { bug: { label: '🐛 Баг', cls: 'bug' }, feature: { label: '💡 Фича', cls: 'feature' }, note: { label: '📝 Заметка', cls: 'note' } }; const cat = catLabels[r.category] || catLabels.bug; const statusLabels = { new: { label: '🆕 Новый', cls: 'new' }, in_progress: { label: '⏳ В работе', cls: 'in_progress' }, resolved: { label: '✅ Исправлено', cls: 'resolved' }, closed: { label: '✕ Закрыт', cls: 'resolved' } }; const st = statusLabels[r.status] || statusLabels.new; const timeStr = formatRelativeTime(r.created_at); const contextStr = r.context_url ? escapeHtml(r.context_url) : ''; card.innerHTML = `
${cat.label} ${st.label}
${escapeHtml(r.message)}
${timeStr} ${contextStr ? `${contextStr}` : ''}
`; return card; } export async function loadFeedbackList() { const listEl = document.getElementById('feedback-messages-list'); const badgeEl = document.getElementById('feedback-badge'); if (!listEl) return; try { const reports = await api.feedback.getAll(); const activeCount = reports.filter(r => r.status === 'new' || r.status === 'in_progress').length; if (badgeEl) { if (activeCount > 0) { badgeEl.textContent = activeCount; badgeEl.classList.remove('hidden'); } else { badgeEl.classList.add('hidden'); } } listEl.innerHTML = ''; if (reports.length === 0) { listEl.innerHTML = `
📝

Нашли баг или есть идея для фичи?

Напишите сообщение ниже — оно сразу синхронизируется с AI-разработчиком.
`; return; } reports.forEach(r => { listEl.appendChild(createReportCard(r)); }); // Scroll to bottom listEl.scrollTop = listEl.scrollHeight; } catch (err) { console.error('Failed to load feedback reports:', err); } } export function openFeedbackWidget() { const widget = document.getElementById('feedback-widget'); const input = document.getElementById('feedback-input'); if (!widget) return; isWidgetOpen = true; widget.classList.remove('hidden'); updateContextDisplay(); loadFeedbackList(); setTimeout(() => { if (input) input.focus(); }, 100); } export function closeFeedbackWidget() { const widget = document.getElementById('feedback-widget'); if (!widget) return; isWidgetOpen = false; widget.classList.add('hidden'); } export function toggleFeedbackWidget() { if (isWidgetOpen) { closeFeedbackWidget(); } else { openFeedbackWidget(); } } export function initFeedbackWidget() { const triggerBtn = document.getElementById('feedback-trigger-btn'); const closeBtn = document.getElementById('feedback-close-btn'); const widget = document.getElementById('feedback-widget'); const form = document.getElementById('feedback-form'); const input = document.getElementById('feedback-input'); const sendBtn = document.getElementById('feedback-send-btn'); const catChips = document.querySelectorAll('.feedback-cat-chip'); if (triggerBtn) { triggerBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleFeedbackWidget(); }); } if (closeBtn) { closeBtn.addEventListener('click', (e) => { e.stopPropagation(); closeFeedbackWidget(); }); } // Category chips selection catChips.forEach(chip => { chip.addEventListener('click', () => { catChips.forEach(c => c.classList.remove('active')); chip.classList.add('active'); activeCategory = chip.dataset.category || 'bug'; if (input) input.focus(); }); }); // Close on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isWidgetOpen) { closeFeedbackWidget(); } }); // Enter to send, Shift+Enter for new line if (input) { input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (form) form.requestSubmit(); } }); } // Form submit if (form) { form.addEventListener('submit', async (e) => { e.preventDefault(); const text = input ? input.value.trim() : ''; if (!text) return; const ctx = getCurrentContext(); if (sendBtn) sendBtn.disabled = true; try { await api.feedback.create(activeCategory, text, ctx.url, ctx.data); showToast('✅ Репорт отправлен в бэклог AI-разработчику!', 'success'); if (input) input.value = ''; await loadFeedbackList(); } catch (err) { console.error('Feedback submit error:', err); showToast(err.message || 'Ошибка отправки репорта', 'error'); } finally { if (sendBtn) sendBtn.disabled = false; if (input) input.focus(); } }); } // Hash change updates context indicator if open window.addEventListener('hashchange', () => { if (isWidgetOpen) updateContextDisplay(); }); // Initial check of badge count loadFeedbackList(); }