youssefreda9 commited on
Commit
b57e9cf
·
1 Parent(s): 7f0ddc6

feat: Professional editor UI redesign - 3-column layout (docs | editor | sidebar) - Documents panel always visible on desktop - Import/export moved to left sidebar - Removed CTA button from nav - Replaced all emojis with SVG icons - Clean toolbar with separator - Mobile: docs stay as overlay sidebar

Browse files
Dockerfile CHANGED
@@ -45,16 +45,6 @@ AutoModelForMaskedLM.from_pretrained('aubmindlab/bert-base-arabertv02'); \
45
  print('Spelling model + MLM cached!'); \
46
  "
47
 
48
- # 3. Grammar model (T5ForConditionalGeneration)
49
- RUN python -c "\
50
- from transformers import AutoTokenizer, AutoModelForSeq2SeqLM; \
51
- print('Downloading Grammar tokenizer...'); \
52
- AutoTokenizer.from_pretrained('bayan10/Bayan_Arabic_Grammar'); \
53
- print('Downloading Grammar T5 model...'); \
54
- AutoModelForSeq2SeqLM.from_pretrained('bayan10/Bayan_Arabic_Grammar'); \
55
- print('Grammar model cached!'); \
56
- "
57
-
58
  # Copy application code
59
  COPY src/ ./src/
60
  COPY .env* ./
 
45
  print('Spelling model + MLM cached!'); \
46
  "
47
 
 
 
 
 
 
 
 
 
 
 
48
  # Copy application code
49
  COPY src/ ./src/
50
  COPY .env* ./
src/app.py CHANGED
@@ -155,7 +155,7 @@ def health_check():
155
  'summarization': summarization_model is not None,
156
  'spelling': _spelling_available(),
157
  'autocomplete': False,
158
- 'grammar': _grammar_available(),
159
  'punctuation': False
160
  },
161
  'note': 'Free tier: summarization local, other models return input unchanged',
@@ -232,15 +232,6 @@ def _spelling_available():
232
  return False
233
 
234
 
235
- def _grammar_available():
236
- """Check if grammar model is loaded (without triggering lazy load)."""
237
- try:
238
- from nlp.grammar.grammar_service import _grammar_model
239
- return _grammar_model is not None
240
- except Exception:
241
- return False
242
-
243
-
244
  @app.route('/api/spelling', methods=['POST'])
245
  def spelling_correction():
246
  """
@@ -452,6 +443,12 @@ def grammar_correction():
452
  "text": "Arabic text to correct"
453
  }
454
  """
 
 
 
 
 
 
455
  try:
456
  if not request.is_json:
457
  return jsonify({'error': 'Request must be JSON', 'status': 'error'}), 400
@@ -463,13 +460,17 @@ def grammar_correction():
463
  return jsonify({'error': 'Text is required', 'status': 'error'}), 400
464
 
465
  logger.info(f"Correcting grammar for text of length: {len(text)}")
466
- from nlp.grammar.grammar_service import correct_grammar
467
- corrected = correct_grammar(text)
 
 
 
468
 
469
  return jsonify({
470
- 'original_text': text,
471
- 'corrected_text': corrected,
472
- 'status': 'success'
 
473
  })
474
 
475
  except Exception as e:
@@ -849,13 +850,15 @@ def analyze_text():
849
  logger.error(f"[ANALYZE] Spelling failed: {e}")
850
 
851
  # 2. Grammar (runs on spelling-corrected text)
852
- has_grammar = True # Always available via lazy-loaded grammar_service
853
  if has_grammar:
854
  try:
855
  t0 = time.time()
856
  logger.info(f"[ANALYZE] Step 2: Grammar correction starting...")
857
- from nlp.grammar.grammar_service import correct_grammar
858
- corrected_grammar = correct_grammar(current_text)
 
 
859
  logger.info(f"[ANALYZE] Step 2: Grammar done in {time.time()-t0:.2f}s")
860
  if corrected_grammar != current_text:
861
  diffs = get_word_diffs(current_text, corrected_grammar)
 
155
  'summarization': summarization_model is not None,
156
  'spelling': _spelling_available(),
157
  'autocomplete': False,
158
+ 'grammar': False,
159
  'punctuation': False
160
  },
161
  'note': 'Free tier: summarization local, other models return input unchanged',
 
232
  return False
233
 
234
 
 
 
 
 
 
 
 
 
 
235
  @app.route('/api/spelling', methods=['POST'])
236
  def spelling_correction():
237
  """
 
443
  "text": "Arabic text to correct"
444
  }
445
  """
446
+ if not USE_HF_API and grammar_model is None:
447
+ return jsonify({
448
+ 'error': 'Grammar model not loaded. Please check server logs.',
449
+ 'status': 'error'
450
+ }), 503
451
+
452
  try:
453
  if not request.is_json:
454
  return jsonify({'error': 'Request must be JSON', 'status': 'error'}), 400
 
460
  return jsonify({'error': 'Text is required', 'status': 'error'}), 400
461
 
462
  logger.info(f"Correcting grammar for text of length: {len(text)}")
463
+ if USE_HF_API:
464
+ # Grammar uses spelling model as proxy (no dedicated grammar model yet)
465
+ corrected = hf_correct_spelling(text)
466
+ else:
467
+ corrected = grammar_model.correct(text)
468
 
469
  return jsonify({
470
+ 'corrected': corrected,
471
+ 'status': 'success',
472
+ 'original_length': len(text),
473
+ 'corrected_length': len(corrected)
474
  })
475
 
476
  except Exception as e:
 
850
  logger.error(f"[ANALYZE] Spelling failed: {e}")
851
 
852
  # 2. Grammar (runs on spelling-corrected text)
853
+ has_grammar = USE_HF_API or grammar_model
854
  if has_grammar:
855
  try:
856
  t0 = time.time()
857
  logger.info(f"[ANALYZE] Step 2: Grammar correction starting...")
858
+ if USE_HF_API:
859
+ corrected_grammar = hf_correct_spelling(current_text)
860
+ else:
861
+ corrected_grammar = grammar_model.correct(current_text)
862
  logger.info(f"[ANALYZE] Step 2: Grammar done in {time.time()-t0:.2f}s")
863
  if corrected_grammar != current_text:
864
  diffs = get_word_diffs(current_text, corrected_grammar)
src/css/components.css CHANGED
@@ -948,17 +948,152 @@ input[type="range"]::-webkit-slider-thumb {
948
 
949
  @media (min-width: 1024px) {
950
  .editor-layout {
951
- grid-template-columns: 1fr 340px;
952
  padding: var(--spacing-lg);
953
  }
954
  }
955
 
956
  @media (min-width: 1280px) {
957
  .editor-layout {
958
- grid-template-columns: 1fr 380px;
959
  }
960
  }
961
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
962
  /* ── Marketing token utilities ── */
963
  .marketing-card {
964
  background: var(--color-surface);
 
948
 
949
  @media (min-width: 1024px) {
950
  .editor-layout {
951
+ grid-template-columns: 220px 1fr 340px;
952
  padding: var(--spacing-lg);
953
  }
954
  }
955
 
956
  @media (min-width: 1280px) {
957
  .editor-layout {
958
+ grid-template-columns: 240px 1fr 380px;
959
  }
960
  }
961
 
962
+ /* ── Documents Panel (inline, always visible on desktop) ── */
963
+ .docs-panel {
964
+ display: none;
965
+ }
966
+
967
+ .docs-panel-desktop {
968
+ display: none;
969
+ }
970
+
971
+ @media (min-width: 1024px) {
972
+ .docs-panel-desktop {
973
+ display: flex;
974
+ flex-direction: column;
975
+ background: var(--color-surface);
976
+ border: 1px solid var(--color-border);
977
+ border-radius: var(--radius-card);
978
+ overflow: hidden;
979
+ max-height: calc(100vh - 100px);
980
+ position: sticky;
981
+ top: 80px;
982
+ }
983
+ }
984
+
985
+ .docs-panel__header {
986
+ display: flex;
987
+ align-items: center;
988
+ gap: var(--spacing-sm);
989
+ padding: var(--spacing-md) var(--spacing-lg);
990
+ border-bottom: 1px solid var(--color-border);
991
+ flex-shrink: 0;
992
+ }
993
+
994
+ .docs-panel__icon {
995
+ color: var(--color-text-secondary);
996
+ flex-shrink: 0;
997
+ }
998
+
999
+ .docs-panel__title {
1000
+ font-size: var(--font-size-caption);
1001
+ font-weight: var(--font-weight-bold);
1002
+ color: var(--color-text-secondary);
1003
+ text-transform: uppercase;
1004
+ letter-spacing: 0.06em;
1005
+ }
1006
+
1007
+ .docs-panel__body {
1008
+ flex: 1;
1009
+ overflow-y: auto;
1010
+ padding: var(--spacing-sm);
1011
+ }
1012
+
1013
+ /* ── Toolbar separator ── */
1014
+ .toolbar-separator {
1015
+ width: 1px;
1016
+ height: 20px;
1017
+ background: var(--color-border-strong);
1018
+ margin: 0 var(--spacing-xs);
1019
+ }
1020
+
1021
+ /* ── Save button (icon only) ── */
1022
+ .doc-save-btn-icon {
1023
+ display: flex;
1024
+ align-items: center;
1025
+ justify-content: center;
1026
+ padding: 4px;
1027
+ border-radius: var(--radius-md);
1028
+ min-height: 32px;
1029
+ min-width: 32px;
1030
+ }
1031
+
1032
+ /* ── Mobile docs toggle (hide on desktop where panel is inline) ── */
1033
+ .docs-sidebar-toggle-mobile {
1034
+ display: flex;
1035
+ align-items: center;
1036
+ justify-content: center;
1037
+ padding: 4px;
1038
+ min-height: 32px;
1039
+ min-width: 32px;
1040
+ }
1041
+
1042
+ @media (min-width: 1024px) {
1043
+ .docs-sidebar-toggle-mobile {
1044
+ display: none;
1045
+ }
1046
+ }
1047
+
1048
+ /* ── Sidebar Import/Export card ── */
1049
+ .sidebar-io {
1050
+ padding: var(--spacing-md) var(--spacing-lg);
1051
+ }
1052
+
1053
+ .sidebar-io__title {
1054
+ font-size: var(--font-size-caption);
1055
+ font-weight: var(--font-weight-bold);
1056
+ color: var(--color-text-secondary);
1057
+ text-transform: uppercase;
1058
+ letter-spacing: 0.06em;
1059
+ margin-bottom: var(--spacing-sm);
1060
+ }
1061
+
1062
+ .sidebar-io__actions {
1063
+ display: flex;
1064
+ gap: var(--spacing-sm);
1065
+ }
1066
+
1067
+ .sidebar-io__btn {
1068
+ flex: 1;
1069
+ display: flex;
1070
+ align-items: center;
1071
+ justify-content: center;
1072
+ gap: var(--spacing-xs);
1073
+ padding: 8px 12px;
1074
+ border: 1px solid var(--color-border);
1075
+ border-radius: var(--radius-md);
1076
+ background: var(--color-surface);
1077
+ color: var(--color-text-secondary);
1078
+ font-family: inherit;
1079
+ font-size: var(--font-size-caption);
1080
+ font-weight: var(--font-weight-semibold);
1081
+ cursor: pointer;
1082
+ transition: all var(--transition-base);
1083
+ min-height: 40px;
1084
+ }
1085
+
1086
+ .sidebar-io__btn:hover {
1087
+ border-color: var(--color-primary);
1088
+ color: var(--color-text-primary);
1089
+ background: var(--color-surface-elevated);
1090
+ }
1091
+
1092
+ .sidebar-io__dropdown {
1093
+ flex: 1;
1094
+ position: relative;
1095
+ }
1096
+
1097
  /* ── Marketing token utilities ── */
1098
  .marketing-card {
1099
  background: var(--color-surface);
src/index.html CHANGED
@@ -86,7 +86,7 @@
86
  </div>
87
  </div>
88
  <button id="theme-toggle" class="theme-toggle" aria-label="تبديل السمة" aria-pressed="true" type="button"></button>
89
- <button id="nav-cta" onclick="showPage('editor')" class="hidden sm:inline-flex px-4 md:px-6 py-2.5 rounded-xl text-sm md:text-base font-bold text-white gradient-accent transition-all">ابدأ الكتابة</button>
90
  </div>
91
  </div>
92
  </div>
@@ -113,14 +113,17 @@
113
  <div id="auth-offline-banner" class="auth-offline-banner is-hidden" role="status" aria-live="polite">
114
  تعذر الاتصال بخدمة المصادقة. يمكنك متابعة استخدام المحرر والمحاولة لاحقاً.
115
  </div>
116
- <!-- Phase 6.1 — Docs Sidebar -->
117
  <div id="docs-sidebar" class="docs-sidebar" role="complementary" aria-label="مستنداتي">
118
  <div class="docs-sidebar__header">
119
- <span class="docs-sidebar__title">📁 مستنداتي</span>
120
  </div>
121
  <div class="docs-sidebar__body">
122
- <button id="docs-new-btn" class="docs-new-btn" type="button" aria-label="مستند جديد">+ مستند جديد</button>
123
- <div id="docs-list" role="list" aria-label="قائمة المستندات"></div>
 
 
 
124
  </div>
125
  </div>
126
  <!-- Auth Gate -->
@@ -446,31 +449,34 @@
446
  <section class="pt-16 md:pt-20 min-h-screen page-bg">
447
  <div class="max-w-[1600px] mx-auto">
448
  <div class="editor-layout">
449
- <!-- Main Editor -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  <div>
451
  <div class="editor-shell">
452
  <div class="editor-toolbar">
453
  <div class="flex items-center gap-2 flex-wrap">
454
  <button id="write-tab" onclick="switchTab('write')" class="editor-tab active" type="button">كتابة</button>
455
  <button id="summarize-tab" onclick="switchTab('summarize')" class="editor-tab" type="button">تلخيص</button>
456
- <div class="doc-toolbar-actions doc-toolbar-actions--desktop">
457
- <button id="doc-import-btn" class="btn-ghost doc-dropdown__trigger" type="button" aria-label="استيراد ملف نصي أو Word">استيراد</button>
458
- <div class="doc-dropdown">
459
- <button id="doc-export-trigger" class="btn-ghost doc-dropdown__trigger" type="button" aria-label="تصدير المستند" aria-haspopup="true" aria-expanded="false">تصدير ▾</button>
460
- <div id="doc-export-menu" class="doc-dropdown__menu" role="menu" aria-label="خيارات التصدير">
461
- <button type="button" class="doc-dropdown__item" role="menuitem" data-export-format="txt">ملف نصي (.txt)</button>
462
- <button type="button" class="doc-dropdown__item" role="menuitem" data-export-format="docx">Word (.docx)</button>
463
- <button type="button" class="doc-dropdown__item" role="menuitem" data-export-format="pdf">PDF (.pdf)</button>
464
- </div>
465
- </div>
466
- <!-- Phase 6.1: Docs sidebar toggle + save -->
467
- <button id="docs-sidebar-toggle" class="docs-sidebar-toggle btn-ghost" type="button" aria-label="مستنداتي" aria-expanded="false" aria-controls="docs-sidebar">📁 مستنداتي</button>
468
- <button id="doc-save-btn" class="doc-save-btn" type="button" aria-label="حفظ المستند" title="حفظ">💾</button>
469
- </div>
470
- <!-- Doc title bar -->
471
  <div class="doc-title-bar">
472
  <span id="doc-current-title" class="doc-current-title">مستند جديد</span>
473
  </div>
 
 
 
474
  </div>
475
  <div class="flex items-center gap-4 flex-wrap">
476
  <div id="analyzing-indicator" class="analyzing-indicator" aria-live="polite">
@@ -478,6 +484,9 @@
478
  <span>جاري التحليل...</span>
479
  </div>
480
  <span class="text-sm text-secondary">عدد الكلمات: <span id="word-count" class="font-bold">٠</span></span>
 
 
 
481
  </div>
482
  </div>
483
  <input type="file" id="doc-import-input" class="sr-only" accept=".txt,.docx,text/plain,application/vnd.openxmlformats-officedocument.wordprocessingml.document" aria-hidden="true">
@@ -529,10 +538,6 @@
529
  <div class="flex items-center gap-2"><span class="stat-dot stat-dot--punctuation" aria-hidden="true"></span><span class="text-sm text-secondary"><span id="punctuation-count">٠</span> ترقيم</span></div>
530
  </div>
531
  <div class="editor-actions">
532
- <div class="doc-mobile-actions sm:hidden">
533
- <button id="doc-import-btn-mobile" class="btn-ghost flex-1" type="button" aria-label="استيراد ملف">استيراد</button>
534
- <button id="doc-mobile-export-trigger" class="btn-ghost flex-1" type="button" aria-label="تصدير المستند" aria-expanded="false">تصدير</button>
535
- </div>
536
  <button onclick="clearEditor()" class="btn-ghost" type="button">مسح الكل</button>
537
  <button onclick="copyText()" class="btn-primary" type="button">نسخ النص</button>
538
  </div>
@@ -543,7 +548,7 @@
543
  <span aria-hidden="true">▲</span>
544
  </button>
545
  </div>
546
- <!-- Sidebar -->
547
  <div class="sidebar-panel sidebar-desktop">
548
  <div class="surface-card sidebar-card">
549
  <h3 class="text-xl font-bold mb-4 text-right">تقييم الكتابة</h3>
@@ -560,7 +565,27 @@
560
  <div class="surface-card sidebar-card">
561
  <h3 class="text-xl font-bold mb-4 text-right">الاقتراحات</h3>
562
  <div id="suggestions-list" class="suggestions-scroll" role="list" aria-live="polite" aria-label="اقتراحات التصحيح"></div>
563
- <!-- apply-all removed: conflicts with multi-step spelling+grammar pipeline -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  </div>
565
  </div>
566
  </div>
@@ -577,7 +602,7 @@
577
  <button id="bottom-sheet-close" class="btn-ghost" type="button" aria-label="إغلاق">إغلاق</button>
578
  </div>
579
  <div id="bottom-sheet-list" class="suggestions-scroll" role="list" aria-live="polite"></div>
580
- <!-- apply-all removed: conflicts with multi-step spelling+grammar pipeline -->
581
  </div>
582
  </div>
583
  <!-- Mobile export sheet (documents) -->
 
86
  </div>
87
  </div>
88
  <button id="theme-toggle" class="theme-toggle" aria-label="تبديل السمة" aria-pressed="true" type="button"></button>
89
+
90
  </div>
91
  </div>
92
  </div>
 
113
  <div id="auth-offline-banner" class="auth-offline-banner is-hidden" role="status" aria-live="polite">
114
  تعذر الاتصال بخدمة المصادقة. يمكنك متابعة استخدام المحرر والمحاولة لاحقاً.
115
  </div>
116
+ <!-- Phase 6.1 — Docs Sidebar (mobile only overlay) -->
117
  <div id="docs-sidebar" class="docs-sidebar" role="complementary" aria-label="مستنداتي">
118
  <div class="docs-sidebar__header">
119
+ <span class="docs-sidebar__title">مستنداتي</span>
120
  </div>
121
  <div class="docs-sidebar__body">
122
+ <button id="docs-new-btn-mobile" class="docs-new-btn" type="button" aria-label="مستند جديد">
123
+ <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
124
+ مستند جديد
125
+ </button>
126
+ <div id="docs-list-mobile" role="list" aria-label="قائمة المستندات"></div>
127
  </div>
128
  </div>
129
  <!-- Auth Gate -->
 
449
  <section class="pt-16 md:pt-20 min-h-screen page-bg">
450
  <div class="max-w-[1600px] mx-auto">
451
  <div class="editor-layout">
452
+ <!-- RIGHT: Documents Panel (always visible on desktop) -->
453
+ <div class="docs-panel docs-panel-desktop">
454
+ <div class="docs-panel__header">
455
+ <svg class="docs-panel__icon" width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
456
+ <span class="docs-panel__title">مستنداتي</span>
457
+ </div>
458
+ <div class="docs-panel__body">
459
+ <button id="docs-new-btn" class="docs-new-btn" type="button" aria-label="مستند جديد">
460
+ <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
461
+ مستند جديد
462
+ </button>
463
+ <div id="docs-list" role="list" aria-label="قائمة المستندات"></div>
464
+ </div>
465
+ </div>
466
+ <!-- CENTER: Main Editor -->
467
  <div>
468
  <div class="editor-shell">
469
  <div class="editor-toolbar">
470
  <div class="flex items-center gap-2 flex-wrap">
471
  <button id="write-tab" onclick="switchTab('write')" class="editor-tab active" type="button">كتابة</button>
472
  <button id="summarize-tab" onclick="switchTab('summarize')" class="editor-tab" type="button">تلخيص</button>
473
+ <div class="toolbar-separator"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  <div class="doc-title-bar">
475
  <span id="doc-current-title" class="doc-current-title">مستند جديد</span>
476
  </div>
477
+ <button id="docs-sidebar-toggle" class="docs-sidebar-toggle-mobile btn-ghost lg:hidden" type="button" aria-label="مستنداتي" aria-expanded="false" aria-controls="docs-sidebar">
478
+ <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
479
+ </button>
480
  </div>
481
  <div class="flex items-center gap-4 flex-wrap">
482
  <div id="analyzing-indicator" class="analyzing-indicator" aria-live="polite">
 
484
  <span>جاري التحليل...</span>
485
  </div>
486
  <span class="text-sm text-secondary">عدد الكلمات: <span id="word-count" class="font-bold">٠</span></span>
487
+ <button id="doc-save-btn" class="doc-save-btn-icon btn-ghost" type="button" aria-label="حفظ المستند" title="حفظ">
488
+ <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/></svg>
489
+ </button>
490
  </div>
491
  </div>
492
  <input type="file" id="doc-import-input" class="sr-only" accept=".txt,.docx,text/plain,application/vnd.openxmlformats-officedocument.wordprocessingml.document" aria-hidden="true">
 
538
  <div class="flex items-center gap-2"><span class="stat-dot stat-dot--punctuation" aria-hidden="true"></span><span class="text-sm text-secondary"><span id="punctuation-count">٠</span> ترقيم</span></div>
539
  </div>
540
  <div class="editor-actions">
 
 
 
 
541
  <button onclick="clearEditor()" class="btn-ghost" type="button">مسح الكل</button>
542
  <button onclick="copyText()" class="btn-primary" type="button">نسخ النص</button>
543
  </div>
 
548
  <span aria-hidden="true">▲</span>
549
  </button>
550
  </div>
551
+ <!-- LEFT: Sidebar (score + suggestions + import/export) -->
552
  <div class="sidebar-panel sidebar-desktop">
553
  <div class="surface-card sidebar-card">
554
  <h3 class="text-xl font-bold mb-4 text-right">تقييم الكتابة</h3>
 
565
  <div class="surface-card sidebar-card">
566
  <h3 class="text-xl font-bold mb-4 text-right">الاقتراحات</h3>
567
  <div id="suggestions-list" class="suggestions-scroll" role="list" aria-live="polite" aria-label="اقتراحات التصحيح"></div>
568
+ <button id="apply-all-btn" class="btn-primary apply-all-btn is-hidden" type="button">تطبيق الكل</button>
569
+ </div>
570
+ <div class="surface-card sidebar-card sidebar-io">
571
+ <h3 class="sidebar-io__title text-right">استيراد وتصدير</h3>
572
+ <div class="sidebar-io__actions">
573
+ <button id="doc-import-btn" class="sidebar-io__btn" type="button" aria-label="استيراد ملف">
574
+ <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
575
+ استيراد
576
+ </button>
577
+ <div class="doc-dropdown sidebar-io__dropdown">
578
+ <button id="doc-export-trigger" class="sidebar-io__btn" type="button" aria-label="تصدير المستند" aria-haspopup="true" aria-expanded="false">
579
+ <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
580
+ تصدير
581
+ </button>
582
+ <div id="doc-export-menu" class="doc-dropdown__menu" role="menu" aria-label="خيارات التصدير">
583
+ <button type="button" class="doc-dropdown__item" role="menuitem" data-export-format="txt">نصي (.txt)</button>
584
+ <button type="button" class="doc-dropdown__item" role="menuitem" data-export-format="docx">Word (.docx)</button>
585
+ <button type="button" class="doc-dropdown__item" role="menuitem" data-export-format="pdf">PDF (.pdf)</button>
586
+ </div>
587
+ </div>
588
+ </div>
589
  </div>
590
  </div>
591
  </div>
 
602
  <button id="bottom-sheet-close" class="btn-ghost" type="button" aria-label="إغلاق">إغلاق</button>
603
  </div>
604
  <div id="bottom-sheet-list" class="suggestions-scroll" role="list" aria-live="polite"></div>
605
+ <button id="apply-all-sheet" class="btn-primary apply-all-btn mt-4 is-hidden" type="button">تطبيق الكل</button>
606
  </div>
607
  </div>
608
  <!-- Mobile export sheet (documents) -->
src/nlp/grammar/__init__.py DELETED
@@ -1 +0,0 @@
1
- # Grammar NLP package
 
 
src/nlp/grammar/grammar_service.py DELETED
@@ -1,118 +0,0 @@
1
- """
2
- Grammar correction service using Bayan_Arabic_Grammar (T5).
3
- Lazy-loads the model on first request, keeps it resident in memory.
4
- """
5
- import logging
6
- import torch
7
- from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
8
-
9
- logger = logging.getLogger(__name__)
10
-
11
- _grammar_model = None
12
- _grammar_tokenizer = None
13
- _device = None
14
-
15
- MODEL_ID = "bayan10/Bayan_Arabic_Grammar"
16
-
17
- # Generation parameters
18
- MAX_INPUT_LENGTH = 512
19
- MAX_OUTPUT_LENGTH = 512
20
- NUM_BEAMS = 4
21
-
22
-
23
- def get_grammar_model():
24
- """
25
- Lazy-load the grammar model. Returns (model, tokenizer, device).
26
- Loads once, stays in memory forever.
27
- """
28
- global _grammar_model, _grammar_tokenizer, _device
29
-
30
- if _grammar_model is not None:
31
- return _grammar_model, _grammar_tokenizer, _device
32
-
33
- logger.info("Loading Grammar model (lazy init)...")
34
-
35
- _device = "cpu"
36
-
37
- try:
38
- logger.info(f"Loading tokenizer: {MODEL_ID}")
39
- _grammar_tokenizer = AutoTokenizer.from_pretrained(
40
- MODEL_ID, local_files_only=True
41
- )
42
-
43
- logger.info(f"Loading T5 model: {MODEL_ID}")
44
- _grammar_model = AutoModelForSeq2SeqLM.from_pretrained(
45
- MODEL_ID, local_files_only=True
46
- )
47
- _grammar_model.to(_device)
48
- _grammar_model.eval()
49
-
50
- logger.info(f"Grammar model loaded on {_device}")
51
-
52
- except Exception as e:
53
- logger.error(f"Failed to load grammar model: {e}")
54
- # Try without local_files_only as fallback
55
- try:
56
- logger.info("Retrying without local_files_only...")
57
- _grammar_tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
58
- _grammar_model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_ID)
59
- _grammar_model.to(_device)
60
- _grammar_model.eval()
61
- logger.info(f"Grammar model loaded on {_device} (fallback)")
62
- except Exception as e2:
63
- logger.error(f"Grammar model load failed completely: {e2}")
64
- _grammar_model = None
65
- _grammar_tokenizer = None
66
- raise
67
-
68
- return _grammar_model, _grammar_tokenizer, _device
69
-
70
-
71
- def correct_grammar(text: str) -> str:
72
- """
73
- Run grammar correction on input text using T5.
74
- Returns the corrected text.
75
- """
76
- if not text or not text.strip():
77
- return text
78
-
79
- model, tokenizer, device = get_grammar_model()
80
-
81
- try:
82
- # Tokenize input
83
- inputs = tokenizer(
84
- text,
85
- return_tensors="pt",
86
- max_length=MAX_INPUT_LENGTH,
87
- truncation=True,
88
- padding=True
89
- )
90
- inputs = {k: v.to(device) for k, v in inputs.items()}
91
-
92
- # Generate correction
93
- with torch.no_grad():
94
- outputs = model.generate(
95
- **inputs,
96
- max_length=MAX_OUTPUT_LENGTH,
97
- num_beams=NUM_BEAMS,
98
- early_stopping=True,
99
- no_repeat_ngram_size=3,
100
- )
101
-
102
- # Decode output
103
- corrected = tokenizer.decode(outputs[0], skip_special_tokens=True)
104
-
105
- # Safety: if model returns empty or much shorter, keep original
106
- if not corrected or not corrected.strip():
107
- return text
108
-
109
- # If the correction is drastically different (>80% changed), keep original
110
- if len(corrected) < len(text) * 0.3:
111
- logger.warning(f"Grammar output too short ({len(corrected)} vs {len(text)}), keeping original")
112
- return text
113
-
114
- return corrected.strip()
115
-
116
- except Exception as e:
117
- logger.error(f"Grammar correction failed: {e}")
118
- return text