رغد commited on
Commit
e2b07db
·
1 Parent(s): b94eb70

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

Browse files

This reverts commit b94eb70e91e28b52917679101a2284362ba68b0d.

frontend/app/layout.tsx CHANGED
@@ -2,7 +2,6 @@ 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 { ReviewModeProvider } from '@/components/review-mode'
6
  import './globals.css'
7
 
8
  const cairo = Cairo({
@@ -46,9 +45,7 @@ export default function RootLayout({
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>
 
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
  <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>
frontend/app/page.tsx CHANGED
@@ -14,7 +14,6 @@ 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
- import { ReviewHighlight } from "@/components/review-mode"
18
 
19
  const CompareAnalyses = dynamic(() =>
20
  import("@/components/compare-analyses").then((m) => m.CompareAnalyses), { ssr: false }
@@ -180,7 +179,7 @@ function ImprovedResults({ result }: { result: AnalysisResult | null }) {
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,7 +192,7 @@ function ImprovedResults({ result }: { result: AnalysisResult | null }) {
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>
 
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
  <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
  <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>
frontend/components/chat-bot.tsx CHANGED
@@ -5,7 +5,6 @@ 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
- import { ReviewHighlight } from "./review-mode"
9
 
10
  const WELCOME = "مرحباً! أنا مساعدك الطبي. كيف يمكنني مساعدتك اليوم؟"
11
 
@@ -163,7 +162,7 @@ export function ChatBot({ analysisResult, sessionId = "anonymous" }: ChatBotProp
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,10 +172,11 @@ export function ChatBot({ analysisResult, sessionId = "anonymous" }: ChatBotProp
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,7 +212,7 @@ export function ChatBot({ analysisResult, sessionId = "anonymous" }: ChatBotProp
212
  )}
213
  </AnimatePresence>
214
  <div ref={messagesEndRef} />
215
- </ReviewHighlight>
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
- <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,7 +271,7 @@ export function ChatBot({ analysisResult, sessionId = "anonymous" }: ChatBotProp
271
  </svg>
272
  </motion.button>
273
  </div>
274
- </ReviewHighlight>
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
 
9
  const WELCOME = "مرحباً! أنا مساعدك الطبي. كيف يمكنني مساعدتك اليوم؟"
10
 
 
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
  <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
  )}
213
  </AnimatePresence>
214
  <div ref={messagesEndRef} />
215
+ </div>
216
 
217
  {/* FAQ quick questions */}
218
  <div className="px-4 pb-2">
 
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
  </svg>
272
  </motion.button>
273
  </div>
274
+ </div>
275
  </motion.div>
276
  )}
277
  </AnimatePresence>
frontend/components/compare-analyses.tsx CHANGED
@@ -3,7 +3,6 @@
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,7 +277,7 @@ function CompareContent({
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,7 +365,7 @@ function CompareContent({
366
  </motion.div>
367
  )
368
  })}
369
- </ReviewHighlight>
370
 
371
  {/* ── Groq AI Summary ── */}
372
  <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
 
7
  interface SavedAnalysis {
8
  id: string
 
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
  </motion.div>
366
  )
367
  })}
368
+ </div>
369
 
370
  {/* ── Groq AI Summary ── */}
371
  <div className="px-6 pb-2">
frontend/components/review-mode.tsx DELETED
@@ -1,505 +0,0 @@
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,7 +7,6 @@ import {
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,7 +240,7 @@ export function RiskDashboard({ analysis }: RiskDashboardProps) {
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,7 +260,7 @@ export function RiskDashboard({ analysis }: RiskDashboardProps) {
261
  >
262
  إعادة المحاولة
263
  </button>
264
- </ReviewHighlight>
265
  )}
266
 
267
  {/* Risk cards (sorted by score desc) */}
 
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
  )}
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
  >
261
  إعادة المحاولة
262
  </button>
263
+ </div>
264
  )}
265
 
266
  {/* Risk cards (sorted by score desc) */}
frontend/components/upload-section.tsx CHANGED
@@ -3,7 +3,6 @@
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,7 +230,7 @@ export function UploadSection({ onResult }: UploadSectionProps) {
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,7 +243,7 @@ export function UploadSection({ onResult }: UploadSectionProps) {
244
  >
245
  إعادة المحاولة
246
  </button>
247
- </ReviewHighlight>
248
  )}
249
  </motion.div>
250
  )}
 
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
  )}
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
  >
244
  إعادة المحاولة
245
  </button>
246
+ </div>
247
  )}
248
  </motion.div>
249
  )}
frontend/components/voice-recorder.tsx CHANGED
@@ -3,7 +3,6 @@
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,7 +128,7 @@ export function VoiceRecorder({ onTranscription, ttsText, disabled = false }: Vo
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,6 +204,6 @@ export function VoiceRecorder({ onTranscription, ttsText, disabled = false }: Vo
205
  </motion.span>
206
  )}
207
  </AnimatePresence>
208
- </ReviewHighlight>
209
  )
210
  }
 
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
  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
  </motion.span>
205
  )}
206
  </AnimatePresence>
207
+ </div>
208
  )
209
  }