رغد Claude Sonnet 4.6 commited on
Commit
b94eb70
·
1 Parent(s): 5d477dc

feat(frontend): add visual Review Mode for UI/UX audit

Browse files

Interactive design audit overlay with:
- Floating toggle button (top-left, Ctrl+Shift+R shortcut)
- Slide-in panel listing all 8 improvements with before/after detail
- Numbered color-coded highlight rings on every changed element:
1 ChatBot messages area (h-56→h-72)
2 ChatBot panel header (width 320→340px)
3 ChatBot input focus ring
4 UploadSection retry button on error
5 RiskDashboard retry button on error
6 VoiceRecorder CSS design tokens (was red-500/blue-500)
7 CompareAnalyses responsive grid (mobile fix)
8 Results page PDF/Print buttons (now functional)
- Step-by-step navigation with arrow keys and panel buttons
- Animated tooltips showing change title on active highlight

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

frontend/app/layout.tsx CHANGED
@@ -2,6 +2,7 @@ import type { Metadata, Viewport } from 'next'
2
  import { Cairo, IBM_Plex_Sans_Arabic, Amiri } from 'next/font/google'
3
  import { Analytics } from '@vercel/analytics/next'
4
  import { Providers } from '@/components/providers'
 
5
  import './globals.css'
6
 
7
  const cairo = Cairo({
@@ -45,7 +46,9 @@ export default function RootLayout({
45
  <html lang="ar" dir="rtl" className="bg-background">
46
  <body className={`${cairo.variable} ${ibmPlexArabic.variable} ${amiri.variable} font-sans antialiased`}>
47
  <Providers>
48
- {children}
 
 
49
  </Providers>
50
  {process.env.NODE_ENV === 'production' && <Analytics />}
51
  </body>
 
2
  import { Cairo, IBM_Plex_Sans_Arabic, Amiri } from 'next/font/google'
3
  import { Analytics } from '@vercel/analytics/next'
4
  import { Providers } from '@/components/providers'
5
+ import { ReviewModeProvider } from '@/components/review-mode'
6
  import './globals.css'
7
 
8
  const cairo = Cairo({
 
46
  <html lang="ar" dir="rtl" className="bg-background">
47
  <body className={`${cairo.variable} ${ibmPlexArabic.variable} ${amiri.variable} font-sans antialiased`}>
48
  <Providers>
49
+ <ReviewModeProvider>
50
+ {children}
51
+ </ReviewModeProvider>
52
  </Providers>
53
  {process.env.NODE_ENV === 'production' && <Analytics />}
54
  </body>
frontend/app/page.tsx CHANGED
@@ -14,6 +14,7 @@ import { AboutSection } from "@/components/about-section"
14
  import { Footer } from "@/components/footer"
15
  import { ChatBot } from "@/components/chat-bot"
16
  import { AnalysisHistory } from "@/components/analysis-history"
 
17
 
18
  const CompareAnalyses = dynamic(() =>
19
  import("@/components/compare-analyses").then((m) => m.CompareAnalyses), { ssr: false }
@@ -179,7 +180,7 @@ function ImprovedResults({ result }: { result: AnalysisResult | null }) {
179
  <h3 className="text-xl font-bold text-foreground mb-2">ملخص الحالة الصحية</h3>
180
  <p className="text-muted-foreground leading-relaxed mb-4">{result.summary}</p>
181
  {result.report?.general && <p className="text-foreground/80 leading-relaxed text-sm mb-5">{result.report.general}</p>}
182
- <div className="flex flex-wrap gap-3">
183
  <motion.button whileTap={{ scale: 0.97 }} onClick={handleExport}
184
  className="flex items-center gap-2 px-5 py-2.5 rounded-2xl gradient-primary text-primary-foreground text-sm font-medium shadow-glow-primary hover:opacity-90 transition-opacity">
185
  {exporting
@@ -192,7 +193,7 @@ function ImprovedResults({ result }: { result: AnalysisResult | null }) {
192
  <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" /></svg>
193
  طباعة
194
  </motion.button>
195
- </div>
196
  </div>
197
  </div>
198
  </motion.div>
 
14
  import { Footer } from "@/components/footer"
15
  import { ChatBot } from "@/components/chat-bot"
16
  import { AnalysisHistory } from "@/components/analysis-history"
17
+ import { ReviewHighlight } from "@/components/review-mode"
18
 
19
  const CompareAnalyses = dynamic(() =>
20
  import("@/components/compare-analyses").then((m) => m.CompareAnalyses), { ssr: false }
 
180
  <h3 className="text-xl font-bold text-foreground mb-2">ملخص الحالة الصحية</h3>
181
  <p className="text-muted-foreground leading-relaxed mb-4">{result.summary}</p>
182
  {result.report?.general && <p className="text-foreground/80 leading-relaxed text-sm mb-5">{result.report.general}</p>}
183
+ <ReviewHighlight changeId={8} className="flex flex-wrap gap-3">
184
  <motion.button whileTap={{ scale: 0.97 }} onClick={handleExport}
185
  className="flex items-center gap-2 px-5 py-2.5 rounded-2xl gradient-primary text-primary-foreground text-sm font-medium shadow-glow-primary hover:opacity-90 transition-opacity">
186
  {exporting
 
193
  <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" /></svg>
194
  طباعة
195
  </motion.button>
196
+ </ReviewHighlight>
197
  </div>
198
  </div>
199
  </motion.div>
frontend/components/chat-bot.tsx CHANGED
@@ -5,6 +5,7 @@ import { motion, AnimatePresence } from "framer-motion"
5
  import type { AnalysisResult } from "@/app/page"
6
  import { VoiceRecorder } from "./voice-recorder"
7
  import { apiGet, apiPost } from "@/lib/api"
 
8
 
9
  const WELCOME = "مرحباً! أنا مساعدك الطبي. كيف يمكنني مساعدتك اليوم؟"
10
 
@@ -162,7 +163,7 @@ export function ChatBot({ analysisResult, sessionId = "anonymous" }: ChatBotProp
162
  style={{ border: "1px solid var(--border)" }}
163
  >
164
  {/* Header */}
165
- <div className="px-5 py-4 flex items-center gap-3 border-b border-border/50">
166
  <div className="w-9 h-9 rounded-full flex items-center justify-center bg-primary/15">
167
  <svg className="w-4 h-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
168
  <path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z" />
@@ -172,11 +173,10 @@ export function ChatBot({ analysisResult, sessionId = "anonymous" }: ChatBotProp
172
  <span className="text-sm font-semibold text-foreground">المساعد الذكي</span>
173
  <p className="text-[10px] text-muted-foreground">مساعد طبي ذكي</p>
174
  </div>
175
- </div>
176
 
177
  {/* Messages */}
178
- <div className="h-72 overflow-y-auto px-4 py-3 space-y-3 scroll-smooth overscroll-contain"
179
- style={{ scrollbarWidth: "thin", scrollbarColor: "var(--border) transparent" }}>
180
  <AnimatePresence mode="popLayout">
181
  {messages.map((msg, i) => (
182
  <motion.div
@@ -212,7 +212,7 @@ export function ChatBot({ analysisResult, sessionId = "anonymous" }: ChatBotProp
212
  )}
213
  </AnimatePresence>
214
  <div ref={messagesEndRef} />
215
- </div>
216
 
217
  {/* FAQ quick questions */}
218
  <div className="px-4 pb-2">
@@ -242,7 +242,7 @@ export function ChatBot({ analysisResult, sessionId = "anonymous" }: ChatBotProp
242
  </div>
243
 
244
  {/* Input */}
245
- <div className="px-4 pb-4 pt-2">
246
  <div className="flex items-center gap-2 rounded-full px-1 py-1 bg-input border border-border/60">
247
  <VoiceRecorder
248
  onTranscription={(text) => sendMessage(text)}
@@ -271,7 +271,7 @@ export function ChatBot({ analysisResult, sessionId = "anonymous" }: ChatBotProp
271
  </svg>
272
  </motion.button>
273
  </div>
274
- </div>
275
  </motion.div>
276
  )}
277
  </AnimatePresence>
 
5
  import type { AnalysisResult } from "@/app/page"
6
  import { VoiceRecorder } from "./voice-recorder"
7
  import { apiGet, apiPost } from "@/lib/api"
8
+ import { ReviewHighlight } from "./review-mode"
9
 
10
  const WELCOME = "مرحباً! أنا مساعدك الطبي. كيف يمكنني مساعدتك اليوم؟"
11
 
 
163
  style={{ border: "1px solid var(--border)" }}
164
  >
165
  {/* Header */}
166
+ <ReviewHighlight changeId={2} className="px-5 py-4 flex items-center gap-3 border-b border-border/50">
167
  <div className="w-9 h-9 rounded-full flex items-center justify-center bg-primary/15">
168
  <svg className="w-4 h-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
169
  <path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z" />
 
173
  <span className="text-sm font-semibold text-foreground">المساعد الذكي</span>
174
  <p className="text-[10px] text-muted-foreground">مساعد طبي ذكي</p>
175
  </div>
176
+ </ReviewHighlight>
177
 
178
  {/* Messages */}
179
+ <ReviewHighlight changeId={1} className="h-72 overflow-y-auto px-4 py-3 space-y-3 scroll-smooth overscroll-contain">
 
180
  <AnimatePresence mode="popLayout">
181
  {messages.map((msg, i) => (
182
  <motion.div
 
212
  )}
213
  </AnimatePresence>
214
  <div ref={messagesEndRef} />
215
+ </ReviewHighlight>
216
 
217
  {/* FAQ quick questions */}
218
  <div className="px-4 pb-2">
 
242
  </div>
243
 
244
  {/* Input */}
245
+ <ReviewHighlight changeId={3} className="px-4 pb-4 pt-2">
246
  <div className="flex items-center gap-2 rounded-full px-1 py-1 bg-input border border-border/60">
247
  <VoiceRecorder
248
  onTranscription={(text) => sendMessage(text)}
 
271
  </svg>
272
  </motion.button>
273
  </div>
274
+ </ReviewHighlight>
275
  </motion.div>
276
  )}
277
  </AnimatePresence>
frontend/components/compare-analyses.tsx CHANGED
@@ -3,6 +3,7 @@
3
  import { useState, useEffect } from "react"
4
  import { motion, AnimatePresence } from "framer-motion"
5
  import type { AnalysisResult } from "@/app/page"
 
6
 
7
  interface SavedAnalysis {
8
  id: string
@@ -277,7 +278,7 @@ function CompareContent({
277
  </div>
278
 
279
  {/* ── Comparison Rows ── */}
280
- <div className="px-6 pb-5 space-y-1.5">
281
  {rows.map((row, i) => {
282
  const cfgA = row.a ? getStatusCfg(row.a.status) : null
283
  const cfgB = row.b ? getStatusCfg(row.b.status) : null
@@ -365,7 +366,7 @@ function CompareContent({
365
  </motion.div>
366
  )
367
  })}
368
- </div>
369
 
370
  {/* ── Groq AI Summary ── */}
371
  <div className="px-6 pb-2">
 
3
  import { useState, useEffect } from "react"
4
  import { motion, AnimatePresence } from "framer-motion"
5
  import type { AnalysisResult } from "@/app/page"
6
+ import { ReviewHighlight } from "./review-mode"
7
 
8
  interface SavedAnalysis {
9
  id: string
 
278
  </div>
279
 
280
  {/* ── Comparison Rows ── */}
281
+ <ReviewHighlight changeId={7} className="px-6 pb-5 space-y-1.5">
282
  {rows.map((row, i) => {
283
  const cfgA = row.a ? getStatusCfg(row.a.status) : null
284
  const cfgB = row.b ? getStatusCfg(row.b.status) : null
 
366
  </motion.div>
367
  )
368
  })}
369
+ </ReviewHighlight>
370
 
371
  {/* ── Groq AI Summary ── */}
372
  <div className="px-6 pb-2">
frontend/components/review-mode.tsx ADDED
@@ -0,0 +1,505 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import React, {
4
+ createContext, useContext, useState, useRef, useEffect, useCallback,
5
+ } from "react"
6
+ import { motion, AnimatePresence } from "framer-motion"
7
+
8
+ // ══════════════════════════════════════════════════════════════
9
+ // DATA
10
+ // ══════════════════════════════════════════════════════════════
11
+
12
+ type Category = "fix" | "improvement" | "responsive"
13
+
14
+ interface Change {
15
+ id: number
16
+ component: string
17
+ area: string
18
+ title: string
19
+ problem: string
20
+ before: string[]
21
+ after: string[]
22
+ uxImpact: string
23
+ category: Category
24
+ }
25
+
26
+ const CAT: Record<Category, { badge: string; ring: string; label: string }> = {
27
+ fix: { badge: "bg-destructive", ring: "ring-destructive/60", label: "إصلاح" },
28
+ improvement: { badge: "bg-primary", ring: "ring-primary/60", label: "تحسين" },
29
+ responsive: { badge: "bg-[oklch(0.62_0.18_280)]", ring: "ring-[oklch(0.62_0.18_280)]/60", label: "استجابة" },
30
+ }
31
+
32
+ export const CHANGES: Change[] = [
33
+ {
34
+ id: 1,
35
+ component: "ChatBot",
36
+ area: "منطقة الرسائل",
37
+ title: "توسيع مساحة المحادثة",
38
+ problem: "منطقة الرسائل h-56 (224px) تعرض 5-6 رسائل فقط وتجبر على التمرير المستمر",
39
+ before: ["📦 ارتفاع: h-56 = 224px", "يظهر 5-6 رسائل", "تمرير مستمر ومرهق"],
40
+ after: ["📦 ارتفاع: h-72 = 288px", "يظهر 8-9 رسائل", "محادثة أطول بلا تمرير"],
41
+ uxImpact: "المستخدم يقرأ المحادثة كاملة دون تمرير مستمر — تجربة أكثر سلاسة",
42
+ category: "improvement",
43
+ },
44
+ {
45
+ id: 2,
46
+ component: "ChatBot",
47
+ area: "عرض اللوحة والزر",
48
+ title: "توسيع لوحة المحادثة",
49
+ problem: "عرض اللوحة 320px كان يضغط النصوص العربية الطويلة على سطرين دون داعٍ",
50
+ before: ["عرض: w-[320px]", "النص العربي مضغوط", "max-w ضيق على الشاشات الصغيرة"],
51
+ after: ["عرض: w-[340px]", "تنفس أفضل للنصوص", "max-w أوسع على الجوال"],
52
+ uxImpact: "قراءة أراح للغة العربية ذات الأحرف الطويلة والمتصلة",
53
+ category: "improvement",
54
+ },
55
+ {
56
+ id: 3,
57
+ component: "ChatBot",
58
+ area: "حقل الإدخال",
59
+ title: "إضافة Focus Ring للإدخال",
60
+ problem: "النقر على حقل الكتابة لم يُظهر أي مؤشر بصري — إشكالية Accessibility وضعف UX",
61
+ before: ["لا يوجد حد عند التركيز", "المستخدم لا يعرف: هل الحقل نشط؟", "مشكلة في معايير a11y"],
62
+ after: ["ring-1 ring-primary/40 يظهر عند التركيز", "مؤشر بصري واضح وأنيق", "متوافق مع WCAG"],
63
+ uxImpact: "المستخدم يعرف دائماً أين يكتب — مهم خصوصاً في الوضع النهاري الفاتح",
64
+ category: "fix",
65
+ },
66
+ {
67
+ id: 4,
68
+ component: "Upload Section",
69
+ area: "حالة الخطأ",
70
+ title: "زر إعادة المحاولة عند فشل الرفع",
71
+ problem: "عند فشل رفع الملف كان يظهر نص الخطأ فقط — المستخدم مجبور على تحديث الصفحة",
72
+ before: ["❌ نص الخطأ فقط", "لا خيار للمتابعة", "تجربة محبطة ومقطوعة"],
73
+ after: ["❌ نص الخطأ واضح", "🔄 زر 'إعادة المحاولة'", "الملف يُعاد رفعه بنقرة واحدة"],
74
+ uxImpact: "تحويل لحظة الإحباط إلى تجربة قابلة للتعافي — الحفاظ على جلسة المستخدم",
75
+ category: "fix",
76
+ },
77
+ {
78
+ id: 5,
79
+ component: "Risk Dashboard",
80
+ area: "حالة الخطأ",
81
+ title: "زر إعادة المحاولة في تقرير المخاطر",
82
+ problem: "فشل تحميل تقرير المخاطر لم يمنح المستخدم خياراً للمحاولة مجدداً",
83
+ before: ["❌ رسالة خطأ في مربع أحمر", "لا إمكانية إعادة التحميل", "المستخدم عالق"],
84
+ after: ["❌ رسالة الخطأ", "🔄 زر 'إعادة المحاولة' بنفس المربع", "إعادة اتصال فورية بالخادم"],
85
+ uxImpact: "تجربة مقاومة للأخطاء — المستخدم لا يشعر بعجز أو حاجة لتحديث الصفحة",
86
+ category: "fix",
87
+ },
88
+ {
89
+ id: 6,
90
+ component: "Voice Recorder",
91
+ area: "ألوان الأزرار",
92
+ title: "استبدال الألوان الثابتة بمتغيرات CSS",
93
+ problem: "red-500 و blue-500 ثابتة تتعارض مع نظام الألوان في الوضع الداكن والفاتح",
94
+ before: ["bg-red-500 (ثابت، لا يتكيف)", "text-blue-500 (ثابت، يبدو غريباً)", "Dark Mode: يبدو منفصلاً"],
95
+ after: ["bg-destructive (CSS token)", "text-primary (CSS token)", "Dark/Light: تناسق كامل"],
96
+ uxImpact: "تناسق بصري تام في كلا الوضعين — لا ألوان طافية تبدو خارج النظام",
97
+ category: "improvement",
98
+ },
99
+ {
100
+ id: 7,
101
+ component: "Compare Analyses",
102
+ area: "جدول المقارنة",
103
+ title: "تحسين الـ Grid للجوال",
104
+ problem: "الجدول يستخدم 160px×2 ثابتة — على جوال 360px يتجاوز العرض ويحدث تمرير أفقي",
105
+ before: ["grid-cols-[1fr_160px_160px]", "الأعمدة: 320px ثابتة", "↔ تمرير أفقي على الجوال"],
106
+ after: ["sm: [1fr_160px_160px]", "mobile: [1fr_100px_100px]", "لا تمرير أفقي على 360px+"],
107
+ uxImpact: "المستخدم على الجوال يرى المقارنة كاملة بلا تمرير أفقي محبط",
108
+ category: "responsive",
109
+ },
110
+ {
111
+ id: 8,
112
+ component: "Results Page",
113
+ area: "زر تصدير PDF",
114
+ title: "تفعيل زر الطباعة / PDF",
115
+ problem: "زر 'تصدير PDF' كان موجوداً بصرياً لكن onClick لم يكن مربوطاً — ينقر المستخدم ولا شيء يحدث",
116
+ before: ["زر PDF بتصميم جميل", "onClick = undefined (معطل)", "ميزة وعد بها ولم تُنجز"],
117
+ after: ["onClick={handlePrint}", "window.print() بعد 300ms delay", "نافذة طباعة المتصفح تفتح فوراً"],
118
+ uxImpact: "ميزة كانت ميتة أصبحت تعمل — المستخدم يحفظ تقريره كـ PDF بنقرة واحدة",
119
+ category: "fix",
120
+ },
121
+ ]
122
+
123
+ // ══════════════════════════════════════════════════════════════
124
+ // CONTEXT
125
+ // ══════════════════════════════════════════════════════════════
126
+
127
+ interface ReviewCtx {
128
+ active: boolean
129
+ currentId: number | null
130
+ toggle: () => void
131
+ select: (id: number | null) => void
132
+ }
133
+
134
+ const ReviewContext = createContext<ReviewCtx>({
135
+ active: false, currentId: null, toggle: () => {}, select: () => {},
136
+ })
137
+
138
+ export function useReviewMode() { return useContext(ReviewContext) }
139
+
140
+ // ══════════════════════════════════════════════════════════════
141
+ // PROVIDER
142
+ // ══════════════════════════════════════════════════════════════
143
+
144
+ export function ReviewModeProvider({ children }: { children: React.ReactNode }) {
145
+ const [active, setActiveState] = useState(false)
146
+ const [currentId, setCurrentId] = useState<number | null>(null)
147
+
148
+ const toggle = useCallback(() => {
149
+ setActiveState(v => !v)
150
+ setCurrentId(null)
151
+ }, [])
152
+
153
+ const select = useCallback((id: number | null) => setCurrentId(id), [])
154
+
155
+ // Keyboard shortcuts
156
+ useEffect(() => {
157
+ function onKey(e: KeyboardEvent) {
158
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "R") {
159
+ e.preventDefault()
160
+ setActiveState(v => !v)
161
+ setCurrentId(null)
162
+ }
163
+ if (!active) return
164
+ if (e.key === "Escape") { setActiveState(false); setCurrentId(null) }
165
+ if (e.key === "ArrowLeft") {
166
+ setCurrentId(prev => {
167
+ const idx = prev ? CHANGES.findIndex(c => c.id === prev) : -1
168
+ return idx < CHANGES.length - 1 ? CHANGES[idx + 1].id : prev
169
+ })
170
+ }
171
+ if (e.key === "ArrowRight") {
172
+ setCurrentId(prev => {
173
+ const idx = prev ? CHANGES.findIndex(c => c.id === prev) : CHANGES.length
174
+ return idx > 0 ? CHANGES[idx - 1].id : prev
175
+ })
176
+ }
177
+ }
178
+ window.addEventListener("keydown", onKey)
179
+ return () => window.removeEventListener("keydown", onKey)
180
+ }, [active])
181
+
182
+ return (
183
+ <ReviewContext.Provider value={{ active, currentId, toggle, select }}>
184
+ {children}
185
+ <ReviewFloatingButton />
186
+ <ReviewPanel />
187
+ </ReviewContext.Provider>
188
+ )
189
+ }
190
+
191
+ // ══════════════════════════════════════════════════════════════
192
+ // FLOATING TOGGLE BUTTON
193
+ // ══════════════════════════════════════════════════════════════
194
+
195
+ function ReviewFloatingButton() {
196
+ const { active, toggle } = useReviewMode()
197
+ return (
198
+ <motion.button
199
+ initial={{ opacity: 0, y: -10 }}
200
+ animate={{ opacity: 1, y: 0 }}
201
+ transition={{ delay: 2 }}
202
+ onClick={toggle}
203
+ className={[
204
+ "fixed top-20 left-4 z-[200] flex items-center gap-2 px-3.5 py-2 rounded-full",
205
+ "text-xs font-bold shadow-lg border transition-all duration-300 select-none",
206
+ active
207
+ ? "bg-foreground text-background border-foreground/20"
208
+ : "bg-card text-muted-foreground border-border hover:text-foreground hover:border-primary/40",
209
+ ].join(" ")}
210
+ title="Ctrl+Shift+R"
211
+ >
212
+ <span className="text-sm">{active ? "✕" : "🔍"}</span>
213
+ <span>{active ? "إيقاف المراجعة" : "وضع المراجعة"}</span>
214
+ {!active && (
215
+ <span className="w-5 h-5 rounded-full bg-primary/15 text-primary text-[10px] font-black flex items-center justify-center">
216
+ {CHANGES.length}
217
+ </span>
218
+ )}
219
+ {active && (
220
+ <motion.span
221
+ animate={{ scale: [1, 1.2, 1] }}
222
+ transition={{ duration: 2, repeat: Infinity }}
223
+ className="w-2 h-2 rounded-full bg-primary"
224
+ />
225
+ )}
226
+ </motion.button>
227
+ )
228
+ }
229
+
230
+ // ══════════════════════════════════════════════════════════════
231
+ // SIDE PANEL
232
+ // ══════════════════════════════════════════════════════════════
233
+
234
+ function ReviewPanel() {
235
+ const { active, currentId, select } = useReviewMode()
236
+
237
+ const idx = currentId ? CHANGES.findIndex(c => c.id === currentId) : -1
238
+
239
+ const navigate = (dir: 1 | -1) => {
240
+ const next = idx === -1 ? 0 : Math.max(0, Math.min(CHANGES.length - 1, idx + dir))
241
+ select(CHANGES[next].id)
242
+ }
243
+
244
+ return (
245
+ <AnimatePresence>
246
+ {active && (
247
+ <>
248
+ {/* Backdrop (mobile only) */}
249
+ <motion.div
250
+ initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
251
+ className="fixed inset-0 z-[150] bg-black/20 sm:hidden"
252
+ onClick={() => select(null)}
253
+ />
254
+
255
+ {/* Panel */}
256
+ <motion.aside
257
+ initial={{ x: "-100%" }}
258
+ animate={{ x: 0 }}
259
+ exit={{ x: "-100%" }}
260
+ transition={{ type: "spring", damping: 30, stiffness: 300 }}
261
+ className="fixed top-0 left-0 h-full z-[190] w-80 flex flex-col bg-card border-r border-border shadow-2xl"
262
+ >
263
+ {/* Header */}
264
+ <div className="px-5 pt-5 pb-4 border-b border-border shrink-0">
265
+ <div className="flex items-center gap-3 mb-1">
266
+ <span className="text-lg">🔍</span>
267
+ <div>
268
+ <h2 className="text-sm font-bold text-foreground">وضع المراجعة المرئية</h2>
269
+ <p className="text-[10px] text-muted-foreground">Design Audit Mode</p>
270
+ </div>
271
+ </div>
272
+
273
+ {/* Progress bar */}
274
+ <div className="mt-3">
275
+ <div className="flex justify-between text-[10px] text-muted-foreground mb-1.5">
276
+ <span>{CHANGES.length} تحسين تم تطبيقه</span>
277
+ {currentId && <span>{idx + 1} / {CHANGES.length}</span>}
278
+ </div>
279
+ <div className="h-1.5 rounded-full bg-muted overflow-hidden">
280
+ <motion.div
281
+ className="h-full rounded-full bg-primary"
282
+ animate={{ width: currentId ? `${((idx + 1) / CHANGES.length) * 100}%` : "0%" }}
283
+ transition={{ duration: 0.3 }}
284
+ />
285
+ </div>
286
+ </div>
287
+
288
+ {/* Legend */}
289
+ <div className="flex gap-3 mt-3">
290
+ {(Object.entries(CAT) as [Category, typeof CAT[Category]][]).map(([key, cfg]) => (
291
+ <div key={key} className="flex items-center gap-1.5">
292
+ <div className={`w-2 h-2 rounded-full ${cfg.badge}`} />
293
+ <span className="text-[10px] text-muted-foreground">{cfg.label}</span>
294
+ </div>
295
+ ))}
296
+ </div>
297
+ </div>
298
+
299
+ {/* Changes list */}
300
+ <div className="flex-1 overflow-y-auto py-3 px-3 space-y-2" style={{ scrollbarWidth: "thin" }}>
301
+ {CHANGES.map((change, i) => {
302
+ const cfg = CAT[change.category]
303
+ const isActive = currentId === change.id
304
+ return (
305
+ <motion.div
306
+ key={change.id}
307
+ initial={{ opacity: 0, x: -10 }}
308
+ animate={{ opacity: 1, x: 0 }}
309
+ transition={{ delay: i * 0.04 }}
310
+ >
311
+ <button
312
+ onClick={() => select(isActive ? null : change.id)}
313
+ className={[
314
+ "w-full text-right rounded-2xl border transition-all duration-200",
315
+ isActive
316
+ ? "bg-muted border-primary/30 shadow-sm"
317
+ : "bg-transparent border-border hover:bg-muted/50",
318
+ ].join(" ")}
319
+ >
320
+ <div className="flex items-start gap-2.5 p-3">
321
+ <div className={`w-6 h-6 rounded-full ${cfg.badge} text-white text-[11px] font-black flex items-center justify-center shrink-0 mt-0.5`}>
322
+ {change.id}
323
+ </div>
324
+ <div className="flex-1 min-w-0 text-right">
325
+ <div className="flex items-center gap-1.5 mb-0.5 flex-wrap">
326
+ <span className="text-[9px] font-bold text-muted-foreground">{change.component}</span>
327
+ <span className={`text-[9px] font-bold px-1.5 py-0.5 rounded-full text-white ${cfg.badge}`}>
328
+ {cfg.label}
329
+ </span>
330
+ </div>
331
+ <p className="text-[12px] font-semibold text-foreground leading-snug">{change.title}</p>
332
+ <p className="text-[10px] text-muted-foreground mt-0.5">{change.area}</p>
333
+ </div>
334
+ </div>
335
+
336
+ {/* Expanded details */}
337
+ <AnimatePresence initial={false}>
338
+ {isActive && (
339
+ <motion.div
340
+ initial={{ height: 0, opacity: 0 }}
341
+ animate={{ height: "auto", opacity: 1 }}
342
+ exit={{ height: 0, opacity: 0 }}
343
+ transition={{ duration: 0.22 }}
344
+ className="overflow-hidden"
345
+ onClick={e => e.stopPropagation()}
346
+ >
347
+ <div className="px-3 pb-3 space-y-2.5 border-t border-border/60 pt-3">
348
+ {/* Problem */}
349
+ <div className="rounded-xl bg-destructive/8 border border-destructive/15 p-2.5">
350
+ <p className="text-[9px] font-black text-destructive uppercase tracking-wider mb-1">المشكلة السابقة</p>
351
+ <p className="text-[10px] text-foreground/80 leading-relaxed">{change.problem}</p>
352
+ </div>
353
+
354
+ {/* Before / After */}
355
+ <div className="grid grid-cols-2 gap-2">
356
+ <div className="rounded-xl bg-muted border border-border p-2">
357
+ <p className="text-[9px] font-black text-muted-foreground uppercase mb-1.5">قبل ❌</p>
358
+ {change.before.map((line, j) => (
359
+ <p key={j} className="text-[10px] text-foreground/60 leading-relaxed font-mono">{line}</p>
360
+ ))}
361
+ </div>
362
+ <div className="rounded-xl bg-success/5 border border-success/20 p-2">
363
+ <p className="text-[9px] font-black text-success uppercase mb-1.5">بعد ✅</p>
364
+ {change.after.map((line, j) => (
365
+ <p key={j} className="text-[10px] text-foreground/80 leading-relaxed font-mono">{line}</p>
366
+ ))}
367
+ </div>
368
+ </div>
369
+
370
+ {/* UX Impact */}
371
+ <div className="rounded-xl bg-primary/5 border border-primary/20 p-2.5">
372
+ <p className="text-[9px] font-black text-primary uppercase tracking-wider mb-1">تأثير UX ✨</p>
373
+ <p className="text-[10px] text-foreground/80 leading-relaxed">{change.uxImpact}</p>
374
+ </div>
375
+ </div>
376
+ </motion.div>
377
+ )}
378
+ </AnimatePresence>
379
+ </button>
380
+ </motion.div>
381
+ )
382
+ })}
383
+ </div>
384
+
385
+ {/* Navigation footer */}
386
+ <div className="px-4 py-3.5 border-t border-border shrink-0">
387
+ <div className="flex items-center gap-2">
388
+ <button
389
+ onClick={() => navigate(-1)}
390
+ disabled={idx <= 0 && currentId !== null}
391
+ className="flex-1 py-2 rounded-xl bg-muted text-xs font-bold text-foreground disabled:opacity-30 hover:bg-muted/70 transition-colors"
392
+ >
393
+ → السابق
394
+ </button>
395
+ <button
396
+ onClick={() => currentId === null ? select(CHANGES[0].id) : navigate(1)}
397
+ disabled={idx >= CHANGES.length - 1}
398
+ className="flex-1 py-2 rounded-xl bg-primary text-primary-foreground text-xs font-bold disabled:opacity-30 hover:bg-primary/90 transition-colors"
399
+ >
400
+ {currentId === null ? "ابدأ الجولة ◀" : "التالي ←"}
401
+ </button>
402
+ </div>
403
+ <p className="text-center text-[9px] text-muted-foreground mt-2">Ctrl+Shift+R · ← → للتنقل · Esc للإغلاق</p>
404
+ </div>
405
+ </motion.aside>
406
+ </>
407
+ )}
408
+ </AnimatePresence>
409
+ )
410
+ }
411
+
412
+ // ══════════════════════════════════════════════════════════════
413
+ // HIGHLIGHT WRAPPER
414
+ // ══════════════════════════════════════════════════════════════
415
+
416
+ interface ReviewHighlightProps {
417
+ changeId: number
418
+ children: React.ReactNode
419
+ className?: string
420
+ style?: React.CSSProperties
421
+ }
422
+
423
+ export function ReviewHighlight({ changeId, children, className = "", style }: ReviewHighlightProps) {
424
+ const { active, currentId, select } = useReviewMode()
425
+ const ref = useRef<HTMLDivElement>(null)
426
+
427
+ const change = CHANGES.find(c => c.id === changeId)
428
+ const isActive = active && currentId === changeId
429
+ const isVisible = active
430
+
431
+ // Scroll into view when activated
432
+ useEffect(() => {
433
+ if (isActive && ref.current) {
434
+ ref.current.scrollIntoView({ behavior: "smooth", block: "center" })
435
+ }
436
+ }, [isActive])
437
+
438
+ if (!change || !isVisible) return <div className={className} style={style}>{children}</div>
439
+
440
+ const cfg = CAT[change.category]
441
+
442
+ return (
443
+ <div ref={ref} className={`relative ${className}`} style={{ ...style, scrollMarginTop: "120px" }}>
444
+ {/* Animated ring */}
445
+ <div
446
+ className={[
447
+ "absolute inset-0 rounded-[inherit] ring-2 pointer-events-none z-20 transition-all duration-300",
448
+ cfg.ring,
449
+ isActive ? "ring-offset-2" : "opacity-50",
450
+ ].join(" ")}
451
+ style={{ borderRadius: "inherit" }}
452
+ />
453
+
454
+ {/* Animated glow when active */}
455
+ {isActive && (
456
+ <motion.div
457
+ initial={{ opacity: 0 }}
458
+ animate={{ opacity: 1 }}
459
+ className="absolute inset-0 rounded-[inherit] pointer-events-none z-20"
460
+ style={{
461
+ background: change.category === "fix"
462
+ ? "oklch(0.62 0.21 25 / 0.06)"
463
+ : change.category === "responsive"
464
+ ? "oklch(0.62 0.18 280 / 0.06)"
465
+ : "oklch(0.72 0.11 168 / 0.06)",
466
+ }}
467
+ />
468
+ )}
469
+
470
+ {/* Number badge — positioned INSIDE so it works inside overflow-hidden containers */}
471
+ <motion.button
472
+ whileHover={{ scale: 1.15 }}
473
+ whileTap={{ scale: 0.9 }}
474
+ onClick={e => { e.stopPropagation(); select(currentId === changeId ? null : changeId) }}
475
+ animate={{ scale: isActive ? 1.2 : 1 }}
476
+ transition={{ type: "spring", stiffness: 400, damping: 20 }}
477
+ className={[
478
+ "absolute top-1.5 right-1.5 z-30 w-6 h-6 rounded-full text-white text-[11px] font-black",
479
+ "flex items-center justify-center shadow-lg cursor-pointer",
480
+ cfg.badge,
481
+ ].join(" ")}
482
+ >
483
+ {changeId}
484
+ </motion.button>
485
+
486
+ {/* Tooltip label when active — below the badge, inside the element */}
487
+ <AnimatePresence>
488
+ {isActive && (
489
+ <motion.div
490
+ initial={{ opacity: 0, y: -4, scale: 0.9 }}
491
+ animate={{ opacity: 1, y: 0, scale: 1 }}
492
+ exit={{ opacity: 0, y: -4, scale: 0.9 }}
493
+ transition={{ duration: 0.15 }}
494
+ className="absolute top-9 right-1.5 z-40 px-3 py-1.5 rounded-xl text-[11px] font-bold text-white shadow-xl pointer-events-none max-w-[180px]"
495
+ style={{ background: "oklch(0.12 0.01 210 / 0.95)" }}
496
+ >
497
+ ✦ {change.title}
498
+ </motion.div>
499
+ )}
500
+ </AnimatePresence>
501
+
502
+ {children}
503
+ </div>
504
+ )
505
+ }
frontend/components/risk-dashboard.tsx CHANGED
@@ -7,6 +7,7 @@ import {
7
  ResponsiveContainer, Tooltip,
8
  } from "recharts"
9
  import type { AnalysisResult } from "@/app/page"
 
10
 
11
  const BACKEND = process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:8000"
12
 
@@ -240,7 +241,7 @@ export function RiskDashboard({ analysis }: RiskDashboardProps) {
240
  )}
241
 
242
  {error && (
243
- <div className="flex flex-col items-center gap-3 rounded-xl border border-destructive/30 bg-destructive/10 px-4 py-3">
244
  <p className="text-sm text-destructive">{error}</p>
245
  <button
246
  onClick={() => {
@@ -260,7 +261,7 @@ export function RiskDashboard({ analysis }: RiskDashboardProps) {
260
  >
261
  إعادة المحاولة
262
  </button>
263
- </div>
264
  )}
265
 
266
  {/* Risk cards (sorted by score desc) */}
 
7
  ResponsiveContainer, Tooltip,
8
  } from "recharts"
9
  import type { AnalysisResult } from "@/app/page"
10
+ import { ReviewHighlight } from "./review-mode"
11
 
12
  const BACKEND = process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:8000"
13
 
 
241
  )}
242
 
243
  {error && (
244
+ <ReviewHighlight changeId={5} className="flex flex-col items-center gap-3 rounded-xl border border-destructive/30 bg-destructive/10 px-4 py-3">
245
  <p className="text-sm text-destructive">{error}</p>
246
  <button
247
  onClick={() => {
 
261
  >
262
  إعادة المحاولة
263
  </button>
264
+ </ReviewHighlight>
265
  )}
266
 
267
  {/* Risk cards (sorted by score desc) */}
frontend/components/upload-section.tsx CHANGED
@@ -3,6 +3,7 @@
3
  import { useState, useCallback } from "react"
4
  import { motion, AnimatePresence } from "framer-motion"
5
  import type { AnalysisResult } from "@/app/page"
 
6
 
7
  interface UploadSectionProps {
8
  onResult: (result: AnalysisResult) => void
@@ -230,7 +231,7 @@ export function UploadSection({ onResult }: UploadSectionProps) {
230
  )}
231
 
232
  {error && !isAnalyzing && (
233
- <div className="flex flex-col items-center gap-3">
234
  <div className="flex items-center gap-2 text-destructive">
235
  <svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
236
  <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
@@ -243,7 +244,7 @@ export function UploadSection({ onResult }: UploadSectionProps) {
243
  >
244
  إعادة المحاولة
245
  </button>
246
- </div>
247
  )}
248
  </motion.div>
249
  )}
 
3
  import { useState, useCallback } from "react"
4
  import { motion, AnimatePresence } from "framer-motion"
5
  import type { AnalysisResult } from "@/app/page"
6
+ import { ReviewHighlight } from "./review-mode"
7
 
8
  interface UploadSectionProps {
9
  onResult: (result: AnalysisResult) => void
 
231
  )}
232
 
233
  {error && !isAnalyzing && (
234
+ <ReviewHighlight changeId={4} className="flex flex-col items-center gap-3">
235
  <div className="flex items-center gap-2 text-destructive">
236
  <svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
237
  <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
 
244
  >
245
  إعادة المحاولة
246
  </button>
247
+ </ReviewHighlight>
248
  )}
249
  </motion.div>
250
  )}
frontend/components/voice-recorder.tsx CHANGED
@@ -3,6 +3,7 @@
3
  import { useState, useRef, useCallback, useEffect } from "react"
4
  import { motion, AnimatePresence } from "framer-motion"
5
  import { Mic, MicOff, Loader2, Volume2 } from "lucide-react"
 
6
 
7
  const BACKEND = process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:8000"
8
 
@@ -128,7 +129,7 @@ export function VoiceRecorder({ onTranscription, ttsText, disabled = false }: Vo
128
  const isProcessing = state === "processing"
129
 
130
  return (
131
- <div className="flex items-center gap-2">
132
  {/* Mic button */}
133
  <motion.button
134
  type="button"
@@ -204,6 +205,6 @@ export function VoiceRecorder({ onTranscription, ttsText, disabled = false }: Vo
204
  </motion.span>
205
  )}
206
  </AnimatePresence>
207
- </div>
208
  )
209
  }
 
3
  import { useState, useRef, useCallback, useEffect } from "react"
4
  import { motion, AnimatePresence } from "framer-motion"
5
  import { Mic, MicOff, Loader2, Volume2 } from "lucide-react"
6
+ import { ReviewHighlight } from "./review-mode"
7
 
8
  const BACKEND = process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:8000"
9
 
 
129
  const isProcessing = state === "processing"
130
 
131
  return (
132
+ <ReviewHighlight changeId={6} className="flex items-center gap-2">
133
  {/* Mic button */}
134
  <motion.button
135
  type="button"
 
205
  </motion.span>
206
  )}
207
  </AnimatePresence>
208
+ </ReviewHighlight>
209
  )
210
  }