diff --git a/.gitignore b/.gitignore index a81c8ee1..60deb613 100644 --- a/.gitignore +++ b/.gitignore @@ -113,6 +113,10 @@ ENV/ env.bak/ venv.bak/ +# Local Codex state +.codex +.codex/ + # Spyder project settings .spyderproject .spyproject diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..c1c93d08 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,113 @@ +# Agent Instructions + +- Before merging any branch into `main`, create a backup branch from the branch being merged. +- Name the backup branch `_backup`. + +- This rule applies to all future merges into `main`. + + +## Destructive Edit Safeguard + +- If a request would delete or overwrite a large portion of a dataset file (for example, removing a broad ID range like "51 to end"), do not execute immediately. +- First, restate the exact impact with concrete counts or ranges (for example, "this will delete questions eng-vocab-0051 through eng-vocab-0175"). +- Ask for explicit confirmation before applying the destructive change, unless the user has already confirmed after seeing that impact summary. +- When the user goal is quality cleanup, prefer targeted removal of only the flagged or invalid items instead of bulk truncation. +- After destructive edits, include a short post-change summary of what was removed and what remains. + +## Octopus Endpoint Notes + +- The `/octopus` endpoint returns an HTML page (not JSON). +- It shows the best (cheapest) continuous upcoming usage window for each supported appliance duration. +- For each suggested window, the UI includes a collapsed-by-default expandable section with a mini table of half-hour slots and their tariff values in `p/kWh`. + +## Vocabulary Endpoint Notes + +- The `/vocab` endpoint returns an HTML practice page for `static/English/Vocabulary/questions.json`. +- By default, `/vocab` shows 10 randomly sampled questions. Supported quiz sizes are `5`, `10`, `15`, `20`, `25`, and `30`. +- If the requested quiz size is larger than the available question bank, show all available questions. +- The server shuffles the selected question set and shuffles each question's choices before sending them to the page. +- It is acceptable for the HTML document to carry answer metadata such as `correct` and `target_word`, but the page must not visibly display correct answers before marking. +- The main `Submit` button marks the quiz in the browser. Unanswered questions count as wrong. +- After submit, show the score as a fraction such as `7/10`: green for `>= 80%`, amber for `>= 60%` and `< 80%`, and red for `< 60%`. +- On submit, wrong questions should not reveal the correct answer. They should remain answerable and show a per-question `Retry` button until the student selects the correct option. +- When a retry attempt is correct, show `Retry correct` in blue instead of the normal first-attempt `Correct` label. +- Retry marking is client-side only and does not send feedback to the server. +- On submit, send the `target_word` values for questions missed on the first marking attempt to `POST /vocab/feedback` using this payload shape: + ```json + { + "missed_target_words": ["example"] + } + ``` +- Marking should not wait for feedback to complete. The page should show feedback status beside the submit button. +- The `Regenerate` control should request a fresh random set without a full page reload when practical, using `GET /vocab/questions?count=`. + +## Study Question Storage + +- Store multiple choice study questions under `static///questions.json`. +- Use folder names to identify the subject and category, for example `static/English/Vocabulary/questions.json`. +- Each `questions.json` file should contain a raw JSON array of question objects, not an outer wrapper object. +- Do not include `level` or `difficulty` fields unless the schema is intentionally changed later. +- Each question object should use: + - `id`: stable unique id, for example `eng-vocab-0001`. + - `type`: snake_case question type, for example `word_meaning`, `reverse_meaning`, `fill_in_blank`, `alternative_word`, or `part_of_speech`. + - `target_word`: the single canonical vocabulary item being tested. + - `prompt`: object containing the source material shown to the student, such as `meaning` and/or `sentence`. + - `question`: student-facing question text. + - `choices`: array of answer objects, each with `text` and `correct`. + - `explanation`: short explanation of the correct answer. +- Exactly one choice should have `"correct": true`. + +## Preparing Vocabulary Question JSON + +- When the user provides plain-text vocabulary MCQs, append them to `static/English/Vocabulary/questions.json` unless they specify another subject/category. +- Continue the existing id sequence. For example, if the last id is `eng-vocab-0024`, the next id is `eng-vocab-0025`. +- The same vocabulary word may appear in more than one question as long as the questions are different, for example a word meaning question and a fill-in-the-blank question for the same word. +- Use top-level `target_word` as the single canonical field for the vocabulary item being tested. Do not put `target_word` inside `prompt`, and do not use a separate `word` field. +- Include `target_word` for every vocabulary question type, including reverse-meaning and fill-in-the-blank questions. The app should not expose `target_word` while preparing the student-facing exercise; it is metadata for scoring, review, or future filtering. +- If `target_word` is not explicitly specified in the user's input, infer it from the question using best judgement, usually from the `Word:` line, the correct answer, the highlighted/referenced word, or the blank's correct completion. +- If `target_word` cannot be determined confidently, skip adding that question and report it back to the user. +- Preserve the user's answer option order, but do not store option letters such as `A`, `B`, `C`, `D`, or `E`. +- Convert each answer option to `{ "text": "...", "correct": true/false }`. +- Mark exactly one answer as correct, based on the intended answer from the prompt. +- Add a concise `explanation` for every new question. Explain why the correct answer is correct; do not merely repeat the answer. +- Use ASCII punctuation in JSON strings where practical, including straight quotes rather than curly quotes. +- After editing a question bank, validate it with `python3 -m json.tool static/English/Vocabulary/questions.json`. + +Use these mappings for vocabulary question batches: + +- `Type 1: Word Meaning MCQ` + - `type`: `word_meaning` + - `target_word`: `` + - `prompt`: `{}` + - `question`: `What does "" mean?` +- `Type 2: Reverse Meaning MCQ` + - `type`: `reverse_meaning` + - `target_word`: `` + - `prompt`: `{ "meaning": "" }` + - `question`: `Which word matches the meaning given?` +- `Type 3: Fill in the Blank MCQ` + - `type`: `fill_in_blank` + - `target_word`: `` + - `prompt`: `{ "sentence": "" }` + - `question`: `Which word best completes the sentence?` +- `Type 4: Alternative Word MCQ` + - `type`: `alternative_word` + - `target_word`: `` + - `prompt`: `{ "sentence": "" }` + - `question`: `Which word could best replace "" without changing the meaning?` +- `Type 5: Part of Speech MCQ` + - `type`: `part_of_speech` + - `target_word`: `` + - `prompt`: `{ "sentence": "" }` + - `question`: `In this sentence, what part of speech is ""?` + +Before finishing, check: + +- The top level is still a JSON array. +- Every new id is unique and sequential. +- Every new question has `id`, `type`, `target_word`, `prompt`, `question`, `choices`, and `explanation`. +- Every new `target_word` is either provided by the user or confidently inferred; uncertain questions are skipped and reported. +- No prompt uses `target_word` or `word`. +- Every new question has exactly one correct choice. +- Repeated vocabulary is acceptable when the question itself is distinct. +- No new question includes `level` or `difficulty`. diff --git a/README.md b/README.md index 76dd5e1d..796a5159 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,95 @@ -[![example-flask](https://github.com/koyeb/example-flask/actions/workflows/deploy.yaml/badge.svg)](https://github.com/koyeb/example-flask/actions) +# Avon Flask API -
- - Logo - -

Koyeb Serverless Platform

-

- Deploy a Flask application on Koyeb -
- Learn more about Koyeb - · - Explore the documentation - · - Discover our tutorials -

-
+A small Flask app focused on Octopus Agile tariff planning. +## Endpoints -## About Koyeb and the Flask example application +### `GET /tariff` +Returns the cheapest start time for a device run based on the requested duration. -Koyeb is a developer-friendly serverless platform to deploy apps globally. No-ops, servers, or infrastructure management. -This repository contains a Flask application you can deploy on the Koyeb serverless platform for testing. +**Query parameters** +- `numHours` (required, number): how long the appliance will run. -This example application is designed to show how a Flask application can be deployed on Koyeb. +**Success response** +- `200 OK` +```json +{ + "startTime": "2026-05-13 17:00:00" +} +``` -## Getting Started +**Error responses** +- `400 Bad Request` when `numHours` is missing or not numeric. +- `502 Bad Gateway` when upstream tariff data cannot be fetched. -Follow the steps below to deploy and run the Flask application on your Koyeb account. +### `GET /octopus` +Renders an HTML page showing best tariff windows for preset durations (`1` to `3.5` hours). -### Requirements +- Uses live Octopus tariff data. +- Displays start/end times, total tariff, and average tariff. +- Highlights cases where the total tariff is negative (credit periods). -You need a Koyeb account to successfully deploy and run this application. If you don't already have an account, you can sign-up for free [here](https://app.koyeb.com/auth/signup). +### `GET /vocab` +Renders an HTML vocabulary practice quiz using `static/English/Vocabulary/questions.json`. -### Deploy using the Koyeb button +- Shows 10 random questions by default. +- Supports quiz sizes of `5`, `10`, `15`, `20`, `25`, and `30`. +- Shuffles the selected questions and each question's answer choices. +- Marks answers in the browser when `Submit` is clicked. +- Counts unanswered questions as wrong. +- Shows the score as a colored fraction: green for `>= 80%`, amber for `>= 60%` and `< 80%`, red for `< 60%`. +- Allows retries for wrong answers without immediately showing the correct answer. +- Shows retry successes as `Retry correct` in blue. +- Sends missed `target_word` values from the first submit attempt to the dummy feedback endpoint. -The fastest way to deploy the Flask application is to click the **Deploy to Koyeb** button below. +**Query parameters** +- `count` (optional, number): requested quiz size. Invalid values fall back to `10`. -[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=git&repository=github.com/koyeb/example-flask&branch=main&name=flask-on-koyeb) +Example: -Clicking on this button brings you to the Koyeb App creation page with everything pre-set to launch this application. +```text +http://127.0.0.1:5000/vocab?count=15 +``` -_To modify this application example, you will need to fork this repository. Checkout the [fork and deploy](#fork-and-deploy-to-koyeb) instructions._ +### `GET /vocab/questions` +Returns a fresh random vocabulary question set as JSON for the page's regenerate control. -### Fork and deploy to Koyeb +**Query parameters** +- `count` (optional, number): requested quiz size. Allowed values are `5`, `10`, `15`, `20`, `25`, and `30`. -If you want to customize and enhance this application, you need to fork this repository. +### `POST /vocab/feedback` +Dummy endpoint that accepts the vocabulary words missed on the first submit attempt. -If you used the **Deploy to Koyeb** button, you can simply link your service to your forked repository to be able to push changes. -Alternatively, you can manually create the application as described below. +**Request body** +```json +{ + "missed_target_words": ["reclusive", "wither"] +} +``` -On the [Koyeb Control Panel](https://app.koyeb.com/), on the **Overview** tab, click the **Create Web Service** button to begin. +**Success response** +```json +{ + "missed_count": 2, + "status": "received" +} +``` -1. Select **GitHub** as the deployment method. -2. In the repositories list, select the repository you just forked. -3. In the **Builder** section, click the **override** toggle associated with the **Run command** and enter `gunicorn app:app` in the field. -4. Choose a name for your App and Service, i.e `flask-on-koyeb`, and click **Deploy**. +## Local run -You land on the deployment page where you can follow the build of your Flask application. Once the build is completed, your application is being deployed and you will be able to access it via `-.koyeb.app`. +```bash +python app.py +``` -## Contributing +Default Flask URL: +- `http://127.0.0.1:5000/tariff?numHours=2` +- `http://127.0.0.1:5000/octopus` +- `http://127.0.0.1:5000/vocab` -If you have any questions, ideas or suggestions regarding this application sample, feel free to open an [issue](//github.com//koyeb/example-flask/issues) or fork this repository and open a [pull request](//github.com/koyeb/example-flask/pulls). +## Environment variable -## Contact +Set the Octopus API key before running: -[Koyeb](https://www.koyeb.com) - [@gokoyeb](https://twitter.com/gokoyeb) - [Slack](http://slack.koyeb.com/) +```bash +export OCTOPUS_KEY="your_octopus_api_key" +``` diff --git a/app.py b/app.py index cb89d57b..c78d23e4 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,313 @@ -from flask import Flask +from flask import Flask, jsonify, request, abort, send_from_directory, render_template, session +from tariff_utils import calculate_start_time, get_best_tariff_windows +import os +import json +from datetime import datetime, timedelta +import random + +api_key = os.getenv("OCTOPUS_KEY") + app = Flask(__name__) +app.secret_key = os.getenv('FLASK_SECRET_KEY', 'dev-secret-key') + +# Vocabulary quiz settings are kept here because both the HTML page and +# regenerate endpoint need the same source file and allowed count values. +VOCAB_QUESTION_PATH = os.path.join( + app.root_path, + 'static', + 'English', + 'Vocabulary', + 'questions.json', +) +VOCAB_ALLOWED_COUNTS = [5, 10, 15, 20, 25, 30] +VOCAB_ALLOWED_TYPES = [ + 'word_meaning', + 'reverse_meaning', + 'fill_in_blank', + 'alternative_word', + 'part_of_speech', +] +VOCAB_RECENT_QUESTION_SESSION_KEY = 'recent_vocab_question_ids' +VOCAB_RECENT_QUESTION_LIMIT = 40 + @app.route('/') def hello_world(): - return 'Hello from Koyeb' + return jsonify(message="Hello, Happy Flasking!") + +@app.route('/api/spec') +def api_spec(): + return send_from_directory('static', 'api_spec.yaml') + + +@app.route('/xml/') +def serve_xml(filename: str): + """Serve XML files from the /static/xml directory via /xml/.xml. + + If the requested file does not exist, return a JSON 404 with a clear message. + Only .xml files are allowed (anything else 404s). + """ + if not filename.lower().endswith('.xml'): + abort(404) + + # Resolve from the intended static subdirectory and reject missing files. + xml_dir = os.path.join(app.root_path, 'static', 'xml') + file_path = os.path.join(xml_dir, filename) + + if not os.path.isfile(file_path): + return jsonify(error="File not found"), 404 + + return send_from_directory(xml_dir, filename, mimetype='application/xml') + + +@app.route('/html/') +def serve_html(filename: str): + """Serve HTML files from the /static/html directory via /html/.html. + + If the requested file does not exist, return a JSON 404 with a clear message. + Only .html files are allowed (anything else 404s). + """ + if not filename.lower().endswith('.html'): + abort(404) + + # Mirror the XML route, but restrict this endpoint to static HTML files. + html_dir = os.path.join(app.root_path, 'static', 'html') + file_path = os.path.join(html_dir, filename) + + if not os.path.isfile(file_path): + return jsonify(error="File not found"), 404 + + return send_from_directory(html_dir, filename, mimetype='text/html') + + +@app.route('/tariff') +def tariff(): + num_hours_str = request.args.get('numHours', default=None) + + if num_hours_str is None: + return jsonify(error="numHours parameter is required"), 400 + + try: + num_hours = float(num_hours_str) + except ValueError: + return jsonify(error="numHours must be a number"), 400 + + try: + start_time_str = calculate_start_time(num_hours, api_key) + except ValueError as error: + return jsonify(error=str(error)), 400 + except RuntimeError as error: + return jsonify(error=str(error)), 502 + + return jsonify(startTime=start_time_str) + + +@app.route('/octopus') +def octopus(): + # The Octopus page presents fixed appliance durations as a compact table. + durations = [1, 1.5, 2, 2.5, 3, 3.5] + page_error = None + window_rows = [] + + try: + window_rows = get_best_tariff_windows(durations, api_key) + except RuntimeError as error: + page_error = str(error) + + for row in window_rows: + # Convert tariff utility output into labels that the template can print. + if row.get('error'): + row['duration_label'] = f"{row['duration_hours']:g} hours" + continue + + row['duration_label'] = f"{row['duration_hours']:g} hours" + row['start_label'] = row['start_time'].strftime('%d %b %Y, %H:%M') + row['end_label'] = row['end_time'].strftime('%d %b %Y, %H:%M') + row['total_tariff_label'] = f"{row['total_tariff']:.2f} p/kWh" + row['average_tariff_label'] = f"{row['average_tariff']:.2f} p/kWh" + row['is_credit'] = row['total_tariff'] < 0 + row['slot_details'] = [ + { + 'start_label': slot['start_time'].strftime('%d %b %Y, %H:%M'), + 'end_label': slot['end_time'].strftime('%d %b %Y, %H:%M'), + 'tariff_label': f"{slot['tariff']:.2f} p/kWh", + } + for slot in row.get('slots', []) + ] + + return render_template( + 'octopus.html', + rows=window_rows, + page_error=page_error, + ) + + +def _get_vocab_count(default=10): + """Return a supported quiz size, falling back to the default for bad input.""" + count = request.args.get('count', default=default, type=int) + if count not in VOCAB_ALLOWED_COUNTS: + count = default + return count + + +def _get_vocab_types(): + """Return supported question types parsed from a comma-separated query list.""" + raw_types = request.args.get('types', default='', type=str) + + if not raw_types: + return list(VOCAB_ALLOWED_TYPES) + + requested_types = [question_type.strip() for question_type in raw_types.split(',') if question_type.strip()] + filtered_types = [question_type for question_type in requested_types if question_type in VOCAB_ALLOWED_TYPES] + + if not filtered_types: + return list(VOCAB_ALLOWED_TYPES) + + return filtered_types + + +def _load_vocab_questions(): + """Load the vocabulary question bank and ensure it keeps the expected shape.""" + with open(VOCAB_QUESTION_PATH, encoding='utf-8') as question_file: + questions = json.load(question_file) + + if not isinstance(questions, list): + raise ValueError('Vocabulary question bank must be a JSON array.') + + return questions + + +def _sample_vocab_questions(count, selected_types=None): + """Pick random questions, preferring IDs the current user has not just seen.""" + questions = _load_vocab_questions() + + if selected_types: + questions = [question for question in questions if question.get('type') in selected_types] + + sample_size = min(count, len(questions)) + recent_question_ids = session.get(VOCAB_RECENT_QUESTION_SESSION_KEY, []) + recent_question_id_set = set(recent_question_ids) + fresh_questions = [ + question for question in questions + if question.get('id') not in recent_question_id_set + ] + + if sample_size and len(fresh_questions) >= sample_size: + selected_questions = random.sample(fresh_questions, sample_size) + elif sample_size: + selected_questions = list(fresh_questions) + selected_question_ids = { + question.get('id') for question in selected_questions + if question.get('id') + } + refill_questions = [ + question for question in questions + if question.get('id') not in selected_question_ids + ] + remaining_count = sample_size - len(selected_questions) + selected_questions.extend(random.sample(refill_questions, remaining_count)) + random.shuffle(selected_questions) + else: + selected_questions = [] + + selected_ids = [question.get('id') for question in selected_questions if question.get('id')] + session[VOCAB_RECENT_QUESTION_SESSION_KEY] = ( + recent_question_ids + selected_ids + )[-VOCAB_RECENT_QUESTION_LIMIT:] + + sampled_questions = [] + for question in selected_questions: + question_copy = dict(question) + choices = [dict(choice) for choice in question_copy.get('choices', [])] + random.shuffle(choices) + question_copy['choices'] = choices + sampled_questions.append(question_copy) + + return sampled_questions + + +@app.route('/vocab') +def vocab(): + """Render the vocabulary quiz page with an initial random question set.""" + count = _get_vocab_count() + + selected_types = _get_vocab_types() + + try: + questions = _sample_vocab_questions(count, selected_types) + except (OSError, json.JSONDecodeError, ValueError) as error: + return render_template( + 'vocab.html', + questions=[], + selected_count=count, + allowed_counts=VOCAB_ALLOWED_COUNTS, + allowed_types=VOCAB_ALLOWED_TYPES, + page_error=str(error), + ), 500 + + return render_template( + 'vocab.html', + questions=questions, + selected_count=count, + allowed_counts=VOCAB_ALLOWED_COUNTS, + allowed_types=VOCAB_ALLOWED_TYPES, + page_error=None, + ) + + +@app.route('/vocab/questions') +def vocab_questions(): + """Return a fresh question set for no-refresh quiz regeneration.""" + count = _get_vocab_count() + + selected_types = _get_vocab_types() + + try: + questions = _sample_vocab_questions(count, selected_types) + except (OSError, json.JSONDecodeError, ValueError) as error: + return jsonify(error=str(error)), 500 + + return jsonify(questions=questions, count=len(questions), selected_types=selected_types) + + +@app.route('/vocab/feedback', methods=['POST']) +def vocab_feedback(): + """Accept first-attempt misses; currently this is a dummy receiver.""" + data = request.get_json(silent=True) or {} + missed_target_words = data.get('missed_target_words', []) + + if not isinstance(missed_target_words, list): + return jsonify(error='missed_target_words must be a list'), 400 + + return jsonify( + status='received', + missed_count=len(missed_target_words), + ) + + +@app.route('/demo_status') +def demo_status(): + connection_type = request.args.get('type', default=None) + + if connection_type not in ["FIX", "MQ", "SFTP", "ALL"]: + return jsonify(error="Invalid connection type. Allowed values are FIX, MQ, SFTP, ALL."), 400 + + return jsonify(message=f'All your {"" if connection_type == "ALL" else connection_type} connections are up and running') + + +@app.route('/demo_details') +def demo_details(): + connection_id = request.args.get('id', default=None) + + if not connection_id: + return jsonify(error="ID parameter is required"), 400 + + current_time = datetime.utcnow() + random_minutes = random.randint(1, 20) + last_connection_time = current_time - timedelta(minutes=random_minutes) + + return jsonify(message=f"Connection {connection_id} is up", lastConnectionTime=last_connection_time.isoformat() + "Z") -if __name__ == "__main__": - app.run() +if __name__ == '__main__': + app.run(debug=True) diff --git a/prepare_vocab.md b/prepare_vocab.md new file mode 100644 index 00000000..5dac5553 --- /dev/null +++ b/prepare_vocab.md @@ -0,0 +1,184 @@ +# Preparing Vocabulary Question JSON + +Use this guide to convert plain-text vocabulary multiple choice questions into the app's JSON format. + +## Target File + +Vocabulary questions should be stored here: + +```text +static/English/Vocabulary/questions.json +``` + +The file must contain a raw JSON array: + +```json +[ + { + "id": "eng-vocab-0001", + "type": "word_meaning", + "target_word": "reclusive", + "prompt": {}, + "question": "What does \"reclusive\" mean?", + "choices": [ + { + "text": "Very eager to argue", + "correct": false + }, + { + "text": "Preferring to live alone or avoid other people", + "correct": true + } + ], + "explanation": "Reclusive means preferring to live alone or avoid other people." + } +] +``` + +Do not wrap the array in a top-level object. Do not add `level` or `difficulty`. + +The same vocabulary word may appear in more than one question as long as the questions are different. For example, `prudent` may be used once in a word meaning question and again in an alternative word question. + +## Required Fields + +Each question must have: + +- `id`: stable unique id in sequence, such as `eng-vocab-0001`. +- `type`: snake_case type name. +- `target_word`: the single vocabulary item being tested. +- `prompt`: source material shown to the student, such as a meaning or sentence. +- `question`: the text shown to the student. +- `choices`: answer options as objects with `text` and `correct`. +- `explanation`: a short explanation for the correct answer. + +Exactly one choice must have `"correct": true`. + +Use top-level `target_word` as the single canonical field for the vocabulary item being tested. Do not put `target_word` inside `prompt`, and do not use a separate `word` field. Include `target_word` for every question type, including reverse-meaning and fill-in-the-blank questions. The app should not display `target_word` while preparing the student-facing exercise; it is metadata for scoring, review, or future filtering. + +If `target_word` is not explicitly specified in the input, infer it using best judgement. Usually it comes from the `Word:` line, the correct answer, the highlighted/referenced word, or the blank's correct completion. If `target_word` cannot be determined confidently, skip that question and report it back instead of guessing. + +## Question Types + +Use these type names: + +```text +word_meaning +reverse_meaning +fill_in_blank +alternative_word +part_of_speech +``` + +## Type Mapping + +For `Type 1: Word Meaning MCQ`, convert: + +```text +Word: reclusive +``` + +to: + +```json +"type": "word_meaning", +"target_word": "reclusive", +"prompt": {}, +"question": "What does \"reclusive\" mean?" +``` + +For `Type 2: Reverse Meaning MCQ`, convert: + +```text +Meaning given: +“Done secretly, especially because it should not be noticed.” +``` + +to: + +```json +"type": "reverse_meaning", +"target_word": "surreptitious", +"prompt": { + "meaning": "Done secretly, especially because it should not be noticed." +}, +"question": "Which word matches the meaning given?" +``` + +For `Type 3: Fill in the Blank MCQ`, convert the sentence to: + +```json +"type": "fill_in_blank", +"target_word": "wither", +"prompt": { + "sentence": "The old bridge looked strong, but the wooden railings had begun to ______ after years of rain and wind." +}, +"question": "Which word best completes the sentence?" +``` + +For `Type 4: Alternative Word MCQ`, convert the sentence and target word to: + +```json +"type": "alternative_word", +"target_word": "terse", +"prompt": { + "sentence": "His terse reply made it clear that he did not want to discuss the matter further." +}, +"question": "Which word could best replace \"terse\" without changing the meaning?" +``` + +For `Type 5: Part of Speech MCQ`, convert the sentence and target word to: + +```json +"type": "part_of_speech", +"target_word": "meticulously", +"prompt": { + "sentence": "The students worked meticulously on their model castle, checking every tiny detail." +}, +"question": "In this sentence, what part of speech is \"meticulously\"?" +``` + +## Choices + +Convert lettered answers into the `choices` array. Preserve the answer text, but do not store the letters `A`, `B`, `C`, `D`, or `E`. + +```json +"choices": [ + { + "text": "Surreptitious", + "correct": true + }, + { + "text": "Robust", + "correct": false + } +] +``` + +## Explanations + +Add a concise explanation for each question. The explanation should explain why the correct answer is correct, not merely repeat the answer. + +Examples: + +```text +Surreptitious means done secretly or in a hidden way. +Wither means to dry up, weaken, or decay. +Meticulously describes how the students worked, so it is an adverb. +``` + +## Validation Checklist + +Before returning the JSON: + +- The output is valid JSON. +- The top level is an array. +- Every question has all required fields. +- Every question has top-level `target_word`. +- Every `target_word` is either provided by the input or confidently inferred; uncertain questions are skipped and reported. +- No prompt uses `target_word` or `word`. +- Every `id` is unique. +- Every `type` is one of the approved type names. +- Every question has exactly one correct choice. +- Repeated vocabulary is acceptable when the question itself is distinct. +- No question includes `level` or `difficulty`. +- Quotation marks inside strings are escaped correctly. diff --git a/requirements.txt b/requirements.txt index ccecbc16..d2824552 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,11 @@ click==8.1.3 Flask==2.2.2 -gunicorn==20.1.0 +gunicorn==23.0.0 itsdangerous==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.1 Werkzeug==2.2.2 +Flask-RESTX +requests==2.26.0 +pytz +setuptools>=70 diff --git a/static/English/Vocabulary/AGENTS.md b/static/English/Vocabulary/AGENTS.md new file mode 100644 index 00000000..196822b9 --- /dev/null +++ b/static/English/Vocabulary/AGENTS.md @@ -0,0 +1,83 @@ +# Vocabulary Question Authoring Instructions (Local Scope) + +These instructions apply to files under `static/English/Vocabulary/`. + +## Source Material +- Use `word_bank.txt` as the source of tested vocabulary words. +- Tested word (or clear derivative) must come from `word_bank.txt`. +- Distractors/synonyms can come from inside or outside the word bank. + +## Output Target +- Add new questions to `questions.json` (do not replace existing questions). +- Output must follow the repository JSON schema defined in the root `AGENTS.md`. +- Top level of `questions.json` must remain a raw JSON array. + +## Word Selection +- Select words randomly, preferring words used least often so far. +- Reusing the same target word is allowed when the question itself is different. +- Derived forms are allowed when clearly linked to a word-bank base word. + +## Maintaining `word_bank.txt` +- When asked to add new words or phrases to `word_bank.txt`, first check whether each item already exists in the word bank. +- Only append items that are not already present. +- Keep one vocabulary item per line. + +## Question Type Selection Default +- If the user does not specify question type(s), produce a mixed question set that includes a mixture of all 5 defined types. + +## Supported Question Types and JSON Mapping +1. **Type 1: Word Meaning MCQ** + - `type`: `word_meaning` + - `target_word`: tested word + - `prompt`: `{}` + - `question`: `What does "" mean?` + +2. **Type 2: Reverse Meaning MCQ** + - `type`: `reverse_meaning` + - `target_word`: correct answer word + - `prompt`: `{ "meaning": "..." }` + - `question`: `Which word matches the meaning given?` + +3. **Type 3: Fill in the Blank MCQ** + - `type`: `fill_in_blank` + - `target_word`: correct answer word + - `prompt`: `{ "sentence": "...______..." }` + - `question`: `Which word best completes the sentence?` + +4. **Type 4: Alternative Word MCQ** + - `type`: `alternative_word` + - `target_word`: replaceable word in sentence + - `prompt`: `{ "sentence": "..." }` + - `question`: `Which word could best replace "" without changing the meaning?` + +5. **Type 5: Part of Speech MCQ** + - `type`: `part_of_speech` + - `target_word`: analysed word + - `prompt`: `{ "sentence": "..." }` + - `question`: `In this sentence, what part of speech is ""?` + +## MCQ Construction Rules +- Exactly 5 choices per question. +- Exactly 1 choice has `"correct": true`. +- Shuffle answer order so the correct option is not fixed to one position. +- Preserve user-provided option order only when converting from a provided plain-text MCQ set. +- Keep language suitable for Year 8 learners. + +## Required Fields Per Question +Each new question object must include: +- `id` (sequential and unique, e.g. `eng-vocab-00xx`) +- `type` +- `target_word` +- `prompt` +- `question` +- `choices` (`[{"text":"...","correct":true/false}, ...]`) +- `explanation` (brief, useful reason) + +## Guardrails +- Do not add `level` or `difficulty` unless schema intentionally changes. +- Do not put `target_word` inside `prompt`. +- If `target_word` cannot be inferred confidently from input, skip that question and report it. + +## Validation +After editing `questions.json`, run: +- `python3 -m json.tool static/English/Vocabulary/questions.json` diff --git a/static/English/Vocabulary/Question_instruction.txt b/static/English/Vocabulary/Question_instruction.txt new file mode 100644 index 00000000..e6278851 --- /dev/null +++ b/static/English/Vocabulary/Question_instruction.txt @@ -0,0 +1,159 @@ +You are a vocabulary trainer for Year 8 students. Your job is to create and explain vocabulary exercises based ONLY on the given word_bank.txt file and the rules below. + +GENERAL BEHAVIOUR: +- All questions must be based on words from the word bank (word_bank.txt). You may use words outside the word bank as distractors or synonyms, but the tested word (or its form) must come from the word bank. +- All MCQs must have exactly ONE correct answer. +- Finally, make sure all the choices on all questions a shuffled properly. We don’t want correct answers are always on the particular letter choice. + +WORD SELECTION LOGIC: +- When creating new questions, choose words from the word bank RANDOMLY but with a preference for words that have not been tested yet, or that have been tested the fewest times so far. +- Track, as best you can, how many times each word has been used in previous questions in the current conversation and use this to guide your choices. +- You may use different forms of these words where appropriate (plural, adjective, adverb, different tense, etc.) as long as they clearly relate to a base word in the word bank. + +QUESTION TYPES: +You support 5 types of questions. The user will specify which type(s) and how many questions they want, for example: +- “Give me 4 questions of type 1.” +- “Give me 3 type-3 questions and 2 type-4 questions.” +- “1 question of type 2 using ‘benevolent’.” + +Always label the question type and number clearly (e.g. “Question 1 – Type 3”). + +-------------------------------- +TYPE 1 – WORDS MEANING MCQ +-------------------------------- +Task: +- Given a word from the word bank (or its derived form), create a multiple-choice question where the student chooses the correct MEANING. + +Rules: +- Present the word clearly, e.g. “Word: audacious”. +- Provide 5 answer choices (A–E). +- Exactly ONE choice must be correct. +- The other 4 choices should be plausible but wrong meanings. +- Avoid having the correct choice particularly long or detailed that making it easy to guess. +- all 5 choices should have similar length +- Meanings should be at a Year 8 level (not babyish, not university-level). + +Format example: +Question X – Type 1: Word Meaning MCQ +Word: audacious +A. … +B. … +C. … +D. … +E. … + +-------------------------------- +TYPE 2 – REVERSE MEANING MCQ +-------------------------------- +Task: +- Given a DEFINITION or description of a meaning, the student must choose the matching WORD. + +Rules: +- The correct word must come from the word bank or its clear derivative. +- The other 4 options can be: + - other words from the word bank, and/or + - words outside the word bank. +- Exactly ONE correct answer. +- Definition should be clear for Year 8. + +Format example: +Question X – Type 2: Reverse Meaning MCQ +Meaning given: +“Feeling or showing anger because something seems unfair or insulting.” +A. … +B. … +C. … +D. … +E. … + +-------------------------------- +TYPE 3 – FILL IN THE BLANK MCQ +-------------------------------- +Task: +- Create a sentence with ONE blank space. Students choose the best word to fill the blank. + +Rules: +- The correct answer must be a word (or form) from the word bank. +- Give 5 choices (A–E). +- Incorrect choices can be: + - other words from the word bank, and/or + - words outside the word bank. +- Exactly ONE choice must work naturally in the sentence. +- The sentence must be grammatically correct and suitable for Year 8. +- Make sure the context strongly supports the correct word choice. + +Format example: +Question X – Type 3: Fill in the Blank MCQ +Sentence: +The cliff-top ______ jutted out into the sea, giving us a clear view of the waves below. +A. … +B. … +C. … +D. … +E. … + +-------------------------------- +TYPE 4 – ALTERNATIVE WORD (SYNONYM) MCQ +-------------------------------- +Task: +- Write a sentence that CONTAINS one word from the word bank. +- Ask the student to choose a word that could replace that word WITHOUT changing the meaning of the sentence significantly. + +Rules: +- The target word in the sentence must be from the word bank. +- In the answer choices, give 5 options. +- Exactly ONE option should be the best synonym or near-synonym in context. +- The 4 distractors should NOT work as good synonyms in that sentence. +- Options may come from inside or outside the word bank. + +Format example: +Question X – Type 4: Alternative Word MCQ +Sentence: +The alley was notorious for pickpockets after dark. +Which word could best replace “notorious” without changing the meaning? +A. … +B. … +C. … +D. … +E. … + +-------------------------------- +TYPE 5 – PART OF SPEECH MCQ +-------------------------------- +Task: +- Write a sentence that includes one word from the word bank. +- Ask the student to identify the PART OF SPEECH of that word in the sentence. + +Rules: +- Use standard part-of-speech categories: noun, verb, adjective, adverb, preposition, conjunction, interjection. +- Give 5 options (A–E). +- Exactly ONE correct option. +- Sentence should make the function of the word reasonably clear. + +Format example: +Question X – Type 5: Part of Speech MCQ +Sentence: +She tried to fathom the puzzle, but the solution still escaped her. +In this sentence, what part of speech is “fathom”? +A. Noun +B. Verb +C. Adjective +D. Adverb +E. Preposition + +-------------------------------- +ANSWER DISCLOSURE RULE: +-------------------------------- +- When the user later asks “Give me the answers for Question X–Y” or “Mark my answers” or “Explain Question 3”, you may: + - Provide the correct option(s). + - Provide brief explanations for why they are correct and why others are not. +- Be clear and encouraging in your explanations, suitable for a Year 8 learner. + +-------------------------------- +EXAMPLES OF USER REQUESTS YOU SHOULD HANDLE: +-------------------------------- +- “Give me 5 questions of type 1.” +- “3 questions of type 3, please.” +- “Mix: 2 type-2 and 3 type-4 questions.” + +Always follow the rules above carefully. diff --git a/static/English/Vocabulary/questions.json b/static/English/Vocabulary/questions.json new file mode 100644 index 00000000..bf6c41a5 --- /dev/null +++ b/static/English/Vocabulary/questions.json @@ -0,0 +1,2530 @@ +[ + { + "id": "eng-vocab-0001", + "type": "word_meaning", + "prompt": {}, + "question": "What does \"reclusive\" mean?", + "choices": [ + { + "text": "Very eager to argue", + "correct": false + }, + { + "text": "Preferring to live alone or avoid other people", + "correct": true + }, + { + "text": "Extremely bright and colourful", + "correct": false + }, + { + "text": "Quick to forgive someone", + "correct": false + }, + { + "text": "Full of energy and noise", + "correct": false + } + ], + "explanation": "Reclusive means preferring to live alone or avoid other people.", + "target_word": "reclusive" + }, + { + "id": "eng-vocab-0002", + "type": "reverse_meaning", + "prompt": { + "meaning": "Done secretly, especially because it should not be noticed." + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "Surreptitious", + "correct": true + }, + { + "text": "Robust", + "correct": false + }, + { + "text": "Ample", + "correct": false + }, + { + "text": "Diligent", + "correct": false + }, + { + "text": "Frivolous", + "correct": false + } + ], + "explanation": "Surreptitious means done secretly or in a hidden way.", + "target_word": "surreptitious" + }, + { + "id": "eng-vocab-0003", + "type": "fill_in_blank", + "prompt": { + "sentence": "The old bridge looked strong, but the wooden railings had begun to ______ after years of rain and wind." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "vindicate", + "correct": false + }, + { + "text": "wither", + "correct": true + }, + { + "text": "retain", + "correct": false + }, + { + "text": "appease", + "correct": false + }, + { + "text": "speculate", + "correct": false + } + ], + "explanation": "Wither means to dry up, weaken, or decay.", + "target_word": "wither" + }, + { + "id": "eng-vocab-0004", + "type": "alternative_word", + "prompt": { + "sentence": "His terse reply made it clear that he did not want to discuss the matter further." + }, + "question": "Which word could best replace \"terse\" without changing the meaning?", + "choices": [ + { + "text": "cheerful", + "correct": false + }, + { + "text": "brief", + "correct": true + }, + { + "text": "confused", + "correct": false + }, + { + "text": "generous", + "correct": false + }, + { + "text": "dramatic", + "correct": false + } + ], + "explanation": "Terse means brief, short, or using very few words.", + "target_word": "terse" + }, + { + "id": "eng-vocab-0005", + "type": "fill_in_blank", + "prompt": { + "sentence": "The detective tried to ______ the truth from the tiny clues left at the scene." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "deduce", + "correct": true + }, + { + "text": "strew", + "correct": false + }, + { + "text": "hinder", + "correct": false + }, + { + "text": "grill", + "correct": false + }, + { + "text": "recede", + "correct": false + } + ], + "explanation": "Deduce means to work out an answer or truth from evidence.", + "target_word": "deduce" + }, + { + "id": "eng-vocab-0006", + "type": "part_of_speech", + "prompt": { + "sentence": "The students worked meticulously on their model castle, checking every tiny detail." + }, + "question": "In this sentence, what part of speech is \"meticulously\"?", + "choices": [ + { + "text": "Noun", + "correct": false + }, + { + "text": "Verb", + "correct": false + }, + { + "text": "Adjective", + "correct": false + }, + { + "text": "Adverb", + "correct": true + }, + { + "text": "Preposition", + "correct": false + } + ], + "explanation": "Meticulously describes how the students worked, so it is an adverb.", + "target_word": "meticulously" + }, + { + "id": "eng-vocab-0007", + "type": "word_meaning", + "prompt": {}, + "question": "What does \"prudent\" mean?", + "choices": [ + { + "text": "Very noisy and excited", + "correct": false + }, + { + "text": "Weak and easily broken", + "correct": false + }, + { + "text": "Sensible and careful before making decisions", + "correct": true + }, + { + "text": "Rude in a bold way", + "correct": false + }, + { + "text": "Covered in bright colours", + "correct": false + } + ], + "explanation": "Prudent means sensible, careful, and showing good judgement before acting.", + "target_word": "prudent" + }, + { + "id": "eng-vocab-0008", + "type": "reverse_meaning", + "prompt": { + "meaning": "Kind, helpful, and wanting to do good for others." + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "Benevolent", + "correct": true + }, + { + "text": "Treacherous", + "correct": false + }, + { + "text": "Fusty", + "correct": false + }, + { + "text": "Frantic", + "correct": false + }, + { + "text": "Dingy", + "correct": false + } + ], + "explanation": "Benevolent means kind, generous, and wanting to help others.", + "target_word": "benevolent" + }, + { + "id": "eng-vocab-0009", + "type": "fill_in_blank", + "prompt": { + "sentence": "The path along the cliff was narrow and ______, so we walked very slowly." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "courteous", + "correct": false + }, + { + "text": "treacherous", + "correct": true + }, + { + "text": "splendid", + "correct": false + }, + { + "text": "tangible", + "correct": false + }, + { + "text": "solemn", + "correct": false + } + ], + "explanation": "Treacherous means dangerous or unsafe, which fits a narrow cliff path.", + "target_word": "treacherous" + }, + { + "id": "eng-vocab-0010", + "type": "alternative_word", + "prompt": { + "sentence": "The old castle looked forlorn in the rain." + }, + "question": "Which word could best replace \"forlorn\" without changing the meaning?", + "choices": [ + { + "text": "cheerful", + "correct": false + }, + { + "text": "lonely", + "correct": true + }, + { + "text": "crowded", + "correct": false + }, + { + "text": "shiny", + "correct": false + }, + { + "text": "noisy", + "correct": false + } + ], + "explanation": "Forlorn means lonely, sad, or abandoned.", + "target_word": "forlorn" + }, + { + "id": "eng-vocab-0011", + "type": "part_of_speech", + "prompt": { + "sentence": "She tried to fathom the strange message." + }, + "question": "In this sentence, what part of speech is \"fathom\"?", + "choices": [ + { + "text": "Adjective", + "correct": false + }, + { + "text": "Noun", + "correct": false + }, + { + "text": "Adverb", + "correct": false + }, + { + "text": "Verb", + "correct": true + }, + { + "text": "Preposition", + "correct": false + } + ], + "explanation": "Fathom is a verb here because it names the action of trying to understand something.", + "target_word": "fathom" + }, + { + "id": "eng-vocab-0012", + "type": "fill_in_blank", + "prompt": { + "sentence": "From the high ______, we could see the sea stretching far into the distance." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "belfry", + "correct": false + }, + { + "text": "thicket", + "correct": false + }, + { + "text": "cask", + "correct": false + }, + { + "text": "promontory", + "correct": true + }, + { + "text": "visor", + "correct": false + } + ], + "explanation": "A promontory is a high point of land that sticks out, often over water.", + "target_word": "promontory" + }, + { + "id": "eng-vocab-0013", + "type": "word_meaning", + "prompt": {}, + "question": "What does \"uncanny\" mean?", + "choices": [ + { + "text": "Very old and damaged", + "correct": false + }, + { + "text": "Strange or mysterious in a slightly frightening way", + "correct": true + }, + { + "text": "Extremely loud and cheerful", + "correct": false + }, + { + "text": "Easy to understand", + "correct": false + }, + { + "text": "Full of bright colours", + "correct": false + } + ], + "explanation": "Uncanny means strange or mysterious in a way that can feel slightly frightening.", + "target_word": "uncanny" + }, + { + "id": "eng-vocab-0014", + "type": "reverse_meaning", + "prompt": { + "meaning": "Move quickly or awkwardly over rough ground, often using hands as well as feet." + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "slumber", + "correct": false + }, + { + "text": "scramble", + "correct": true + }, + { + "text": "deceive", + "correct": false + }, + { + "text": "perish", + "correct": false + }, + { + "text": "reckon", + "correct": false + } + ], + "explanation": "Scramble means to move quickly or awkwardly, especially over rough ground.", + "target_word": "scramble" + }, + { + "id": "eng-vocab-0015", + "type": "fill_in_blank", + "prompt": { + "sentence": "After walking all day, the travellers ______ beside the river and slept under the stars." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "glanced", + "correct": false + }, + { + "text": "bivouacked", + "correct": true + }, + { + "text": "dispatched", + "correct": false + }, + { + "text": "crowned", + "correct": false + }, + { + "text": "quenched", + "correct": false + } + ], + "explanation": "Bivouacked means camped temporarily, especially without tents or in the open.", + "target_word": "bivouacked" + }, + { + "id": "eng-vocab-0016", + "type": "alternative_word", + "prompt": { + "sentence": "It was prudent to take a map before entering the forest." + }, + "question": "Which word could best replace \"prudent\" without changing the meaning?", + "choices": [ + { + "text": "careless", + "correct": false + }, + { + "text": "sensible", + "correct": true + }, + { + "text": "noisy", + "correct": false + }, + { + "text": "gloomy", + "correct": false + }, + { + "text": "stubborn", + "correct": false + } + ], + "explanation": "Prudent means sensible and careful before acting.", + "target_word": "prudent" + }, + { + "id": "eng-vocab-0017", + "type": "word_meaning", + "prompt": {}, + "question": "What does \"promontory\" mean?", + "choices": [ + { + "text": "A high piece of land that sticks out into the sea", + "correct": true + }, + { + "text": "A small wooden boat", + "correct": false + }, + { + "text": "A deep underground room", + "correct": false + }, + { + "text": "A royal crown", + "correct": false + }, + { + "text": "A narrow city street", + "correct": false + } + ], + "explanation": "A promontory is a high piece of land that sticks out into the sea or another body of water.", + "target_word": "promontory" + }, + { + "id": "eng-vocab-0018", + "type": "reverse_meaning", + "prompt": { + "meaning": "Something given to show respect, thanks, or honour." + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "tribute", + "correct": true + }, + { + "text": "mischief", + "correct": false + }, + { + "text": "grudge", + "correct": false + }, + { + "text": "carcass", + "correct": false + }, + { + "text": "gauntlet", + "correct": false + } + ], + "explanation": "A tribute is something said, done, or given to show respect, thanks, or honour.", + "target_word": "tribute" + }, + { + "id": "eng-vocab-0019", + "type": "fill_in_blank", + "prompt": { + "sentence": "The old cupboard had a ______ smell, as if it had not been opened for years." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "frantic", + "correct": false + }, + { + "text": "courteous", + "correct": false + }, + { + "text": "fusty", + "correct": true + }, + { + "text": "splendid", + "correct": false + }, + { + "text": "tangible", + "correct": false + } + ], + "explanation": "Fusty means smelling stale, damp, or old.", + "target_word": "fusty" + }, + { + "id": "eng-vocab-0020", + "type": "alternative_word", + "prompt": { + "sentence": "The pirate was notorious for attacking ships along the coast." + }, + "question": "Which word could best replace \"notorious\" without changing the meaning?", + "choices": [ + { + "text": "famous for something bad", + "correct": true + }, + { + "text": "completely unknown", + "correct": false + }, + { + "text": "gentle and kind", + "correct": false + }, + { + "text": "recently arrived", + "correct": false + }, + { + "text": "easily frightened", + "correct": false + } + ], + "explanation": "Notorious means famous or well known for something bad.", + "target_word": "notorious" + }, + { + "id": "eng-vocab-0021", + "type": "word_meaning", + "prompt": {}, + "question": "What does \"fathom\" mean?", + "choices": [ + { + "text": "To understand something after thinking about it", + "correct": true + }, + { + "text": "To shout in anger", + "correct": false + }, + { + "text": "To cover something with gold", + "correct": false + }, + { + "text": "To run away secretly", + "correct": false + }, + { + "text": "To build a wall around something", + "correct": false + } + ], + "explanation": "Fathom means to understand something, especially after thinking carefully.", + "target_word": "fathom" + }, + { + "id": "eng-vocab-0022", + "type": "part_of_speech", + "prompt": { + "sentence": "The cat moved stealthily through the garden." + }, + "question": "In this sentence, what part of speech is \"stealthily\"?", + "choices": [ + { + "text": "Noun", + "correct": false + }, + { + "text": "Verb", + "correct": false + }, + { + "text": "Adjective", + "correct": false + }, + { + "text": "Adverb", + "correct": true + }, + { + "text": "Conjunction", + "correct": false + } + ], + "explanation": "Stealthily describes how the cat moved, so it is an adverb.", + "target_word": "stealthily" + }, + { + "id": "eng-vocab-0023", + "type": "fill_in_blank", + "prompt": { + "sentence": "The path was so ______ that we had to climb slowly and carefully." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "steep", + "correct": true + }, + { + "text": "amiable", + "correct": false + }, + { + "text": "briny", + "correct": false + }, + { + "text": "solemn", + "correct": false + }, + { + "text": "frail", + "correct": false + } + ], + "explanation": "Steep means rising or falling sharply, which explains why the path had to be climbed carefully.", + "target_word": "steep" + }, + { + "id": "eng-vocab-0024", + "type": "reverse_meaning", + "prompt": { + "meaning": "A tower or part of a tower where bells are kept, often in a church." + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "belfry", + "correct": true + }, + { + "text": "prow", + "correct": false + }, + { + "text": "visor", + "correct": false + }, + { + "text": "thicket", + "correct": false + }, + { + "text": "cask", + "correct": false + } + ], + "explanation": "A belfry is a tower or part of a tower where bells are kept.", + "target_word": "belfry" + }, + { + "id": "eng-vocab-0025", + "type": "word_meaning", + "prompt": {}, + "question": "What does \"benevolent\" mean?", + "choices": [ + { + "text": "Sneaky and dishonest", + "correct": false + }, + { + "text": "Kind and wanting to help others", + "correct": true + }, + { + "text": "Very old and weak", + "correct": false + }, + { + "text": "Loud and unpleasant", + "correct": false + }, + { + "text": "Difficult to control", + "correct": false + } + ], + "explanation": "Benevolent means kind, generous, and wanting to help others.", + "target_word": "benevolent" + }, + { + "id": "eng-vocab-0026", + "type": "fill_in_blank", + "prompt": { + "sentence": "The soldier stood as a ______ at the gate, watching carefully for any danger." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "reveller", + "correct": false + }, + { + "text": "regent", + "correct": false + }, + { + "text": "sentinel", + "correct": true + }, + { + "text": "prophet", + "correct": false + }, + { + "text": "boatswain", + "correct": false + } + ], + "explanation": "A sentinel is a guard or watchperson who keeps lookout for danger.", + "target_word": "sentinel" + }, + { + "id": "eng-vocab-0027", + "type": "alternative_word", + "prompt": { + "sentence": "The child looked apprehensive before entering the dark cave." + }, + "question": "Which word could best replace \"apprehensive\" without changing the meaning?", + "choices": [ + { + "text": "excited", + "correct": false + }, + { + "text": "fearless", + "correct": false + }, + { + "text": "sleepy", + "correct": false + }, + { + "text": "anxious", + "correct": true + }, + { + "text": "cheerful", + "correct": false + } + ], + "explanation": "Apprehensive means anxious or worried that something bad may happen.", + "target_word": "apprehensive" + }, + { + "id": "eng-vocab-0028", + "type": "reverse_meaning", + "prompt": { + "meaning": "A deep narrow valley with steep rocky sides." + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "gorge", + "correct": true + }, + { + "text": "belfry", + "correct": false + }, + { + "text": "cask", + "correct": false + }, + { + "text": "wigwam", + "correct": false + }, + { + "text": "avenue", + "correct": false + } + ], + "explanation": "A gorge is a deep, narrow valley with steep rocky sides.", + "target_word": "gorge" + }, + { + "id": "eng-vocab-0029", + "type": "part_of_speech", + "prompt": { + "sentence": "The knight spoke courteously to the old woman." + }, + "question": "In this sentence, what part of speech is \"courteously\"?", + "choices": [ + { + "text": "Noun", + "correct": false + }, + { + "text": "Verb", + "correct": false + }, + { + "text": "Adjective", + "correct": false + }, + { + "text": "Adverb", + "correct": true + }, + { + "text": "Preposition", + "correct": false + } + ], + "explanation": "Courteously describes how the knight spoke, so it is an adverb.", + "target_word": "courteously" + }, + { + "id": "eng-vocab-0030", + "type": "word_meaning", + "prompt": {}, + "question": "What does \"ostentatious\" mean?", + "choices": [ + { + "text": "Quiet and shy", + "correct": false + }, + { + "text": "Weak and easily broken", + "correct": false + }, + { + "text": "Showy in a way that tries too hard to impress people", + "correct": true + }, + { + "text": "Very ordinary and boring", + "correct": false + }, + { + "text": "Kind and forgiving", + "correct": false + } + ], + "explanation": "Ostentatious means showy in a way intended to attract attention or impress people.", + "target_word": "ostentatious" + }, + { + "id": "eng-vocab-0031", + "type": "fill_in_blank", + "prompt": { + "sentence": "She checked every calculation ______, making sure there were no careless mistakes." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "hastily", + "correct": false + }, + { + "text": "bitterly", + "correct": false + }, + { + "text": "vaguely", + "correct": false + }, + { + "text": "noisily", + "correct": false + }, + { + "text": "meticulously", + "correct": true + } + ], + "explanation": "Meticulously means very carefully and with close attention to detail.", + "target_word": "meticulously" + }, + { + "id": "eng-vocab-0032", + "type": "reverse_meaning", + "prompt": { + "meaning": "Something that is very unpleasant, disgusting, or offensive." + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "repugnant", + "correct": true + }, + { + "text": "versatile", + "correct": false + }, + { + "text": "modest", + "correct": false + }, + { + "text": "fleeting", + "correct": false + }, + { + "text": "lenient", + "correct": false + } + ], + "explanation": "Repugnant means extremely unpleasant, disgusting, or offensive.", + "target_word": "repugnant" + }, + { + "id": "eng-vocab-0033", + "type": "alternative_word", + "prompt": { + "sentence": "The new jacket was versatile because it could be worn for school, sports, or a party." + }, + "question": "Which word could best replace \"versatile\" without changing the meaning?", + "choices": [ + { + "text": "expensive", + "correct": false + }, + { + "text": "fragile", + "correct": false + }, + { + "text": "colourful", + "correct": false + }, + { + "text": "adaptable", + "correct": true + }, + { + "text": "ancient", + "correct": false + } + ], + "explanation": "Versatile means adaptable or able to be used in many different ways.", + "target_word": "versatile" + }, + { + "id": "eng-vocab-0034", + "type": "part_of_speech", + "prompt": { + "sentence": "The teacher gave a lenient punishment because it was the student's first mistake." + }, + "question": "In this sentence, what part of speech is \"lenient\"?", + "choices": [ + { + "text": "Noun", + "correct": false + }, + { + "text": "Adjective", + "correct": true + }, + { + "text": "Verb", + "correct": false + }, + { + "text": "Adverb", + "correct": false + }, + { + "text": "Preposition", + "correct": false + } + ], + "explanation": "Lenient describes the punishment, so it is an adjective.", + "target_word": "lenient" + }, + { + "id": "eng-vocab-0035", + "type": "fill_in_blank", + "prompt": { + "sentence": "The old bridge looked ______, so we crossed it one person at a time." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "cheerful", + "correct": false + }, + { + "text": "loyal", + "correct": false + }, + { + "text": "invisible", + "correct": false + }, + { + "text": "luxurious", + "correct": false + }, + { + "text": "precarious", + "correct": true + } + ], + "explanation": "Precarious means unsafe, unstable, or likely to fall or fail.", + "target_word": "precarious" + }, + { + "id": "eng-vocab-0036", + "type": "word_meaning", + "prompt": {}, + "question": "What does \"conspicuous\" mean?", + "choices": [ + { + "text": "Easy to notice", + "correct": true + }, + { + "text": "Difficult to understand", + "correct": false + }, + { + "text": "Quiet and shy", + "correct": false + }, + { + "text": "Moving very fast", + "correct": false + }, + { + "text": "Broken into pieces", + "correct": false + } + ], + "explanation": "Conspicuous means easy to see or notice.", + "target_word": "conspicuous" + }, + { + "id": "eng-vocab-0037", + "type": "word_meaning", + "prompt": {}, + "question": "What does \"grudge\" mean?", + "choices": [ + { + "text": "A joyful celebration", + "correct": false + }, + { + "text": "A long-lasting feeling of resentment", + "correct": true + }, + { + "text": "A type of weapon", + "correct": false + }, + { + "text": "A formal promise", + "correct": false + }, + { + "text": "A sudden mistake", + "correct": false + } + ], + "explanation": "A grudge is a lasting feeling of anger or resentment toward someone.", + "target_word": "grudge" + }, + { + "id": "eng-vocab-0038", + "type": "reverse_meaning", + "prompt": { + "meaning": "Bold and willing to take risks, sometimes in a shocking way." + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "cautious", + "correct": false + }, + { + "text": "audacious", + "correct": true + }, + { + "text": "feeble", + "correct": false + }, + { + "text": "obedient", + "correct": false + }, + { + "text": "mournful", + "correct": false + } + ], + "explanation": "Audacious means boldly daring or willing to take risks.", + "target_word": "audacious" + }, + { + "id": "eng-vocab-0039", + "type": "reverse_meaning", + "prompt": { + "meaning": "Real enough to be touched or clearly felt; not just imagined." + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "sinister", + "correct": false + }, + { + "text": "tangible", + "correct": true + }, + { + "text": "forlorn", + "correct": false + }, + { + "text": "obstinate", + "correct": false + }, + { + "text": "shrill", + "correct": false + } + ], + "explanation": "Tangible means real enough to touch or clearly notice, rather than imagined.", + "target_word": "tangible" + }, + { + "id": "eng-vocab-0040", + "type": "fill_in_blank", + "prompt": { + "sentence": "Mia could not ______ how the magician made the coin disappear." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "scatter", + "correct": false + }, + { + "text": "quarrel", + "correct": false + }, + { + "text": "fathom", + "correct": true + }, + { + "text": "perish", + "correct": false + }, + { + "text": "mimic", + "correct": false + } + ], + "explanation": "Fathom means to understand something after thinking about it.", + "target_word": "fathom" + }, + { + "id": "eng-vocab-0041", + "type": "fill_in_blank", + "prompt": { + "sentence": "Even before the test began, Jayden felt ______ because he had forgotten to revise one whole topic." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "benevolent", + "correct": false + }, + { + "text": "apprehensive", + "correct": true + }, + { + "text": "courtly", + "correct": false + }, + { + "text": "raucous", + "correct": false + }, + { + "text": "pristine", + "correct": false + } + ], + "explanation": "Apprehensive means anxious or worried about something that might happen.", + "target_word": "apprehensive" + }, + { + "id": "eng-vocab-0042", + "type": "alternative_word", + "prompt": { + "sentence": "The benevolent neighbour brought food to the family after the fire." + }, + "question": "Which word could best replace \"benevolent\" without changing the meaning?", + "choices": [ + { + "text": "kind", + "correct": true + }, + { + "text": "noisy", + "correct": false + }, + { + "text": "stubborn", + "correct": false + }, + { + "text": "careless", + "correct": false + }, + { + "text": "greedy", + "correct": false + } + ], + "explanation": "Benevolent means kind and wanting to help others.", + "target_word": "benevolent" + }, + { + "id": "eng-vocab-0043", + "type": "alternative_word", + "prompt": { + "sentence": "There was something sinister about the empty house at the end of the lane." + }, + "question": "Which word could best replace \"sinister\" without changing the meaning?", + "choices": [ + { + "text": "cheerful", + "correct": false + }, + { + "text": "ordinary", + "correct": false + }, + { + "text": "threatening", + "correct": true + }, + { + "text": "colourful", + "correct": false + }, + { + "text": "fragile", + "correct": false + } + ], + "explanation": "Sinister means threatening, evil-looking, or suggesting something bad may happen.", + "target_word": "sinister" + }, + { + "id": "eng-vocab-0044", + "type": "part_of_speech", + "prompt": { + "sentence": "The captain tried to deceive the guards with a false story." + }, + "question": "In this sentence, what part of speech is \"deceive\"?", + "choices": [ + { + "text": "Noun", + "correct": false + }, + { + "text": "Adjective", + "correct": false + }, + { + "text": "Adverb", + "correct": false + }, + { + "text": "Verb", + "correct": true + }, + { + "text": "Preposition", + "correct": false + } + ], + "explanation": "Deceive is a verb here because it names the action the captain tried to do.", + "target_word": "deceive" + }, + { + "id": "eng-vocab-0045", + "type": "part_of_speech", + "prompt": { + "sentence": "The treacherous path was covered in loose stones and mud." + }, + "question": "In this sentence, what part of speech is \"treacherous\"?", + "choices": [ + { + "text": "Conjunction", + "correct": false + }, + { + "text": "Verb", + "correct": false + }, + { + "text": "Adjective", + "correct": true + }, + { + "text": "Interjection", + "correct": false + }, + { + "text": "Noun", + "correct": false + } + ], + "explanation": "Treacherous describes the path, so it is an adjective.", + "target_word": "treacherous" + }, + { + "id": "eng-vocab-0046", + "type": "word_meaning", + "prompt": {}, + "question": "What does \"audacious\" mean?", + "choices": [ + { + "text": "very shy and quiet", + "correct": false + }, + { + "text": "rude in a silly way", + "correct": false + }, + { + "text": "bold and daring", + "correct": true + }, + { + "text": "tired and weak", + "correct": false + }, + { + "text": "slow to understand", + "correct": false + } + ], + "explanation": "Audacious means bold, daring, and willing to take risks.", + "target_word": "audacious" + }, + { + "id": "eng-vocab-0047", + "type": "reverse_meaning", + "prompt": { + "meaning": "Kind and generous, and wanting to help other people." + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "tangible", + "correct": false + }, + { + "text": "sinister", + "correct": false + }, + { + "text": "obstinate", + "correct": false + }, + { + "text": "benevolent", + "correct": true + }, + { + "text": "miserable", + "correct": false + } + ], + "explanation": "Benevolent means kind, generous, and wanting to help others.", + "target_word": "benevolent" + }, + { + "id": "eng-vocab-0048", + "type": "fill_in_blank", + "prompt": { + "sentence": "Even after the teacher explained the science idea twice, Noah still could not ______ it fully." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "dispatch", + "correct": false + }, + { + "text": "fathom", + "correct": true + }, + { + "text": "mimic", + "correct": false + }, + { + "text": "hazard", + "correct": false + }, + { + "text": "avenge", + "correct": false + } + ], + "explanation": "Fathom means to understand something after thinking about it.", + "target_word": "fathom" + }, + { + "id": "eng-vocab-0049", + "type": "alternative_word", + "prompt": { + "sentence": "Sana felt apprehensive before opening the letter from the headteacher." + }, + "question": "Which word could best replace \"apprehensive\" without changing the meaning?", + "choices": [ + { + "text": "curious", + "correct": false + }, + { + "text": "cheerful", + "correct": false + }, + { + "text": "nervous", + "correct": true + }, + { + "text": "careless", + "correct": false + }, + { + "text": "patient", + "correct": false + } + ], + "explanation": "Apprehensive means anxious or nervous about something that may happen.", + "target_word": "apprehensive" + }, + { + "id": "eng-vocab-0050", + "type": "part_of_speech", + "prompt": { + "sentence": "The detectives finally found tangible evidence at the scene." + }, + "question": "In this sentence, what part of speech is \"tangible\"?", + "choices": [ + { + "text": "Noun", + "correct": false + }, + { + "text": "Verb", + "correct": false + }, + { + "text": "Adverb", + "correct": false + }, + { + "text": "Preposition", + "correct": false + }, + { + "text": "Adjective", + "correct": true + } + ], + "explanation": "Tangible describes the noun evidence, so it is an adjective.", + "target_word": "tangible" + }, + { + "id": "eng-vocab-0051", + "type": "word_meaning", + "target_word": "cairn", + "prompt": {}, + "question": "What does \"cairn\" mean?", + "choices": [ + { + "text": "a wooden fishing boat", + "correct": false + }, + { + "text": "a sudden loud cry", + "correct": false + }, + { + "text": "a pile of stones used as a marker", + "correct": true + }, + { + "text": "a narrow castle window", + "correct": false + }, + { + "text": "a small cooking fire", + "correct": false + } + ], + "explanation": "A cairn is a man-made pile of stones used as a trail or location marker." + }, + { + "id": "eng-vocab-0052", + "type": "reverse_meaning", + "target_word": "courteous", + "prompt": { + "meaning": "polite, respectful, and well-mannered" + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "courteous", + "correct": true + }, + { + "text": "coarse", + "correct": false + }, + { + "text": "frantic", + "correct": false + }, + { + "text": "dingy", + "correct": false + }, + { + "text": "solemn", + "correct": false + } + ], + "explanation": "Courteous means showing polite and respectful behavior toward others." + }, + { + "id": "eng-vocab-0053", + "type": "fill_in_blank", + "target_word": "forded", + "prompt": { + "sentence": "The hikers _____ the shallow stream by stepping carefully from stone to stone." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "rested", + "correct": false + }, + { + "text": "whispered", + "correct": false + }, + { + "text": "wandered", + "correct": false + }, + { + "text": "guarded", + "correct": false + }, + { + "text": "forded", + "correct": true + } + ], + "explanation": "Forded means crossed water at a shallow point, which fits the sentence context." + }, + { + "id": "eng-vocab-0054", + "type": "alternative_word", + "target_word": "prudent", + "prompt": { + "sentence": "Maya made a prudent choice and saved some of her money instead of spending it all." + }, + "question": "Which word could best replace \"prudent\" without changing the meaning?", + "choices": [ + { + "text": "reckless", + "correct": false + }, + { + "text": "sensible", + "correct": true + }, + { + "text": "sleepy", + "correct": false + }, + { + "text": "jealous", + "correct": false + }, + { + "text": "careless", + "correct": false + } + ], + "explanation": "Prudent means wise and careful, so sensible is the closest replacement." + }, + { + "id": "eng-vocab-0055", + "type": "part_of_speech", + "target_word": "stealthily", + "prompt": { + "sentence": "The cat moved stealthily across the garden, hoping no one would notice it." + }, + "question": "In this sentence, what part of speech is \"stealthily\"?", + "choices": [ + { + "text": "noun", + "correct": false + }, + { + "text": "verb", + "correct": false + }, + { + "text": "adjective", + "correct": false + }, + { + "text": "adverb", + "correct": true + }, + { + "text": "preposition", + "correct": false + } + ], + "explanation": "Stealthily describes how the cat moved, so it functions as an adverb." + }, + { + "id": "eng-vocab-0056", + "type": "word_meaning", + "target_word": "belfry", + "prompt": {}, + "question": "What does \"belfry\" mean?", + "choices": [ + { + "text": "a room for storing grain", + "correct": false + }, + { + "text": "a deep forest path", + "correct": false + }, + { + "text": "a tower or room where bells are kept", + "correct": true + }, + { + "text": "a small leather bag", + "correct": false + }, + { + "text": "a sharp mountain edge", + "correct": false + } + ], + "explanation": "A belfry is the part of a tower that houses bells." + }, + { + "id": "eng-vocab-0057", + "type": "reverse_meaning", + "target_word": "treacherous", + "prompt": { + "meaning": "dangerous because it is unsafe or not to be trusted" + }, + "question": "Which word matches the meaning given?", + "choices": [ + { + "text": "treacherous", + "correct": true + }, + { + "text": "splendid", + "correct": false + }, + { + "text": "contentment", + "correct": false + }, + { + "text": "earnest", + "correct": false + }, + { + "text": "courteous", + "correct": false + } + ], + "explanation": "Treacherous describes something hazardous or unreliable." + }, + { + "id": "eng-vocab-0058", + "type": "fill_in_blank", + "target_word": "mischief", + "prompt": { + "sentence": "The puppy caused _____ when it dragged muddy socks all over the clean kitchen floor." + }, + "question": "Which word best completes the sentence?", + "choices": [ + { + "text": "silence", + "correct": false + }, + { + "text": "victory", + "correct": false + }, + { + "text": "weather", + "correct": false + }, + { + "text": "comfort", + "correct": false + }, + { + "text": "mischief", + "correct": true + } + ], + "explanation": "Mischief means playful trouble, matching the puppy's messy behavior." + }, + { + "id": "eng-vocab-0059", + "type": "alternative_word", + "target_word": "frail", + "prompt": { + "sentence": "The frail chair creaked loudly when Tom sat on it." + }, + "question": "Which word could best replace \"frail\" without changing the meaning?", + "choices": [ + { + "text": "bright", + "correct": false + }, + { + "text": "weak", + "correct": true + }, + { + "text": "proud", + "correct": false + }, + { + "text": "quick", + "correct": false + }, + { + "text": "round", + "correct": false + } + ], + "explanation": "Frail means weak or easily broken, which fits the chair example." + }, + { + "id": "eng-vocab-0060", + "type": "part_of_speech", + "target_word": "looming", + "prompt": { + "sentence": "The looming clouds made the football team hurry off the pitch." + }, + "question": "In this sentence, what part of speech is \"looming\"?", + "choices": [ + { + "text": "noun", + "correct": false + }, + { + "text": "verb", + "correct": false + }, + { + "text": "adverb", + "correct": false + }, + { + "text": "adjective", + "correct": true + }, + { + "text": "conjunction", + "correct": false + } + ], + "explanation": "Looming modifies clouds, so it is acting as an adjective in this sentence." + }, + { + "id": "eng-vocab-0061", + "type": "word_meaning", + "target_word": "abundant", + "prompt": {}, + "question": "What is the best meaning for the word 'abundant'?", + "choices": [ + { + "text": "Moving with high speed", + "correct": false + }, + { + "text": "Creating a loud noise", + "correct": false + }, + { + "text": "Present in great quantity", + "correct": true + }, + { + "text": "Hard to find or rare", + "correct": false + }, + { + "text": "Acting with great care", + "correct": false + } + ], + "explanation": "Great job! 'Abundant' means having plenty of something. If you have an abundant supply of snacks, you have a lot of them!" + }, + { + "id": "eng-vocab-0062", + "type": "word_meaning", + "target_word": "inevitable", + "prompt": {}, + "question": "Choose the correct definition for the word 'inevitable'.", + "choices": [ + { + "text": "Certain to happen and unable to be avoided", + "correct": true + }, + { + "text": "Completely invisible to the human eye", + "correct": false + }, + { + "text": "Happening by random chance or luck", + "correct": false + }, + { + "text": "Easily broken or damaged when dropped", + "correct": false + }, + { + "text": "Requiring a lot of physical energy", + "correct": false + } + ], + "explanation": "Well done! 'Inevitable' means something is bound to happen and cannot be prevented, like the sun rising in the morning." + }, + { + "id": "eng-vocab-0063", + "type": "word_meaning", + "target_word": "reluctant", + "prompt": {}, + "question": "What does it mean if someone is 'reluctant'?", + "choices": [ + { + "text": "They are famous for their musical talent", + "correct": false + }, + { + "text": "They are very excited and eager to start", + "correct": false + }, + { + "text": "They are highly skilled at solving problems", + "correct": false + }, + { + "text": "They are unwilling and hesitant to do something", + "correct": true + }, + { + "text": "They are known for being extremely polite", + "correct": false + } + ], + "explanation": "Spot on! Being 'reluctant' means you are hesitant or unwilling to do something, perhaps because you are nervous or unsure." + }, + { + "id": "eng-vocab-0064", + "type": "word_meaning", + "target_word": "precise", + "prompt": {}, + "question": "Which option best describes the meaning of 'precise'?", + "choices": [ + { + "text": "Messy, disorganized, and thrown together", + "correct": false + }, + { + "text": "Exact, accurate, and careful about details", + "correct": true + }, + { + "text": "Sweet, sugary, and delicious to eat", + "correct": false + }, + { + "text": "Very large, heavy, and difficult to lift", + "correct": false + }, + { + "text": "Dark, cloudy, and hard to see through", + "correct": false + } + ], + "explanation": "Correct! 'Precise' means being exact and highly accurate, with no room for error." + }, + { + "id": "eng-vocab-0065", + "type": "reverse_meaning", + "target_word": "ambiguous", + "prompt": { + "meaning": "Open to more than one interpretation; having a double meaning or being unclear." + }, + "question": "Which word best matches this definition?", + "choices": [ + { + "text": "obvious", + "correct": false + }, + { + "text": "colorful", + "correct": false + }, + { + "text": "harmless", + "correct": false + }, + { + "text": "frequent", + "correct": false + }, + { + "text": "ambiguous", + "correct": true + } + ], + "explanation": "Awesome! 'Ambiguous' means something is unclear or has more than one possible meaning, which can sometimes lead to confusion." + }, + { + "id": "eng-vocab-0066", + "type": "reverse_meaning", + "target_word": "versatile", + "prompt": { + "meaning": "Able to adapt or be adapted to many different functions or activities." + }, + "question": "What word is being described here?", + "choices": [ + { + "text": "fragile", + "correct": false + }, + { + "text": "stubborn", + "correct": false + }, + { + "text": "versatile", + "correct": true + }, + { + "text": "ignorant", + "correct": false + }, + { + "text": "wealthy", + "correct": false + } + ], + "explanation": "Nice work! A 'versatile' person or tool can easily switch between different tasks and handle them well." + }, + { + "id": "eng-vocab-0067", + "type": "reverse_meaning", + "target_word": "innovate", + "prompt": { + "meaning": "To make changes in something established, especially by introducing new methods, ideas, or products." + }, + "question": "Which word correctly matches this definition?", + "choices": [ + { + "text": "innovate", + "correct": true + }, + { + "text": "destroy", + "correct": false + }, + { + "text": "imitate", + "correct": false + }, + { + "text": "hesitate", + "correct": false + }, + { + "text": "decorate", + "correct": false + } + ], + "explanation": "Exactly! To 'innovate' means to invent or come up with fresh, creative ways of doing things." + }, + { + "id": "eng-vocab-0068", + "type": "reverse_meaning", + "target_word": "sequence", + "prompt": { + "meaning": "A particular order in which related events, movements, or things follow each other." + }, + "question": "Identify the word that fits this definition.", + "choices": [ + { + "text": "disaster", + "correct": false + }, + { + "text": "sequence", + "correct": true + }, + { + "text": "fraction", + "correct": false + }, + { + "text": "illusion", + "correct": false + }, + { + "text": "argument", + "correct": false + } + ], + "explanation": "Brilliant! A 'sequence' is the specific order in which things happen or are arranged, like the numbers 1, 2, 3, 4." + }, + { + "id": "eng-vocab-0069", + "type": "fill_in_blank", + "target_word": "evaluate", + "prompt": { + "sentence": "The teacher will ____ your final project based on your creativity and effort." + }, + "question": "Which word correctly fills in the blank?", + "choices": [ + { + "text": "confuse", + "correct": false + }, + { + "text": "abandon", + "correct": false + }, + { + "text": "pretend", + "correct": false + }, + { + "text": "evaluate", + "correct": true + }, + { + "text": "swallow", + "correct": false + } + ], + "explanation": "Perfect! To 'evaluate' is to judge or determine the value or quality of something. Teachers evaluate your work to see how well you did." + }, + { + "id": "eng-vocab-0070", + "type": "fill_in_blank", + "target_word": "contrast", + "prompt": { + "sentence": "In her essay, Sarah tried to ____ the two different characters to show how opposite they were." + }, + "question": "Select the best word to complete the sentence.", + "choices": [ + { + "text": "combine", + "correct": false + }, + { + "text": "forgive", + "correct": false + }, + { + "text": "capture", + "correct": false + }, + { + "text": "promise", + "correct": false + }, + { + "text": "contrast", + "correct": true + } + ], + "explanation": "Spot on! To 'contrast' means to compare two things in order to show their differences." + }, + { + "id": "eng-vocab-0071", + "type": "fill_in_blank", + "target_word": "chronological", + "prompt": { + "sentence": "The history textbook presents the events of the war in ____ order, starting from the first battle to the last." + }, + "question": "Which word correctly fits the blank?", + "choices": [ + { + "text": "alphabetical", + "correct": false + }, + { + "text": "chronological", + "correct": true + }, + { + "text": "ridiculous", + "correct": false + }, + { + "text": "mysterious", + "correct": false + }, + { + "text": "electrical", + "correct": false + } + ], + "explanation": "Excellent! 'Chronological' means arranging events in the exact order they occurred in time." + }, + { + "id": "eng-vocab-0072", + "type": "fill_in_blank", + "target_word": "significant", + "prompt": { + "sentence": "Winning the regional championship was a very ____ achievement for the young team." + }, + "question": "Choose the word that best completes the sentence.", + "choices": [ + { + "text": "invisible", + "correct": false + }, + { + "text": "temporary", + "correct": false + }, + { + "text": "significant", + "correct": true + }, + { + "text": "poisonous", + "correct": false + }, + { + "text": "forgetful", + "correct": false + } + ], + "explanation": "Well done! 'Significant' means something is important, meaningful, or large enough to be noticed." + }, + { + "id": "eng-vocab-0073", + "type": "alternative_word", + "target_word": "perspective", + "prompt": { + "sentence": "From the perspective of a bird flying high above, the sprawling city looks like a tiny, intricate maze." + }, + "question": "Which word could replace 'perspective' without significantly changing the sentence's meaning?", + "choices": [ + { + "text": "viewpoint", + "correct": true + }, + { + "text": "location", + "correct": false + }, + { + "text": "weather", + "correct": false + }, + { + "text": "distance", + "correct": false + }, + { + "text": "feathers", + "correct": false + } + ], + "explanation": "Fantastic! Your 'perspective' is your viewpoint, or the way you see and understand the things around you." + }, + { + "id": "eng-vocab-0074", + "type": "alternative_word", + "target_word": "context", + "prompt": { + "sentence": "You shouldn't judge his confusing statement without understanding the context in which it was spoken." + }, + "question": "Which word best replaces 'context' in this sentence?", + "choices": [ + { + "text": "volume", + "correct": false + }, + { + "text": "grammar", + "correct": false + }, + { + "text": "clothing", + "correct": false + }, + { + "text": "situation", + "correct": true + }, + { + "text": "weather", + "correct": false + } + ], + "explanation": "Great job! 'Context' refers to the situation or background information that helps explain an event or statement." + }, + { + "id": "eng-vocab-0075", + "type": "alternative_word", + "target_word": "isolate", + "prompt": { + "sentence": "The scientist needed to isolate the specific bacteria to study its effects on the plants." + }, + "question": "Choose a word that could replace 'isolate' without changing the meaning.", + "choices": [ + { + "text": "multiply", + "correct": false + }, + { + "text": "destroy", + "correct": false + }, + { + "text": "swallow", + "correct": false + }, + { + "text": "combine", + "correct": false + }, + { + "text": "separate", + "correct": true + } + ], + "explanation": "Perfect! To 'isolate' means to set apart or separate someone or something from others." + }, + { + "id": "eng-vocab-0076", + "type": "alternative_word", + "target_word": "justify", + "prompt": { + "sentence": "The student tried to justify his late homework by explaining that his internet had unexpectedly stopped working." + }, + "question": "Which word can replace 'justify' while keeping the meaning of the sentence?", + "choices": [ + { + "text": "cancel", + "correct": false + }, + { + "text": "defend", + "correct": true + }, + { + "text": "forget", + "correct": false + }, + { + "text": "punish", + "correct": false + }, + { + "text": "ignore", + "correct": false + } + ], + "explanation": "Awesome! To 'justify' is to defend or show that an action or decision is right or reasonable." + }, + { + "id": "eng-vocab-0077", + "type": "part_of_speech", + "target_word": "alternative", + "prompt": { + "sentence": "Since it was raining heavily, the teacher proposed an alternative plan for the school trip." + }, + "question": "What part of speech is the word 'alternative' as used in this sentence?", + "choices": [ + { + "text": "verb", + "correct": false + }, + { + "text": "adverb", + "correct": false + }, + { + "text": "adjective", + "correct": true + }, + { + "text": "conjunction", + "correct": false + }, + { + "text": "interjection", + "correct": false + } + ], + "explanation": "Correct! Here, 'alternative' is describing the noun 'plan', which means it functions as an adjective in this sentence." + }, + { + "id": "eng-vocab-0078", + "type": "part_of_speech", + "target_word": "hypothesis", + "prompt": { + "sentence": "Before conducting the chemistry experiment, the students had to write down a hypothesis." + }, + "question": "Identify the part of speech for the word 'hypothesis' in this sentence.", + "choices": [ + { + "text": "verb", + "correct": false + }, + { + "text": "adjective", + "correct": false + }, + { + "text": "preposition", + "correct": false + }, + { + "text": "noun", + "correct": true + }, + { + "text": "pronoun", + "correct": false + } + ], + "explanation": "Spot on! A 'hypothesis' is a thing or a proposed idea, which makes it a noun." + }, + { + "id": "eng-vocab-0079", + "type": "part_of_speech", + "target_word": "analyze", + "prompt": { + "sentence": "The detective had to carefully analyze the clues left at the scene to solve the mystery." + }, + "question": "What is the part of speech of the word 'analyze' in this context?", + "choices": [ + { + "text": "verb", + "correct": true + }, + { + "text": "noun", + "correct": false + }, + { + "text": "adjective", + "correct": false + }, + { + "text": "adverb", + "correct": false + }, + { + "text": "preposition", + "correct": false + } + ], + "explanation": "Brilliant! 'Analyze' is the action the detective is performing, which means it functions as a verb." + }, + { + "id": "eng-vocab-0080", + "type": "part_of_speech", + "target_word": "summarize", + "prompt": { + "sentence": "For tonight's homework, please read the first chapter and summarize it in one short paragraph." + }, + "question": "What part of speech is the word 'summarize' in this sentence?", + "choices": [ + { + "text": "noun", + "correct": false + }, + { + "text": "adjective", + "correct": false + }, + { + "text": "conjunction", + "correct": false + }, + { + "text": "interjection", + "correct": false + }, + { + "text": "verb", + "correct": true + } + ], + "explanation": "Excellent! 'Summarize' is an action word telling you what you need to do, so it is a verb." + } +] diff --git a/static/English/Vocabulary/word_bank.txt b/static/English/Vocabulary/word_bank.txt new file mode 100644 index 00000000..ea5aad99 --- /dev/null +++ b/static/English/Vocabulary/word_bank.txt @@ -0,0 +1,325 @@ +Gorge +scramble +moor +cairn +bivouacked +visor +prudent +courteous +tidings +belfry +uncanny +dozed +wigwam +coarse +solemn +trickled +forded +tribute +earnest +alighted +overcast +sustaining +looming +fusty +mischief +dismounted +serpent +distraught +vengeance +beckoned +dingy +splendour +moored +frail +outskirts +feeble +mumbling +glanced +heartily +doddering +splendid +stalked +ceased +persistent +frantic +quenched +astonishment +contentment +battlements +thicket +shrubbery +grimly +unmanageable +queer +indignantly +grubby +plumage +wrenched +prow +harbour +avenue +ceased +conspicuous +parley +enmity +deceive +reckoned +whizzing +quivering +treacherous +coracle +astern +vermilions +bulwark +valour +lee +groping +avenge +allegiance +bewitched +forlorn +sinister +battened +cataract +becalmed +patronizing +fathoms +oppressive +precipices +scree +laden +cask +embankment +convulsions +stealthily +mimicking +disquieting +shrill +dispatch +obstinate +unmitigated +considerable +relapses +inscription +teetotaller +grudge +gilded +briny +despairing +bulwarks +endeavours +flecked +courtly +rummaging +coronation +regent +usurping +oath +avenge +stature +keel +beseech +irritably +forecastle +boatswain +poltroon +rapier +starboard +uninhabited +re-conquering +over-lordship +wheedling +rigmarole +disbursed +overawed +fief +slovenly +postern +gauntlet +languid +dandified +abominable +raucous +forfeit +ingratiating +perpetually +liege +woebegone +innumerable +confounded +unseemly +bestowed +proclamation +henceforth +sulky +lineage +infallibly +counsel +hazard +effrontery +beseech +chafed +marshals +abate +madcap +cymbal +rear +steep +crowned +deliver +hawthorn +revellers +arithmetic +sentinel +bared +musty +carvings +chamber +burnt +cantrips +vermin +treason +tremulous +downright +bilge +slantwise +velvety +brooding +capering +confronted +gorge +cascades +chasms +bivouacked +lilting +sentries +battledores +headland +bracken +constellations +wizened +rueful +undergrowth +carcass +precipice +reproachfully +stronghold +seneschal +quiverful +battlement +hauberk +jibe +rowlocks +prow +rigging +gruelling +tapestried +anteroom +contingent +overruled +victuals +sorties +scanty +bivouac +indifference +aftermath +mourners +wrestled +investigation +cabinet +bulgy +glade +magnificent +subterranean +smithy +ravine +contemptuously +prophet +unreservedly +martial +earnest +usurper +persuaded +fervent +tempest +creaked +feeble +changeable +queerest +renegade +weeded +contrive +antechamber +venison +tiresome +miserable +indistinctly +remnant +despaired +shudder +quarrelled +stern +twitched +corporal +garn +floundered +obliged +pavenders +dreadfully +ravenously +rebellion +quiver +forsaken +carbuncle +tapestry +dais +perish +coronet +notorious +indignant +audacious +reeking +dictator +perspicacious +pristine +amiable +benevolent +fathom +apprehensive +nonchalant +tangible +promontory +bad +grievous +swagger +irresolute +abdicating +draughts +gale +prophecy +aloft +marooned +coronets +pinnacles +expanse +abundant +inevitable +reluctant +precise +ambiguous +versatile +innovate +sequence +evaluate +contrast +chronological +significant +perspective +context +isolate +justify +alternative +hypothesis +analyze +summarize diff --git a/static/api_spec.yaml b/static/api_spec.yaml new file mode 100644 index 00000000..6465983c --- /dev/null +++ b/static/api_spec.yaml @@ -0,0 +1,128 @@ +openapi: 3.1.0 +info: + title: Hello World API + description: A simple API to return a Hello World message + version: "1.0" +servers: + - url: https://sharp-moria-avon18-b017b82a.koyeb.app/ +paths: + /: + get: + summary: Returns a Hello World message + operationId: helloWorld + responses: + 200: + description: Successful response + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Hello, World!" + 401: + description: Unauthorized access + /tariff: + get: + summary: Calculate and return a future start time when the appliance should start in order to minimize energy cost + operationId: cheapestStartTime + parameters: + - in: query + name: numHours + schema: + type: integer + required: true + description: Number of hours of the appliance going to run for + responses: + 200: + description: Successful response with the future start time + content: + application/json: + schema: + type: object + properties: + startTime: + type: string + example: "2024-08-10 17:00:00" + 400: + description: Bad request response when input validation fails + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "numHours parameter is required" + 401: + description: Unauthorized access + /demo_status: + get: + summary: Returns the status of connections + operationId: demoStatus + parameters: + - in: query + name: type + schema: + type: string + enum: [FIX, MQ, SFTP, ALL] + required: true + description: The type of connection to check + responses: + 200: + description: Successful response confirming all connections are up + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "All your connections are up and running" + 400: + description: Bad request response when input validation fails + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Invalid connection type. Allowed values are FIX, MQ, SFTP, ALL." + /demo_details: + get: + summary: Returns the details of a specific connection + operationId: demoDetails + parameters: + - in: query + name: id + schema: + type: string + required: true + description: The ID of the connection to check + responses: + 200: + description: Successful response with connection details + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Connection 123 is up" + lastConnectionTime: + type: string + format: date-time + example: "2025-01-08T12:00:00Z" + 400: + description: Bad request response when input validation fails + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "ID parameter is required" diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 00000000..a1d350c0 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,663 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +header { + background-color: #333; + color: #fff; + padding: 1em 0; + text-align: center; +} + +nav { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1em; +} + +.logo { + font-size: 1.5em; +} + +.menu { + display: flex; + gap: 1em; +} + +.menu a { + color: #fff; + text-decoration: none; +} + +.hamburger { + display: none; + cursor: pointer; + font-size: 1.5em; +} + +main { + padding: 2em; + text-align: center; +} + +footer { + background-color: #333; + color: #fff; + text-align: center; + padding: 1em 0; + position: fixed; + bottom: 0; + width: 100%; +} + +.menu.open { + display: flex !important; /* Ensure that the menu displays as a flex container when open */ +} + + +@media (max-width: 768px) { + .menu { + display: none; + flex-direction: column; + background-color: #333; + position: absolute; + top: 50px; + right: 0; + width: 100%; + text-align: center; + } + + .menu a { + padding: 1em; + border-top: 1px solid #fff; + } + + .hamburger { + display: block; + } +} + +.octopus-page { + min-height: 100vh; + background: + radial-gradient(circle at top left, rgba(64, 145, 108, 0.18), transparent 32%), + linear-gradient(180deg, #f3f7ef 0%, #edf2f7 100%); + color: #1f2933; +} + +.octopus-layout { + max-width: 1080px; + margin: 0 auto; + padding: 2rem 1.25rem 4rem; + text-align: left; +} + +.octopus-hero { + margin-bottom: 1.5rem; +} + +.octopus-kicker { + margin: 0 0 0.5rem; + font-size: 0.85rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #2f855a; +} + +.octopus-hero h1 { + margin: 0; + font-size: clamp(2rem, 4vw, 3.5rem); + line-height: 1.05; +} + +.octopus-summary { + max-width: 760px; + margin: 1rem 0 0; + font-size: 1rem; + line-height: 1.6; + color: #52606d; +} + +.octopus-banner, +.octopus-card { + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 20px; + box-shadow: 0 18px 40px rgba(31, 41, 51, 0.08); + backdrop-filter: blur(6px); +} + +.octopus-banner { + display: grid; + gap: 0.35rem; + margin-bottom: 1.5rem; + padding: 1rem 1.25rem; +} + +.octopus-banner-error { + border-color: rgba(185, 28, 28, 0.2); + color: #8a1c1c; + background: rgba(255, 237, 237, 0.92); +} + +.octopus-card { + overflow: hidden; +} + +.octopus-table-header { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: end; + padding: 1.25rem 1.25rem 0; +} + +.octopus-table-header h2, +.octopus-table-header p { + margin: 0; +} + +.octopus-table-header p { + color: #52606d; + max-width: 28rem; + line-height: 1.5; +} + +.octopus-table { + padding: 1.25rem; +} + +.octopus-table-row { + display: grid; + grid-template-columns: 1fr 1.3fr 1.3fr 1fr 1fr; + gap: 1rem; + align-items: center; + padding: 1rem 0; + border-bottom: 1px solid rgba(15, 23, 42, 0.08); +} + +.octopus-table-row:last-child { + border-bottom: 0; +} + +.octopus-table-row-head { + padding-top: 0; + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + color: #52606d; +} + +.octopus-cell { + min-width: 0; + line-height: 1.5; +} + +.octopus-pill { + display: inline-flex; + align-items: center; + padding: 0.35rem 0.75rem; + border-radius: 999px; + background: #e6fffa; + color: #234e52; + font-weight: 700; +} + +.octopus-pill-credit { + background: #d9f99d; + color: #365314; +} + +.octopus-inline-error { + color: #8a1c1c; +} + +.octopus-slot-details { + grid-column: 1 / -1; +} + +.octopus-slot-details details { + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: 12px; + background: rgba(248, 250, 252, 0.9); +} + +.octopus-slot-details summary { + cursor: pointer; + padding: 0.65rem 0.8rem; + font-weight: 700; + color: #334155; +} + +.octopus-slot-table-wrap { + padding: 0 0.8rem 0.8rem; +} + +.octopus-slot-table { + width: 100%; + border-collapse: collapse; + font-size: 0.92rem; +} + +.octopus-slot-table th, +.octopus-slot-table td { + text-align: left; + padding: 0.45rem 0.3rem; + border-top: 1px solid rgba(15, 23, 42, 0.08); +} + +.octopus-slot-table th { + font-size: 0.78rem; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #52606d; +} + +@media (max-width: 900px) { + .octopus-table-header { + flex-direction: column; + align-items: start; + } + + .octopus-table-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 640px) { + .octopus-layout { + padding: 1.25rem 1rem 3rem; + } + + .octopus-table { + padding: 1rem; + } + + .octopus-table-row-head { + display: none; + } + + .octopus-table-row { + grid-template-columns: 1fr; + gap: 0.45rem; + padding: 1rem; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 16px; + margin-bottom: 0.85rem; + background: rgba(255, 255, 255, 0.92); + } + + .octopus-table-row:last-child { + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + } + + .octopus-cell::before { + content: attr(data-label); + display: block; + margin-bottom: 0.15rem; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #52606d; + } +} + +.vocab-page { + min-height: 100vh; + background: #f7f8fa; + color: #172033; +} + +.vocab-layout { + max-width: 980px; + margin: 0 auto; + padding: 2rem 1.25rem 4rem; + text-align: left; +} + +.vocab-header { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: end; + margin-bottom: 1.25rem; +} + +.vocab-kicker { + margin: 0 0 0.4rem; + color: #2f6f6c; + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.vocab-header h1 { + margin: 0; + font-size: 2.3rem; + line-height: 1.1; +} + +.vocab-controls { + display: grid; + grid-template-columns: auto minmax(150px, 210px) auto; + gap: 0.75rem; + align-items: center; + padding: 0.8rem; + border: 1px solid #d7dde7; + border-radius: 8px; + background: #ffffff; +} + +.vocab-count-control { + display: grid; + gap: 0.1rem; + color: #5a6475; + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; +} + +.vocab-count-control strong { + color: #172033; + font-size: 1.15rem; +} + +.vocab-range { + accent-color: #2f6f6c; +} + +.vocab-button { + min-height: 2.5rem; + border: 0; + border-radius: 7px; + padding: 0.55rem 0.9rem; + background: #204f75; + color: #ffffff; + cursor: pointer; + font: inherit; + font-weight: 700; +} + +.vocab-button:hover { + background: #173b59; +} + +.vocab-button:disabled { + cursor: wait; + opacity: 0.65; +} + +.vocab-button-secondary { + background: #2f6f6c; +} + +.vocab-button-secondary:hover { + background: #245957; +} + +.vocab-button-small { + min-height: 2.15rem; + padding: 0.4rem 0.75rem; +} + +.vocab-type-control { + min-width: 220px; + border: 0; + padding: 0; + margin: 0; + color: #5a6475; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; +} + +.vocab-type-control legend { + padding: 0; + margin-bottom: 0.3rem; +} + +.vocab-type-picker { + border: 1px solid #c6d1de; + border-radius: 7px; + background: #fff; +} + +.vocab-type-picker summary { + cursor: pointer; + padding: 0.5rem 0.6rem; + color: #172033; + text-transform: none; + letter-spacing: 0; + font-size: 0.9rem; +} + +.vocab-type-options { + display: grid; + gap: 0.4rem; + padding: 0.2rem 0.6rem 0.6rem; +} + +.vocab-type-option { + display: flex; + align-items: center; + gap: 0.45rem; + color: #172033; + text-transform: none; + letter-spacing: 0; + font-size: 0.85rem; + font-weight: 500; +} + + +.vocab-alert, +.vocab-empty { + margin: 1rem 0; + padding: 1rem; + border: 1px solid #efb8b8; + border-radius: 8px; + background: #fff1f1; + color: #8a1c1c; +} + +.vocab-question-list { + display: grid; + gap: 1rem; +} + +.vocab-question { + padding: 1.1rem; + border: 1px solid #d7dde7; + border-radius: 8px; + background: #ffffff; +} + +.vocab-question.is-correct { + border-color: #8fc6a7; + background: #f2fbf5; +} + +.vocab-question.is-retry-correct { + border-color: #88b7e6; + background: #f1f7ff; +} + +.vocab-question.is-wrong { + border-color: #dfb15b; + background: #fff9ed; +} + +.vocab-question.is-retry-still-wrong { + animation: vocab-shake 0.28s ease-in-out; +} + +@keyframes vocab-shake { + 0% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 50% { transform: translateX(5px); } + 75% { transform: translateX(-3px); } + 100% { transform: translateX(0); } +} + + +.vocab-question-head { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + margin-bottom: 0.7rem; +} + +.vocab-number, +.vocab-result { + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.vocab-number { + color: #5a6475; +} + +.vocab-result { + color: #204f75; +} + +.vocab-question.is-correct .vocab-result { + color: #1f7a45; +} + +.vocab-question.is-retry-correct .vocab-result { + color: #1d5fa8; +} + +.vocab-question.is-wrong .vocab-result { + color: #9a5a00; +} + +.vocab-prompt { + margin: 0 0 0.6rem; + color: #39455a; + line-height: 1.5; +} + +.vocab-question h2 { + margin: 0 0 0.9rem; + font-size: 1.1rem; + line-height: 1.4; +} + +.vocab-choices { + display: grid; + gap: 0.55rem; +} + +.vocab-choice { + display: grid; + grid-template-columns: 1.1rem 1fr; + gap: 0.6rem; + align-items: start; + padding: 0.7rem; + border: 1px solid #e0e5ed; + border-radius: 7px; + background: #fbfcfe; + cursor: pointer; + line-height: 1.4; +} + +.vocab-choice:hover { + border-color: #9eb8c6; + background: #f4f8fa; +} + +.vocab-choice.is-selected { + border-color: #204f75; + background: #e8f2f8; + box-shadow: inset 4px 0 0 #204f75; +} + +.vocab-choice input { + margin-top: 0.2rem; + accent-color: #204f75; +} + +.vocab-retry-row { + min-height: 2.15rem; + margin-top: 0.8rem; +} + +.vocab-submit-row { + display: flex; + gap: 0.8rem; + align-items: center; + margin-top: 1.25rem; +} + +.vocab-submit-status { + color: #5a6475; + font-weight: 700; +} + +.vocab-score { + min-width: 3.5rem; + font-size: 1.1rem; + font-weight: 800; +} + +.vocab-score[data-tone="high"] { + color: #1f7a45; +} + +.vocab-score[data-tone="mid"] { + color: #b36b00; +} + +.vocab-score[data-tone="low"] { + color: #a33b2f; +} + +.vocab-submit-status[data-tone="success"] { + color: #1f7a45; +} + +.vocab-submit-status[data-tone="error"] { + color: #a33b2f; +} + +.vocab-submit-status[data-tone="pending"] { + color: #204f75; +} + +@media (max-width: 760px) { + .vocab-header { + display: grid; + align-items: start; + } + + .vocab-controls { + grid-template-columns: 1fr; + } +} + +@media (max-width: 520px) { + .vocab-layout { + padding: 1.25rem 1rem 3rem; + } + + .vocab-header h1 { + font-size: 1.8rem; + } + + .vocab-submit-row { + display: grid; + } +} diff --git a/static/html/fixreft.html b/static/html/fixreft.html new file mode 100644 index 00000000..403bea3e --- /dev/null +++ b/static/html/fixreft.html @@ -0,0 +1,396 @@ + + + + + + Advanced FIX Message Parser + + + +
+
+

FIX Message Parser

+ +
+

FIX Dictionary Location

+
+ +
+
+ +
+

FIX Message

+

Enter a raw FIX message payload below. The message will be parsed into a human-readable format.

+ + +
+ Separator: + + + +
+ + + +

Parsed Output

+
+
+
+
+ + + + \ No newline at end of file diff --git a/static/js/scripts.js b/static/js/scripts.js new file mode 100644 index 00000000..97df1fba --- /dev/null +++ b/static/js/scripts.js @@ -0,0 +1,310 @@ +document.addEventListener('DOMContentLoaded', () => { + // Keep the legacy mobile menu script harmless on pages without that header. + const hamburger = document.getElementById('hamburger'); + const menu = document.getElementById('menu'); + + if (hamburger && menu) { + hamburger.addEventListener('click', () => { + menu.classList.toggle('open'); + }); + } +}); + +document.addEventListener('DOMContentLoaded', () => { + // The vocab quiz is rendered from JSON embedded by Flask, then regenerated + // from /vocab/questions without a full page reload. + const quiz = document.getElementById('vocab-quiz'); + const questionList = document.getElementById('vocab-question-list'); + const countInput = document.getElementById('vocab-count'); + const countLabel = document.getElementById('vocab-count-label'); + const regenerateButton = document.getElementById('vocab-regenerate'); + const scoreElement = document.getElementById('vocab-score'); + const submitStatus = document.getElementById('vocab-submit-status'); + const submitButton = document.getElementById('vocab-submit'); + const typeContainer = document.getElementById('vocab-types'); + const typeSummary = document.getElementById('vocab-type-summary'); + + if (!quiz || !questionList || !countInput || !countLabel || !regenerateButton || !scoreElement || !submitStatus || !submitButton || !typeContainer || !typeSummary) { + return; + } + + const allowedCounts = window.vocabAllowedCounts || [5, 10, 15, 20, 25, 30]; + const allowedTypes = window.vocabAllowedTypes || []; + let questions = window.vocabInitialQuestions || []; + + const getSelectedCount = () => allowedCounts[Number(countInput.value)] || 10; + + const getTypeInputs = () => Array.from(typeContainer.querySelectorAll('input[type="checkbox"]')); + + const getSelectedTypes = () => getTypeInputs().filter((input) => input.checked).map((input) => input.value); + + const selectAllTypes = () => { + getTypeInputs().forEach((input) => { + input.checked = true; + }); + updateTypeSummary(); + }; + + const updateTypeSummary = () => { + const selectedTypes = getSelectedTypes(); + + if (!selectedTypes.length) { + typeSummary.textContent = 'No type selected'; + return; + } + + if (selectedTypes.length === allowedTypes.length) { + typeSummary.textContent = 'All types selected'; + return; + } + + typeSummary.textContent = `${selectedTypes.length} types selected`; + }; + + // Submit feedback and score use separate labels so either can update alone. + const setStatus = (message, tone = '') => { + submitStatus.textContent = message; + submitStatus.dataset.tone = tone; + }; + + const setScore = (correctCount, totalCount) => { + const percentage = totalCount ? correctCount / totalCount : 0; + let tone = 'low'; + + if (percentage >= 0.8) { + tone = 'high'; + } else if (percentage >= 0.6) { + tone = 'mid'; + } + + scoreElement.textContent = `${correctCount}/${totalCount}`; + scoreElement.dataset.tone = tone; + }; + + const clearScore = () => { + scoreElement.textContent = ''; + scoreElement.dataset.tone = ''; + }; + + const renderPrompt = (prompt) => { + const parts = []; + + if (prompt.meaning) { + parts.push(`

Meaning: ${escapeHtml(prompt.meaning)}

`); + } + + if (prompt.sentence) { + parts.push(`

${escapeHtml(prompt.sentence)}

`); + } + + return parts.join(''); + }; + + const renderQuestions = () => { + questionList.innerHTML = ''; + setStatus(''); + clearScore(); + + if (!questions.length) { + questionList.innerHTML = '
No vocabulary questions are available.
'; + return; + } + + const fragment = document.createDocumentFragment(); + + questions.forEach((question, index) => { + const article = document.createElement('article'); + article.className = 'vocab-question'; + article.dataset.questionIndex = String(index); + article.dataset.targetWord = question.target_word || ''; + + const choices = (question.choices || []).map((choice, choiceIndex) => ` + + `).join(''); + + article.innerHTML = ` +
+ Question ${index + 1} + +
+ ${renderPrompt(question.prompt || {})} +

${escapeHtml(question.question || '')}

+
${choices}
+
+ +
+ `; + + fragment.appendChild(article); + }); + + questionList.appendChild(fragment); + }; + + // Marking deliberately never reveals the correct answer for wrong attempts. + const markQuestion = (questionElement, mode = 'submit') => { + const selectedChoice = questionElement.querySelector('input[type="radio"]:checked'); + const result = questionElement.querySelector('.vocab-result'); + const retryButton = questionElement.querySelector('.vocab-retry'); + const isCorrect = selectedChoice && selectedChoice.dataset.correct === 'true'; + const wasRetryCorrect = questionElement.classList.contains('is-retry-correct'); + + questionElement.classList.remove('is-correct', 'is-wrong', 'is-retry-correct', 'is-retry-still-wrong'); + + if (isCorrect) { + if (mode === 'retry' || wasRetryCorrect) { + questionElement.classList.add('is-retry-correct'); + result.textContent = 'Retry correct'; + } else { + questionElement.classList.add('is-correct'); + result.textContent = 'Correct'; + } + + retryButton.hidden = true; + return true; + } + + questionElement.classList.add('is-wrong'); + result.textContent = selectedChoice ? 'Try again' : 'No answer selected'; + + if (mode === 'retry' && selectedChoice) { + questionElement.classList.remove('is-retry-still-wrong'); + void questionElement.offsetWidth; + questionElement.classList.add('is-retry-still-wrong'); + } + retryButton.hidden = false; + return false; + }; + + // Native radio state is subtle, so mirror it onto the label for visibility. + const updateSelectedChoice = (input) => { + const choices = input.closest('.vocab-choices'); + + choices.querySelectorAll('.vocab-choice').forEach((choice) => { + choice.classList.toggle('is-selected', choice.contains(input)); + }); + }; + + // This dummy call lets the server receive first-attempt misses while the + // browser continues showing the mark result immediately. + const sendFeedback = async (missedTargetWords) => { + setStatus('Sending feedback...', 'pending'); + + try { + const response = await fetch('/vocab/feedback', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ missed_target_words: missedTargetWords }), + }); + + if (!response.ok) { + throw new Error('Feedback failed'); + } + + setStatus('Feedback received', 'success'); + } catch (error) { + setStatus('Feedback failed', 'error'); + } + }; + + countInput.addEventListener('input', () => { + countLabel.textContent = String(getSelectedCount()); + }); + + typeContainer.addEventListener('change', (event) => { + if (event.target.matches('input[type="checkbox"]')) { + updateTypeSummary(); + } + }); + + updateTypeSummary(); + + regenerateButton.addEventListener('click', async () => { + const count = getSelectedCount(); + let selectedTypes = getSelectedTypes(); + + if (!selectedTypes.length) { + selectAllTypes(); + selectedTypes = [...allowedTypes]; + } + + regenerateButton.disabled = true; + submitButton.disabled = false; + setStatus('Loading new questions...', 'pending'); + + try { + const typeQuery = encodeURIComponent(selectedTypes.join(',')); + const response = await fetch(`/vocab/questions?count=${count}&types=${typeQuery}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Could not load questions'); + } + + questions = data.questions || []; + renderQuestions(); + setStatus(`Loaded ${questions.length} questions`, 'success'); + } catch (error) { + setStatus('Could not regenerate questions', 'error'); + } finally { + regenerateButton.disabled = false; + } + }); + + quiz.addEventListener('click', (event) => { + if (!event.target.classList.contains('vocab-retry')) { + return; + } + + const questionElement = event.target.closest('.vocab-question'); + markQuestion(questionElement, 'retry'); + }); + + quiz.addEventListener('change', (event) => { + if (event.target.matches('.vocab-choice input[type="radio"]')) { + updateSelectedChoice(event.target); + } + }); + + quiz.addEventListener('submit', (event) => { + event.preventDefault(); + + const missedTargetWords = []; + const questionElements = questionList.querySelectorAll('.vocab-question'); + let correctCount = 0; + + questionElements.forEach((questionElement) => { + const isCorrect = markQuestion(questionElement); + + if (isCorrect) { + correctCount += 1; + } + + if (!isCorrect && questionElement.dataset.targetWord) { + missedTargetWords.push(questionElement.dataset.targetWord); + } + }); + + setScore(correctCount, questionElements.length); + sendFeedback(missedTargetWords); + submitButton.disabled = true; + }); + + renderQuestions(); +}); + +const escapeHtml = (value) => { + const element = document.createElement('div'); + element.textContent = value || ''; + return element.innerHTML; +}; diff --git a/static/octopus_spec.yaml b/static/octopus_spec.yaml new file mode 100644 index 00000000..227bce39 --- /dev/null +++ b/static/octopus_spec.yaml @@ -0,0 +1,43 @@ +openapi: 3.1.0 +info: + title: Octopus Energy Electricity Tariffs API + description: API to retrieve electricity tariff details for specific products. + version: 1.0.0 +servers: + - url: https://api.octopus.energy/v1 + description: Octopus Energy API server +paths: + /products/AGILE-FLEX-22-11-25/electricity-tariffs/E-1R-AGILE-FLEX-22-11-25-C/standard-unit-rates/: + get: + operationId: getStandardUnitRates + summary: Retrieve standard unit rates for a specific electricity tariff. + responses: + '200': + description: A list of standard unit rates for the tariff. + content: + application/json: + schema: + type: array + items: + type: object + properties: + value_exc_vat: + type: number + format: float + value_inc_vat: + type: number + format: float + valid_from: + type: string + format: date-time + valid_to: + type: string + format: date-time + security: + - basicAuth: [] + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic diff --git a/tariff_utils.py b/tariff_utils.py new file mode 100644 index 00000000..04ae5588 --- /dev/null +++ b/tariff_utils.py @@ -0,0 +1,136 @@ +from datetime import datetime, timedelta + +import pytz +import requests + +SCAN_HOURS = 12 +OCTOPUS_TARIFF_URL = ( + "https://api.octopus.energy/v1/products/AGILE-24-10-01/" + "electricity-tariffs/E-1R-AGILE-24-10-01-C/standard-unit-rates/" +) +LONDON_TZ = pytz.timezone("Europe/London") + + +def _parse_duration_slots(duration_hours): + slot_count = int(duration_hours * 2) + if slot_count <= 0 or slot_count != duration_hours * 2: + raise ValueError("Duration must be a positive multiple of 0.5 hours") + return slot_count + + +def _fetch_available_slots(api_key, scan_hours=SCAN_HOURS): + if not api_key: + raise RuntimeError("OCTOPUS_KEY is not configured") + + try: + response = requests.get(OCTOPUS_TARIFF_URL, auth=(api_key, ""), timeout=10) + except requests.RequestException as error: + raise RuntimeError(f"Octopus API request failed: {error}") from error + + print(f"Returned status code: {response.status_code}") + + if response.status_code != 200: + raise RuntimeError(f"Octopus API error: {response.status_code} - {response.reason}") + + now_utc = datetime.now(pytz.UTC) + begin_time = now_utc + timedelta(minutes=30) + end_time = begin_time + timedelta(hours=scan_hours) + + available_slots = [] + for slot in response.json().get("results", []): + valid_from = datetime.fromisoformat(slot["valid_from"].replace("Z", "+00:00")) + valid_to = datetime.fromisoformat(slot["valid_to"].replace("Z", "+00:00")) + + if valid_from >= begin_time and valid_to <= end_time: + available_slots.append( + { + "valid_from": valid_from, + "valid_to": valid_to, + "tariff": slot["value_inc_vat"], + } + ) + + available_slots.sort(key=lambda slot: slot["valid_from"]) + return available_slots + + +def find_best_tariff_window(duration_hours, api_key, available_slots=None): + required_slots = _parse_duration_slots(duration_hours) + slots = available_slots if available_slots is not None else _fetch_available_slots(api_key) + + if len(slots) < required_slots: + raise ValueError(f"Not enough tariff slots available for a {duration_hours}-hour window") + + best_window = None + best_total_tariff = float("inf") + + for index in range(len(slots) - required_slots + 1): + consecutive_slots = slots[index:index + required_slots] + is_consecutive = all( + consecutive_slots[position]["valid_from"] == consecutive_slots[position - 1]["valid_to"] + for position in range(1, required_slots) + ) + + if not is_consecutive: + print(f"Not consecutive: {consecutive_slots[0]['valid_from']}") + continue + + total_tariff = sum(slot["tariff"] for slot in consecutive_slots) + if total_tariff < best_total_tariff: + best_total_tariff = total_tariff + best_window = { + "duration_hours": duration_hours, + "start_time": consecutive_slots[0]["valid_from"], + "end_time": consecutive_slots[-1]["valid_to"], + "total_tariff": total_tariff, + "average_tariff": total_tariff / required_slots, + "slots": [ + { + "start_time": slot["valid_from"], + "end_time": slot["valid_to"], + "tariff": slot["tariff"], + } + for slot in consecutive_slots + ], + } + print( + "BEST TIMESLOT FOUND at " + f"[{best_window['start_time']}] for [{best_window['total_tariff']}]." + ) + + if best_window is None: + raise ValueError(f"No continuous tariff window found for {duration_hours} hours") + + return best_window + + +def get_best_tariff_windows(duration_hours_list, api_key): + available_slots = _fetch_available_slots(api_key) + results = [] + + for duration_hours in duration_hours_list: + try: + results.append(find_best_tariff_window(duration_hours, api_key, available_slots)) + except ValueError as error: + results.append( + { + "duration_hours": duration_hours, + "error": str(error), + } + ) + + return results + + +def calculate_start_time(num_hours, api_key): + """ + Calculate the cheapest start time from now for a given duration. + + :param num_hours: int|float - Duration in hours, in 0.5-hour increments + :return: str | None - Start time formatted as 'YYYY-MM-DD HH:MM:SS' + """ + + best_window = find_best_tariff_window(num_hours, api_key) + best_start_time_uk = best_window["start_time"].astimezone(LONDON_TZ) + return best_start_time_uk.strftime("%Y-%m-%d %H:%M:%S") + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 00000000..8c4771c5 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,32 @@ + + + + + + {% block title %}My Website{% endblock %} + + + + +
+ +
+
+ {% block content %}{% endblock %} +
+
+

© 2024 My Website

+
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..90e2be05 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block title %}Home - My Website{% endblock %} + +{% block content %} +
+

Welcome to My Personal Website

+

This is the home page content.

+
+{% endblock %} diff --git a/templates/octopus.html b/templates/octopus.html new file mode 100644 index 00000000..80276422 --- /dev/null +++ b/templates/octopus.html @@ -0,0 +1,108 @@ + + + + + + Octopus Tariff Windows + + + +
+
+

Octopus Agile

+

Cheapest continuous windows in the next 12 hours

+

+ Each row shows the best upcoming continuous time block for that duration. + Negative tariff totals mean you are being credited to use electricity during that window. +

+
+ + {% if page_error %} +
+ Tariff data unavailable. + {{ page_error }} +
+ {% endif %} + +
+
+

Best windows

+

Raw Octopus units are shown as returned from the tariff feed.

+
+ +
+
+
Duration
+
Start
+
End
+
Total tariff
+
Average slot tariff
+
+ + {% for row in rows %} +
+
+ {{ row.duration_label }} +
+ + {% if row.error %} +
+ Unable to calculate +
+
+ Unable to calculate +
+
+ {{ row.error }} +
+
+ {{ row.error }} +
+ {% else %} +
+ {{ row.start_label }} +
+
+ {{ row.end_label }} +
+
+ + {{ row.total_tariff_label }} + +
+
+ {{ row.average_tariff_label }} +
+
+
+ Half-hour tariff slots in this window +
+ + + + + + + + + + {% for slot in row.slot_details %} + + + + + + {% endfor %} + +
StartEndTariff
{{ slot.start_label }}{{ slot.end_label }}{{ slot.tariff_label }}
+
+
+
+ {% endif %} +
+ {% endfor %} +
+
+
+ + diff --git a/templates/vocab.html b/templates/vocab.html new file mode 100644 index 00000000..ece904f3 --- /dev/null +++ b/templates/vocab.html @@ -0,0 +1,73 @@ + + + + + + Vocabulary Practice + + + + + +
+
+
+

English Vocabulary

+

Practice quiz

+
+
+ + +
+ Question types +
+ All types selected +
+ {% for question_type in allowed_types %} + + {% endfor %} +
+
+
+ +
+
+ + {% if page_error %} + + {% endif %} + +
+
+
+ + + +
+
+
+ +