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 +0 -10
- src/app.py +21 -18
- src/css/components.css +137 -2
- src/index.html +53 -28
- src/nlp/grammar/__init__.py +0 -1
- src/nlp/grammar/grammar_service.py +0 -118
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':
|
| 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 |
-
|
| 467 |
-
|
|
|
|
|
|
|
|
|
|
| 468 |
|
| 469 |
return jsonify({
|
| 470 |
-
'
|
| 471 |
-
'
|
| 472 |
-
'
|
|
|
|
| 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 =
|
| 853 |
if has_grammar:
|
| 854 |
try:
|
| 855 |
t0 = time.time()
|
| 856 |
logger.info(f"[ANALYZE] Step 2: Grammar correction starting...")
|
| 857 |
-
|
| 858 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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">
|
| 120 |
</div>
|
| 121 |
<div class="docs-sidebar__body">
|
| 122 |
-
<button id="docs-new-btn" class="docs-new-btn" type="button" aria-label="مستند جديد">
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<!--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|