diff --git a/App.tsx b/App.tsx index bb886bb..7401825 100644 --- a/App.tsx +++ b/App.tsx @@ -4,6 +4,7 @@ import { dbService } from './services/db'; import { useZenStore } from './store/zenStore'; import { ExtremeErrorBoundary } from './components/ExtremeErrorBoundary'; import { CryptoErrorBoundary } from './components/CryptoErrorBoundary'; +import { TestDashboard } from './test/TestDashboard'; export default function App() { const { setHistory } = useZenStore(); @@ -15,8 +16,7 @@ export default function App() { const entries = await dbService.getAllEntries(); setHistory(entries); } catch (error) { - console.error("DB Load failed - this is normal if vault is locked:", error); - // Don't throw error - it's normal when vault is locked + console.error('Failed to load history:', error); } }; @@ -27,6 +27,7 @@ export default function App() { + ); diff --git a/components/HistoryPanel.tsx b/components/HistoryPanel.tsx index b508b56..9d790c0 100644 --- a/components/HistoryPanel.tsx +++ b/components/HistoryPanel.tsx @@ -1,7 +1,8 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import { History, TrendingUp, X, Trash2, Brain, Map } from 'lucide-react'; import { ConversationEntry } from '../types'; import { dbService } from '../services/db'; +import { getWidthClass } from '../src/utils/progressUtils'; interface Props { history: ConversationEntry[]; @@ -11,6 +12,25 @@ interface Props { export const HistoryPanel: React.FC = ({ history, onClear }) => { const [isOpen, setIsOpen] = useState(false); + // Handle click outside to close + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + setIsOpen(false); + } + }; + + // Handle ESC key + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + setIsOpen(false); + } + }; + + window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [isOpen]); + // Memoize the heavy analysis logic so it doesn't run on every render const analysis = useMemo(() => { if (history.length === 0) return null; @@ -28,48 +48,37 @@ export const HistoryPanel: React.FC = ({ history, onClear }) => { const avgEmotionalRegulation = displayData.reduce((sum, e) => sum + safeMetric(e, 'emotional_regulation'), 0) / (displayData.length || 1); // DETERMINE MINDFULNESS PROFILE (clinically based) - let profileTitle = "Developing Awareness"; - let description = "Bạn đang xây dựng nền tảng chánh niệm."; - - if (avgPresentMomentAwareness > 0.7 && avgAttentionStability > 0.7) { - profileTitle = "Strong Practice"; // High discipline - description = "Bạn đã phát triển khả năng định tâm vững chãi."; - } else if (avgEmotionalRegulation > 0.7) { - profileTitle = "Emotional Balance"; // High regulation - description = "Bạn điều tiết cảm xúc một cách khéo léo."; - } else if (avgAttentionStability > 0.8) { - profileTitle = "Focused Attention"; // High attention - description = "Bạn có khả năng tập trung ổn định tốt."; + let profileTitle = "Beginning"; + let description = "Starting your mindfulness journey"; + + if (avgPresentMomentAwareness >= 0.7 && avgEmotionalRegulation >= 0.7 && avgAttentionStability >= 0.7) { + profileTitle = "Advanced"; + description = "Deep mindfulness practice established"; + } else if (avgPresentMomentAwareness >= 0.5 && avgEmotionalRegulation >= 0.5 && avgAttentionStability >= 0.5) { + profileTitle = "Intermediate"; + description = "Building consistent mindfulness habits"; + } else if (avgPresentMomentAwareness >= 0.3 || avgEmotionalRegulation >= 0.3 || avgAttentionStability >= 0.3) { + profileTitle = "Developing"; + description = "Early progress in mindfulness practice"; } return { - displayData, + profileTitle, + description, avgAttentionStability, avgPresentMomentAwareness, avgEmotionalRegulation, - profileTitle, - description + displayData }; }, [history]); if (!analysis) return null; - const { displayData, avgAttentionStability, avgPresentMomentAwareness, avgEmotionalRegulation, profileTitle, description } = analysis; + const { profileTitle, description, avgAttentionStability, avgPresentMomentAwareness, avgEmotionalRegulation, displayData } = analysis; - const emotionColor = { - anxious: 'border-orange-400 bg-orange-50', - sad: 'border-blue-400 bg-blue-50', - joyful: 'border-yellow-400 bg-yellow-50', - calm: 'border-emerald-400 bg-emerald-50', - neutral: 'border-stone-300 bg-stone-50' - }; - - const handleClear = async () => { - if (window.confirm('Xóa toàn bộ lịch sử? (Không thể hoàn tác)')) { - await dbService.clearAll(); - onClear(); - setIsOpen(false); - } + const handleClear = () => { + onClear(); + setIsOpen(false); }; return ( @@ -83,71 +92,83 @@ export const HistoryPanel: React.FC = ({ history, onClear }) => { {isOpen && ( -
- {/* Header */} -
-
- -

Mindfulness Profile

+
+
e.stopPropagation()} + > + {/* Header */} +
+
+ +

Mindfulness Profile

+
+
- -
- - {/* Mindfulness Profile (Clinical Metrics) */} -
-
- Practice Level -

{profileTitle}

-

{description}

-
- - {/* Mindfulness Metrics Bar Chart */} -
- - - -
-
- {/* History list */} -
-
- - Journey Log + {/* Mindfulness Profile (Clinical Metrics) */} +
+
+ Practice Level +

{profileTitle}

+

{description}

+
+ + {/* Mindfulness Metrics Bar Chart */} +
+ + + +
- - {[...displayData].reverse().map((entry) => { - const date = new Date(entry.timestamp); - const isToday = date.toDateString() === new Date().toDateString(); - const timeStr = isToday - ? date.toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit' }) - : date.toLocaleDateString('vi-VN', { month: 'numeric', day: 'numeric' }); - - return ( -
-
- {entry.emotion} - {timeStr} -
-
- Attn: {Math.round((entry.mindfulness_metrics?.attention_stability || 0) * 100)} - Reg: {Math.round((entry.mindfulness_metrics?.emotional_regulation || 0) * 100)} + + {/* History list */} +
+
+ + Journey Log +
+ {displayData.map((entry, idx) => { + const timeStr = new Date(entry.timestamp).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit' + }); + return ( +
+
+ {entry.emotion} + {timeStr} +
+
+ Attn: {Math.round((entry.mindfulness_metrics?.attention_stability || 0) * 100)} + Reg: {Math.round((entry.mindfulness_metrics?.emotional_regulation || 0) * 100)} +
-
- ); - })} -
+ ); + })} +
- {/* Clear button */} -
- + {/* Clear button */} +
+ +
)} @@ -160,9 +181,8 @@ const DnaBar = ({ label, value, color }: { label: string, value: number, color: {label}
-); \ No newline at end of file +); diff --git a/components/NarrativeMemory.tsx b/components/NarrativeMemory.tsx index 0cc5f45..5560626 100644 --- a/components/NarrativeMemory.tsx +++ b/components/NarrativeMemory.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { BookOpen, TrendingUp, Target, AlertTriangle, X } from 'lucide-react'; import { ConversationMemory } from '../types'; import { ConversationMemoryService } from '../services/conversationMemoryService'; +import { getWidthClass } from '../src/utils/progressUtils'; interface Props { isOpen: boolean; @@ -19,6 +20,27 @@ export const NarrativeMemory: React.FC = ({ isOpen, onClose, language }) } }, [isOpen]); + // Handle click outside to close + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + // Handle ESC key + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + if (isOpen) { + window.addEventListener('keydown', handleEsc); + } + return () => window.removeEventListener('keydown', handleEsc); + }, [onClose, isOpen]); + const loadMemory = async () => { setLoading(true); try { @@ -55,21 +77,31 @@ export const NarrativeMemory: React.FC = ({ isOpen, onClose, language }) }; return ( -
-
+
+
e.stopPropagation()} + > {/* Header */} -
-
-
- -

{text.title}

+
+
+
+
+
+ +

{text.title}

+
+
-
@@ -174,8 +206,7 @@ export const NarrativeMemory: React.FC = ({ isOpen, onClose, language })
diff --git a/components/PHQ4Assessment.tsx b/components/PHQ4Assessment.tsx index c642c9e..dc971cc 100644 --- a/components/PHQ4Assessment.tsx +++ b/components/PHQ4Assessment.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { ClipboardCheck, AlertCircle, TrendingUp } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { ClipboardCheck, AlertCircle, TrendingUp, X } from 'lucide-react'; import { PHQ4Response, PHQ4Result } from '../types'; interface Props { @@ -16,6 +16,25 @@ export const PHQ4Assessment: React.FC = ({ onComplete, onClose, language q4_worry: -1 }); + // Handle click outside to close + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + // Handle ESC key + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [onClose]); + const questions = language === 'vi' ? { title: "Đánh Giá Sức Khỏe Tâm Lý", subtitle: "Trong 2 tuần qua, con có bị làm phiền bởi những vấn đề sau không?", @@ -92,21 +111,54 @@ export const PHQ4Assessment: React.FC = ({ onComplete, onClose, language }; return ( -
-
+
+
e.stopPropagation()} + > + {/* Close Button */} + + {/* Header */} -
-
- -

{questions.title}

+
+
+
+
+
+ +
+
+

{questions.title}

+

{questions.subtitle}

+
+
+
+
+
PHQ-4
+
+
-

{questions.subtitle}

{/* Questions */} -
+
{/* Depression Questions */} -
+
+
+
+ D +
+

{language === 'vi' ? 'Trầm cảm' : 'Depression'}

+
= ({ onComplete, onClose, language />
-
+
+
+
+
+
+
+ {language === 'vi' ? 'Lo lắng' : 'Anxiety'} +
+
+
{/* Anxiety Questions */} -
+
+
+
+ A +
+

{language === 'vi' ? 'Lo lắng' : 'Anxiety'}

+
= ({ onComplete, onClose, language
{/* Info Note */} -
- -

- {language === 'vi' - ? "PHQ-4 là bài đánh giá sàng lọc ngắn gọn, được chứng minh lâm sàng. Kết quả không thay thế chẩn đoán chuyên nghiệp." - : "PHQ-4 is a brief, clinically validated screening. Results do not replace professional diagnosis."} -

+
+
+
+ +
+
+
+

+ {language === 'vi' + ? "PHQ-4 là bài đánh giá sàng lọc ngắn gọn, được chứng minh lâm sàng. Kết quả không thay thế chẩn đoán chuyên nghiệp." + : "PHQ-4 is a brief, clinically validated screening. Results do not replace professional diagnosis."} +

+

+ {language === 'vi' ? '⏱️ Khoảng 2 phút để hoàn thành' : '⏱️ Takes about 2 minutes to complete'} +

+
{/* Actions */} -
+
@@ -189,25 +272,32 @@ interface QuestionCardProps { const QuestionCard: React.FC = ({ number, question, selected, onSelect, options }) => { return ( -
-
- +
+
+
{number} - -

{question}

+
+

{question}

-
+
{options.map((option, idx) => ( ))}
diff --git a/components/PHQ4Results.tsx b/components/PHQ4Results.tsx index f1ee2eb..6529469 100644 --- a/components/PHQ4Results.tsx +++ b/components/PHQ4Results.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useState } from 'react'; -import { TrendingDown, TrendingUp, Minus, Calendar, Activity, Award } from 'lucide-react'; +import { TrendingDown, TrendingUp, Minus, Calendar, Activity, Award, X } from 'lucide-react'; import { PHQ4Result } from '../types'; import { PHQ4Service } from '../services/phq4Service'; +import { getWidthClass } from '../src/utils/progressUtils'; interface Props { latestResult?: PHQ4Result; @@ -17,6 +18,25 @@ export const PHQ4Results: React.FC = ({ latestResult, onClose, language } loadData(); }, []); + // Handle click outside to close + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + // Handle ESC key + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [onClose]); + const loadData = async () => { const trendData = await PHQ4Service.analyzeTrend(); const historyData = await PHQ4Service.getAll(); @@ -84,22 +104,40 @@ export const PHQ4Results: React.FC = ({ latestResult, onClose, language } }; return ( -
-
+
+
e.stopPropagation()} + > + {/* Close Button */} + + {/* Header */} -
-
-

{text.title}

- {severityIcons[latestResult.severity]} +
+
+
+
+

{text.title}

+ {severityIcons[latestResult.severity]} +
+

+ {new Date(latestResult.timestamp).toLocaleDateString(language === 'vi' ? 'vi-VN' : 'en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + })} +

-

- {new Date(latestResult.timestamp).toLocaleDateString(language === 'vi' ? 'vi-VN' : 'en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - })} -

{/* Score Summary */} @@ -115,8 +153,7 @@ export const PHQ4Results: React.FC = ({ latestResult, onClose, language }
@@ -201,10 +238,10 @@ export const PHQ4Results: React.FC = ({ latestResult, onClose, language }
{/* Close Button */} -
+
@@ -231,8 +268,7 @@ const ScoreCard: React.FC = ({ label, score, max, color }) => {
diff --git a/components/PHQ4Tracker.tsx b/components/PHQ4Tracker.tsx index def6428..1c6eae6 100644 --- a/components/PHQ4Tracker.tsx +++ b/components/PHQ4Tracker.tsx @@ -67,23 +67,23 @@ export const PHQ4Tracker: React.FC = ({ language }) => { {/* Assessment Button */} @@ -92,16 +92,18 @@ export const PHQ4Tracker: React.FC = ({ language }) => { {latestResult && ( )}
diff --git a/index.html b/index.html index e5a9844..5b829a5 100644 --- a/index.html +++ b/index.html @@ -130,6 +130,100 @@ } } + /* Enhanced animations */ + @keyframes glow { + 0%, 100% { + box-shadow: 0 0 20px rgba(0, 243, 255, 0.3); + } + 50% { + box-shadow: 0 0 40px rgba(0, 243, 255, 0.6); + } + } + + @keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } + } + + @keyframes shimmer { + 0% { + background-position: -200% center; + } + 100% { + background-position: 200% center; + } + } + + @keyframes pulse-slow { + 0%, 100% { + opacity: 0.8; + } + 50% { + opacity: 1; + } + } + + @keyframes bounce-subtle { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-2px); + } + } + + @keyframes rotate-slow { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + @keyframes wave { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } + } + + .animate-glow { + animation: glow 2s ease-in-out infinite; + } + + .animate-float { + animation: float 3s ease-in-out infinite; + } + + .animate-shimmer { + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); + background-size: 200% 100%; + animation: shimmer 2s infinite; + } + + .animate-pulse-slow { + animation: pulse-slow 3s ease-in-out infinite; + } + + .animate-bounce-subtle { + animation: bounce-subtle 2s ease-in-out infinite; + } + + .animate-rotate-slow { + animation: rotate-slow 20s linear infinite; + } + + .animate-wave { + animation: wave 2s ease-in-out infinite; + } + /* Scrollbar hiding */ .no-scrollbar::-webkit-scrollbar { display: none; @@ -158,19 +252,91 @@ } } - /* Custom Scrollbar for history */ + /* Modern Scrollbar for history */ .custom-scrollbar::-webkit-scrollbar { - width: 4px; + width: 6px; } .custom-scrollbar::-webkit-scrollbar-track { - background: transparent; + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; } .custom-scrollbar::-webkit-scrollbar-thumb { - background: #d6d3d1; - border-radius: 4px; + background: linear-gradient(180deg, rgba(0, 243, 255, 0.3), rgba(147, 51, 234, 0.3)); + border-radius: 3px; + transition: background-color 0.2s ease; + } + + .custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, rgba(0, 243, 255, 0.5), rgba(147, 51, 234, 0.5)); + } + + /* Enhanced glassmorphism */ + .glass-enhanced { + background: rgba(255, 255, 255, 0.1); + -webkit-backdrop-filter: blur(20px); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + } + + .glass-card { + background: rgba(255, 255, 255, 0.08); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 16px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + } + + /* Modern color palette */ + .primary-gradient { + background: linear-gradient(135deg, #3b82f6, #8b5cf6); + } + + .secondary-gradient { + background: linear-gradient(135deg, #10b981, #06b6d4); + } + + .accent-gradient { + background: linear-gradient(135deg, #f59e0b, #ef4444); + } + + /* Enhanced shadows */ + .shadow-glow { + box-shadow: 0 0 40px rgba(0, 243, 255, 0.3); } + + .shadow-card { + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + } + + /* Dynamic width utilities for progress bars */ + .progress-bar-fill { + transition: all 0.7s ease; + } + + .dna-bar-fill { + transition: all 0.7s ease; + } + + .results-bar-fill { + transition: all 1s ease; + } + + /* Width utilities for progress bars */ + .w-0 { width: 0%; } + .w-10 { width: 10%; } + .w-20 { width: 20%; } + .w-30 { width: 30%; } + .w-40 { width: 40%; } + .w-50 { width: 50%; } + .w-60 { width: 60%; } + .w-70 { width: 70%; } + .w-80 { width: 80%; } + .w-90 { width: 90%; } + .w-100 { width: 100%; }