// =======================================================================
// 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 = `
${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();
}