Skip to content

Instantly share code, notes, and snippets.

@fardm
Last active September 22, 2025 11:36
Show Gist options
  • Select an option

  • Save fardm/facadaf8238629bb96b9ab69b10b9dc3 to your computer and use it in GitHub Desktop.

Select an option

Save fardm/facadaf8238629bb96b9ab69b10b9dc3 to your computer and use it in GitHub Desktop.
تمرین راتینگ از حفظ

توضیح

این پروژه رو برای تمرین رایتینگ درست کردم. البته بیشتر برای این بوده که میخواستم یک متنی رو حفظ کنم. گفتم اگر یکی دو بار هم بنویسمش توی ذهنم بهتر ثبت میشه.

اول متن رفرنس رو بهش میدم. بعد شروع میکنم از حفظ می نویسم. اگر درست نوشته باشم متن سبز میشه اگر غلط بنویسم قرمز میشه.

اگر املای کلمات رو اشتباه بنویسید هم قرمز میشه.

توی بخش تنظیمات میتونید حساسیت رو تنظیم کنید. به بزرگ و کوچیک بودن کلمات حساس باشه یانه. همچنین به علائم نگارشی.

استفاده

  1. فایل پروژه رو دانلود کنید. بالا سمت راست روی دکمه download zip کلیک کنید.
  2. اول روی فایل install_update.bat کلیک کنید تا پیش نیاز ها نصب و آپدیت بشه.
  3. بعد روی فایل run.bat کلیک کنید تا اجرا بشه.
  4. حالا توی مروگر آدرس زیر رو وارد کنید:
http://127.0.0.1:5000

از شورتکات های زیر می تونید برای دکمه هینت و نکست استفاده کنید: Hint: ctrl + alt + h Next: ctrl + alt + n

# ===== IMPORTS & DEPENDENCIES =====
import os
from flask import Flask, render_template, request, jsonify
# ===== CONFIGURATION & CONSTANTS =====
app = Flask(__name__, template_folder='.', static_folder='.', static_url_path='')
# In a real app, use a more secure secret key
app.config['SECRET_KEY'] = 'your_super_secret_key_for_a_local_app'
# This will act as our in-memory "database" to store the reference text.
# For a local, single-user app, this is sufficient.
reference_data = {
"text": "",
"words": []
}
# ===== CORE BUSINESS LOGIC & API ROUTES =====
@app.route('/')
def index():
"""
Renders the main application page.
"""
return render_template('index.html')
@app.route('/api/set-reference', methods=['POST'])
def set_reference():
"""
Sets the reference text from a JSON payload (manual input).
"""
global reference_data
data = request.get_json()
if not data or 'text' not in data:
return jsonify({"status": "error", "message": "Invalid data provided."}), 400
text = data['text'].strip()
if not text:
return jsonify({"status": "error", "message": "Reference text cannot be empty."}), 400
reference_data['text'] = text
reference_data['words'] = text.split()
return jsonify({
"status": "success",
"message": "Reference text set successfully.",
"word_count": len(reference_data['words'])
})
@app.route('/api/upload-reference', methods=['POST'])
def upload_reference():
"""
Sets the reference text from an uploaded file.
"""
global reference_data
if 'referenceFile' not in request.files:
return jsonify({"status": "error", "message": "No file part in the request."}), 400
file = request.files['referenceFile']
if file.filename == '':
return jupytext({"status": "error", "message": "No file selected."}), 400
if file:
try:
text = file.read().decode('utf-8').strip()
if not text:
return jsonify({"status": "error", "message": "Uploaded file is empty."}), 400
reference_data['text'] = text
reference_data['words'] = text.split()
return jsonify({
"status": "success",
"message": f"File '{file.filename}' uploaded successfully.",
"word_count": len(reference_data['words'])
})
except Exception as e:
return jsonify({"status": "error", "message": f"Error reading file: {e}"}), 500
@app.route('/api/delete-reference', methods=['POST'])
def delete_reference():
"""
Clears the current reference text.
"""
global reference_data
reference_data['text'] = ""
reference_data['words'] = []
return jsonify({"status": "success", "message": "Reference text deleted."})
@app.route('/api/get-word', methods=['GET'])
def get_word():
"""
Gets a specific word from the reference text by its index.
Used for Hint and Next Word functionality.
"""
global reference_data
try:
index = int(request.args.get('index', -1))
except (ValueError, TypeError):
return jsonify({"status": "error", "message": "Invalid index provided."}), 400
if not reference_data['words']:
return jsonify({"status": "error", "message": "No reference text available."}), 404
if 0 <= index < len(reference_data['words']):
return jsonify({
"status": "success",
"word": reference_data['words'][index],
"index": index
})
else:
return jsonify({"status": "error", "message": "Index out of bounds."}), 404
@app.route('/api/get-all-words', methods=['GET'])
def get_all_words():
"""
Returns the entire list of reference words to the frontend.
This allows the frontend to perform real-time validation without constant API calls.
"""
global reference_data
return jsonify({
"status": "success",
"text": reference_data['text'],
"words": reference_data['words']
})
# ===== INITIALIZATION & STARTUP =====
if __name__ == '__main__':
# Using debug=True is great for development
app.run(debug=True, port=5000)
<!DOCTYPE html>
<html lang="en" class="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Writing Practice Web</title>
<!-- Tailwind CSS via CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
// Custom Tailwind configuration for dark mode and themes
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
light: {
bg: '#f0f2f5',
'bg-secondary': '#ffffff',
text: '#2c3e50',
accent: '#3498db',
'accent-hover': '#2980b9',
},
dark: {
bg: '#1e1e1e',
'bg-secondary': '#2c2c2c',
text: '#e0e0e0',
accent: '#1f6399',
'accent-hover': '#145a8a',
},
correct: '#27ae60',
wrong: '#c0392b',
'correct-dark': '#4caf50',
'wrong-dark': '#f44336',
},
}
}
}
</script>
<style>
/* Simple transition for modals and color changes */
body, .modal-backdrop, .modal {
transition: background-color 0.3s ease, color 0.3s ease;
}
.practice-container {
position: relative;
font-family: 'Segoe UI', sans-serif;
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
}
#practice-display {
padding: 0.75rem;
white-space: pre-wrap;
word-wrap: break-word;
z-index: 1;
min-height: 20rem; /* Same as textarea */
}
#practice-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0.75rem;
color: transparent;
background-color: transparent;
caret-color: black; /* Visible caret */
z-index: 2;
resize: none;
border: none;
outline: none;
}
.dark #practice-input {
caret-color: white;
}
</style>
</head>
<body class="bg-light-bg text-light-text dark:bg-dark-bg dark:text-dark-text font-sans antialiased">
<div class="container mx-auto max-w-4xl p-4 md:p-6">
<!-- Header -->
<header class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<button id="ref-note-btn" class="w-full text-white bg-light-accent hover:bg-light-accent-hover dark:bg-dark-accent dark:hover:bg-dark-accent-hover font-bold py-3 px-4 rounded-lg transition-colors duration-200">
📝 Reference Note
</button>
<button id="delete-btn" class="w-full text-white bg-light-accent hover:bg-light-accent-hover dark:bg-dark-accent dark:hover:bg-dark-accent-hover font-bold py-3 px-4 rounded-lg transition-colors duration-200" disabled>
🗑️ Delete Reference
</button>
<button id="settings-btn" class="w-full text-white bg-light-accent hover:bg-light-accent-hover dark:bg-dark-accent dark:hover:bg-dark-accent-hover font-bold py-3 px-4 rounded-lg transition-colors duration-200">
⚙️ Settings
</button>
</header>
<!-- Practice Area -->
<main class="bg-light-bg-secondary dark:bg-dark-bg-secondary p-6 rounded-lg shadow-md">
<h2 class="text-xl font-bold mb-4">Your Practice</h2>
<div id="practice-container" class="practice-container relative w-full border border-gray-300 dark:border-gray-600 rounded-lg">
<div id="practice-display" class="w-full h-80 overflow-y-auto"></div>
<textarea id="practice-input" class="w-full h-80" placeholder="Add a reference note to start practicing..." disabled></textarea>
</div>
<p id="word-count" class="text-sm text-right mt-2 text-gray-500 dark:text-gray-400">Words: 0 / 0</p>
</main>
<!-- Action Buttons -->
<footer class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6">
<button id="hint-btn" class="w-full text-white bg-light-accent hover:bg-light-accent-hover dark:bg-dark-accent dark:hover:bg-dark-accent-hover font-bold py-3 px-4 rounded-lg transition-colors duration-200" disabled>
💡 Hint
</button>
<button id="next-btn" class="w-full text-white bg-light-accent hover:bg-light-accent-hover dark:bg-dark-accent dark:hover:bg-dark-accent-hover font-bold py-3 px-4 rounded-lg transition-colors duration-200" disabled>
➡️ Next Word
</button>
</footer>
</div>
<!-- Modals -->
<!-- Reference Note Modal -->
<div id="reference-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-light-bg dark:bg-dark-bg p-6 rounded-lg shadow-xl w-full max-w-lg mx-4">
<h3 class="text-2xl font-bold mb-4">Add Reference Note</h3>
<div class="mb-4">
<label class="inline-flex items-center">
<input type="radio" name="inputMethod" value="manual" checked class="form-radio">
<span class="ml-2">Enter Text</span>
</label>
<label class="inline-flex items-center ml-6">
<input type="radio" name="inputMethod" value="file" class="form-radio">
<span class="ml-2">Import File</span>
</label>
</div>
<!-- Manual Input -->
<div id="manual-input-area">
<textarea id="ref-textarea" class="w-full h-40 p-2 border rounded-md bg-light-bg-secondary dark:bg-dark-bg-secondary dark:border-gray-600" placeholder="Paste or type your reference text here..."></textarea>
</div>
<!-- File Input -->
<div id="file-input-area" class="hidden">
<input type="file" id="ref-file-input" accept="text/*, .md" class="w-full text-sm file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-violet-50 file:text-violet-700 hover:file:bg-violet-100"/>
<p id="file-name" class="text-sm mt-2 text-gray-500"></p>
</div>
<div class="flex justify-end gap-4 mt-6">
<button id="add-ref-btn" class="text-white bg-light-accent hover:bg-light-accent-hover dark:bg-dark-accent dark:hover:bg-dark-accent-hover font-bold py-2 px-6 rounded-lg">Add</button>
<button id="close-ref-modal-btn" class="bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500 text-black dark:text-white font-bold py-2 px-6 rounded-lg">Close</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-light-bg dark:bg-dark-bg p-6 rounded-lg shadow-xl w-full max-w-sm mx-4">
<h3 class="text-2xl font-bold mb-6">Settings</h3>
<div class="space-y-4">
<label class="flex items-center justify-between">
<span>Case Sensitive</span>
<input type="checkbox" id="case-sensitive-toggle" class="form-checkbox h-5 w-5 rounded">
</label>
<label class="flex items-center justify-between">
<span>Punctuation Sensitive</span>
<input type="checkbox" id="punctuation-sensitive-toggle" class="form-checkbox h-5 w-5 rounded">
</label>
<div class="border-t dark:border-gray-600 pt-4 mt-4">
<p class="mb-2 font-semibold">Theme</p>
<div class="flex justify-between">
<label><input type="radio" name="theme" value="light" class="form-radio"> Light</label>
<label><input type="radio" name="theme" value="dark" class="form-radio"> Dark</label>
<label><input type="radio" name="theme" value="system" class="form-radio"> System</label>
</div>
</div>
</div>
<div class="text-center mt-8">
<button id="close-settings-modal-btn" class="bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500 text-black dark:text-white font-bold py-2 px-8 rounded-lg">Close</button>
</div>
</div>
</div>
<!-- Hint/Next Word Modal -->
<div id="hint-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-light-bg dark:bg-dark-bg p-6 rounded-lg shadow-xl w-full max-w-sm mx-4 text-center">
<h3 id="hint-modal-title" class="text-2xl font-bold mb-4">Hint</h3>
<p id="hint-modal-content" class="text-xl font-mono mb-6 p-4 bg-gray-200 dark:bg-gray-700 rounded-lg"></p>
<div class="flex justify-center gap-4 mt-6">
<button id="insert-word-btn" class="bg-light-accent hover:bg-light-accent-hover dark:bg-dark-accent dark:hover:bg-dark-accent-hover text-white font-bold py-2 px-8 rounded-lg">Enter</button>
<button id="close-hint-modal-btn" class="bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500 text-black dark:text-white font-bold py-2 px-8 rounded-lg">Close</button>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-light-bg dark:bg-dark-bg p-6 rounded-lg shadow-xl w-full max-w-sm mx-4 text-center">
<h3 class="text-2xl font-bold mb-4">Confirm Deletion</h3>
<p class="mb-6">Are you sure you want to delete the reference text?</p>
<div class="flex justify-center gap-4">
<button id="confirm-delete-btn" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-6 rounded-lg">Yes, Delete</button>
<button id="cancel-delete-btn" class="bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500 text-black dark:text-white font-bold py-2 px-6 rounded-lg">Cancel</button>
</div>
</div>
</div>
<!-- Simple Toast Notification -->
<div id="toast" class="fixed bottom-5 right-5 bg-gray-800 text-white py-2 px-5 rounded-lg shadow-lg opacity-0 transition-opacity duration-300">
<p id="toast-message"></p>
</div>
<!-- App Logic -->
<script src="main.js"></script>
</body>
</html>
@echo off
setlocal
:: Check if Python is installed
python --version >nul 2>&1
if %ERRORLEVEL% neq 0 (
echo Python is not installed. Please install Python and ensure it's added to PATH.
exit /b 1
)
:: Check if pip is installed
pip --version >nul 2>&1
if %ERRORLEVEL% neq 0 (
echo pip is not installed. Please ensure pip is installed with Python.
exit /b 1
)
:: Upgrade pip to the latest version
echo Upgrading pip...
python -m pip install --upgrade pip
:: Check if requirements.txt exists, otherwise install Flask directly
if exist requirements.txt (
echo Installing dependencies from requirements.txt...
pip install -r requirements.txt
) else (
echo No requirements.txt found. Installing Flask...
pip install flask
)
echo Dependencies installed/updated successfully.
echo To continue, please run run.bat
endlocal
pause
// ===== DOM ELEMENTS & STATE =====
document.addEventListener('DOMContentLoaded', () => {
// --- State ---
let state = {
referenceText: '',
referenceWords: [],
settings: {
isCaseSensitive: false,
isPunctuationSensitive: false,
theme: 'system' // 'light', 'dark', 'system'
},
currentWordIndex: 0
};
// --- DOM Selectors ---
const practiceInput = document.getElementById('practice-input');
const practiceDisplay = document.getElementById('practice-display');
const wordCountEl = document.getElementById('word-count');
const refNoteBtn = document.getElementById('ref-note-btn');
const deleteBtn = document.getElementById('delete-btn');
const settingsBtn = document.getElementById('settings-btn');
const hintBtn = document.getElementById('hint-btn');
const nextBtn = document.getElementById('next-btn');
// Reference Modal
const refModal = document.getElementById('reference-modal');
const refTextarea = document.getElementById('ref-textarea');
const refFileInput = document.getElementById('ref-file-input');
const fileNameEl = document.getElementById('file-name');
const manualInputArea = document.getElementById('manual-input-area');
const fileInputArea = document.getElementById('file-input-area');
const addRefBtn = document.getElementById('add-ref-btn');
const closeRefModalBtn = document.getElementById('close-ref-modal-btn');
// Settings Modal
const settingsModal = document.getElementById('settings-modal');
const caseSensitiveToggle = document.getElementById('case-sensitive-toggle');
const punctuationSensitiveToggle = document.getElementById('punctuation-sensitive-toggle');
const closeSettingsModalBtn = document.getElementById('close-settings-modal-btn');
// Hint/Next Word Modal
const hintModal = document.getElementById('hint-modal');
const hintModalTitle = document.getElementById('hint-modal-title');
const hintModalContent = document.getElementById('hint-modal-content');
const closeHintModalBtn = document.getElementById('close-hint-modal-btn');
const insertWordBtn = document.getElementById('insert-word-btn');
// Delete Confirmation Modal
const deleteModal = document.getElementById('delete-modal');
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
const cancelDeleteBtn = document.getElementById('cancel-delete-btn');
// Toast Notification
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
// Array of all modals
const allModals = [refModal, settingsModal, hintModal, deleteModal];
// ===== CORE LOGIC =====
/**
* Updates the practice display with colored characters based on user input.
*/
const updateHighlighting = () => {
const typedText = practiceInput.value;
const punctuationRegex = /[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/g;
// Compute normalized reference and typed text (remove whitespace always, punctuation if not sensitive)
let normRef = state.referenceText.replace(/\s/g, '');
if (!state.settings.isPunctuationSensitive) {
normRef = normRef.replace(punctuationRegex, '');
}
let normTyped = typedText.replace(/\s/g, '');
if (!state.settings.isPunctuationSensitive) {
normTyped = normTyped.replace(punctuationRegex, '');
}
// Build highlighted HTML
let html = '';
let normIdx = 0;
for (let j = 0; j < typedText.length; j++) {
const char = typedText[j];
if (/\s/.test(char)) {
html += char; // Preserve whitespace without coloring
} else {
let colorClass = 'text-correct dark:text-correct-dark';
let isMainChar = state.settings.isPunctuationSensitive || !punctuationRegex.test(char);
if (isMainChar) {
if (normIdx >= normRef.length) {
colorClass = 'text-wrong dark:text-wrong-dark';
} else {
const refChar = normRef[normIdx];
const isCorrect = state.settings.isCaseSensitive
? char === refChar
: char.toLowerCase() === refChar.toLowerCase();
colorClass = isCorrect ? 'text-correct dark:text-correct-dark' : 'text-wrong dark:text-wrong-dark';
normIdx++;
}
}
// Ignored punctuation is always correct
html += `<span class="${colorClass}">${char}</span>`;
}
}
practiceDisplay.innerHTML = html;
// Compute word starts for normalized positions
const wordStarts = [0];
let pos = 0;
for (let word of state.referenceWords) {
let normWord = state.settings.isPunctuationSensitive ? word : word.replace(punctuationRegex, '');
pos += normWord.length;
wordStarts.push(pos);
}
const len = normTyped.length;
// Find current word index
let currentWordIndex = 0;
for (let i = 0; i < state.referenceWords.length; i++) {
if (wordStarts[i] <= len && len < wordStarts[i + 1]) {
currentWordIndex = i;
break;
}
}
if (len >= wordStarts[state.referenceWords.length]) {
currentWordIndex = state.referenceWords.length - 1;
}
state.currentWordIndex = currentWordIndex;
// Compute completed words
let completed = 0;
for (let i = 1; i < wordStarts.length; i++) {
if (len >= wordStarts[i]) {
completed = i;
} else {
break;
}
}
updateWordCount(completed, state.referenceWords.length);
syncScroll();
};
/**
* Updates the UI state to enable or disable practice controls.
* @param {boolean} isEnabled - Whether to enable or disable.
*/
const setPracticeState = (isEnabled) => {
practiceInput.disabled = !isEnabled;
deleteBtn.disabled = !isEnabled;
hintBtn.disabled = !isEnabled;
nextBtn.disabled = !isEnabled;
if (!isEnabled) {
practiceInput.value = '';
practiceDisplay.innerHTML = '';
practiceInput.placeholder = 'Add a reference note to start practicing...';
updateWordCount(0, 0);
} else {
practiceInput.placeholder = 'Start typing here...';
}
};
/**
* Updates the word count display.
*/
const updateWordCount = (typed, total) => {
wordCountEl.textContent = `Words: ${typed} / ${total}`;
};
/**
* Synchronizes the scroll position of the input and display areas.
*/
const syncScroll = () => {
practiceDisplay.scrollTop = practiceInput.scrollTop;
};
// ===== API & EVENT HANDLERS =====
/**
* Shows a toast notification.
* @param {string} message - The message to display.
* @param {('success'|'error')} type - The type of message.
*/
const showToast = (message, type = 'success') => {
toastMessage.textContent = message;
toast.className = `fixed bottom-5 right-5 text-white py-2 px-5 rounded-lg shadow-lg transition-opacity duration-300 ${type === 'success' ? 'bg-green-600' : 'bg-red-600'}`;
toast.classList.remove('opacity-0');
setTimeout(() => toast.classList.add('opacity-0'), 3000);
};
/**
* Fetches the reference text and words from the backend and updates the state.
*/
const fetchReference = async () => {
try {
const response = await fetch('/api/get-all-words');
const data = await response.json();
if (data.status === 'success') {
state.referenceText = data.text;
state.referenceWords = data.words;
setPracticeState(state.referenceWords.length > 0);
updateHighlighting();
}
} catch (error) {
console.error('Error fetching reference:', error);
showToast('Could not fetch reference text.', 'error');
}
};
// --- Modal Controls ---
const openModal = (modal) => modal.classList.remove('hidden');
const closeModal = (modal) => modal.classList.add('hidden');
const closeAllModals = () => allModals.forEach(closeModal);
refNoteBtn.addEventListener('click', () => openModal(refModal));
closeRefModalBtn.addEventListener('click', () => closeAllModals());
settingsBtn.addEventListener('click', () => openModal(settingsModal));
closeSettingsModalBtn.addEventListener('click', () => closeAllModals());
closeHintModalBtn.addEventListener('click', () => closeAllModals());
cancelDeleteBtn.addEventListener('click', () => closeAllModals());
// Close modals when clicking outside or pressing Escape
allModals.forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeModal(modal);
}
});
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeAllModals();
}
});
// Reference modal input method toggle
document.querySelectorAll('input[name="inputMethod"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.value === 'manual') {
manualInputArea.classList.remove('hidden');
fileInputArea.classList.add('hidden');
} else {
manualInputArea.classList.add('hidden');
fileInputArea.classList.remove('hidden');
}
});
});
refFileInput.addEventListener('change', () => {
fileNameEl.textContent = refFileInput.files[0]?.name || 'No file chosen';
});
addRefBtn.addEventListener('click', async () => {
const inputMethod = document.querySelector('input[name="inputMethod"]:checked').value;
let response;
try {
if (inputMethod === 'manual') {
const text = refTextarea.value.trim();
if (!text) {
showToast('Please enter some text.', 'error');
return;
}
response = await fetch('/api/set-reference', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
} else {
const file = refFileInput.files[0];
if (!file) {
showToast('Please select a file.', 'error');
return;
}
const formData = new FormData();
formData.append('referenceFile', file);
response = await fetch('/api/upload-reference', {
method: 'POST',
body: formData,
});
}
const data = await response.json();
if (data.status === 'success') {
showToast(data.message);
await fetchReference();
closeAllModals();
refTextarea.value = '';
refFileInput.value = null;
fileNameEl.textContent = '';
} else {
showToast(data.message, 'error');
}
} catch (error) {
console.error('Error setting reference:', error);
showToast('An unexpected error occurred.', 'error');
}
});
// Handle delete button click, opens confirmation modal
deleteBtn.addEventListener('click', () => openModal(deleteModal));
// Handle the confirm deletion action
confirmDeleteBtn.addEventListener('click', async () => {
try {
const response = await fetch('/api/delete-reference', { method: 'POST' });
const data = await response.json();
if (data.status === 'success') {
showToast(data.message);
state.referenceText = '';
state.referenceWords = [];
setPracticeState(false);
} else {
showToast(data.message, 'error');
}
} catch (error) {
showToast('Error deleting reference.', 'error');
}
closeAllModals();
});
hintBtn.addEventListener('click', async () => {
try {
const response = await fetch(`/api/get-word?index=${state.currentWordIndex}`);
const data = await response.json();
if (data.status === 'success') {
hintModalTitle.textContent = "Hint";
hintModalContent.textContent = data.word;
openModal(hintModal);
} else {
showToast(data.message, 'error');
}
} catch (error) {
showToast('Could not fetch hint.', 'error');
}
});
nextBtn.addEventListener('click', async () => {
try {
const nextIndex = state.currentWordIndex + 1;
const response = await fetch(`/api/get-word?index=${nextIndex}`);
const data = await response.json();
if (data.status === 'success') {
hintModalTitle.textContent = "Next Word";
hintModalContent.textContent = data.word;
openModal(hintModal);
} else {
showToast('You are at the end of the text.', 'error');
}
} catch (error) {
showToast('Could not fetch next word.', 'error');
}
});
// Handle the "Enter" button click in the hint modal
insertWordBtn.addEventListener('click', () => {
const word = hintModalContent.textContent;
const currentValue = practiceInput.value.trim();
// Add a space if the input is not empty
const newText = currentValue.length > 0 ? currentValue + ' ' + word : word;
practiceInput.value = newText;
practiceInput.focus(); // Keep focus on the input box
updateHighlighting(); // Recalculate highlighting
closeAllModals();
});
// --- Settings Logic ---
const saveSettings = () => localStorage.setItem('writingPracticeSettings', JSON.stringify(state.settings));
const loadSettings = () => {
const saved = localStorage.getItem('writingPracticeSettings');
if (saved) {
state.settings = JSON.parse(saved);
}
caseSensitiveToggle.checked = state.settings.isCaseSensitive;
punctuationSensitiveToggle.checked = state.settings.isPunctuationSensitive;
document.querySelector(`input[name="theme"][value="${state.settings.theme}"]`).checked = true;
applyTheme();
};
caseSensitiveToggle.addEventListener('change', () => {
state.settings.isCaseSensitive = caseSensitiveToggle.checked;
saveSettings();
updateHighlighting();
});
punctuationSensitiveToggle.addEventListener('change', () => {
state.settings.isPunctuationSensitive = punctuationSensitiveToggle.checked;
saveSettings();
updateHighlighting();
});
// --- Theme Logic ---
const applyTheme = () => {
const root = document.documentElement;
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (state.settings.theme === 'dark' || (state.settings.theme === 'system' && systemPrefersDark)) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};
document.querySelectorAll('input[name="theme"]').forEach(radio => {
radio.addEventListener('change', (e) => {
state.settings.theme = e.target.value;
saveSettings();
applyTheme();
});
});
// --- Event Listeners ---
practiceInput.addEventListener('input', updateHighlighting);
practiceInput.addEventListener('scroll', syncScroll);
// Add keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.altKey) {
const key = e.key.toLowerCase();
if (key === 'h') {
if (!hintBtn.disabled) {
hintBtn.click();
e.preventDefault();
}
} else if (key === 'n') {
if (!nextBtn.disabled) {
nextBtn.click();
e.preventDefault();
}
}
}
});
// --- Initialization ---
const initializeApp = () => {
loadSettings();
fetchReference(); // See if there's a reference on the server from a previous session
};
initializeApp();
});
@echo off
setlocal
:: Check if app.py exists
if not exist app.py (
echo app.py not found. Please ensure it exists in the current directory.
exit /b 1
)
:: Run the Flask application
echo Starting the Flask application...
python app.py
endlocal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment