Spaces:
Running
Running
Commit
Β·
53d138c
0
Parent(s):
feat: clean cloze reader for HuggingFace Space
Browse filesSimple cloze reading app with progressive levels and no external dependencies.
- .gitignore +61 -0
- .parcelrc +7 -0
- .postcssrc.json +5 -0
- README.md +38 -0
- app.py +12 -0
- index.html +211 -0
- requirements.txt +2 -0
.gitignore
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Dependencies
|
2 |
+
node_modules/
|
3 |
+
npm-debug.log*
|
4 |
+
yarn-debug.log*
|
5 |
+
yarn-error.log*
|
6 |
+
|
7 |
+
# Build outputs
|
8 |
+
dist/
|
9 |
+
.parcel-cache/
|
10 |
+
|
11 |
+
# Environment variables
|
12 |
+
.env
|
13 |
+
.env.local
|
14 |
+
.env.development.local
|
15 |
+
.env.test.local
|
16 |
+
.env.production.local
|
17 |
+
|
18 |
+
# IDE and editor files
|
19 |
+
.vscode/
|
20 |
+
.idea/
|
21 |
+
*.swp
|
22 |
+
*.swo
|
23 |
+
*~
|
24 |
+
|
25 |
+
# OS generated files
|
26 |
+
.DS_Store
|
27 |
+
.DS_Store?
|
28 |
+
._*
|
29 |
+
.Spotlight-V100
|
30 |
+
.Trashes
|
31 |
+
ehthumbs.db
|
32 |
+
Thumbs.db
|
33 |
+
|
34 |
+
# Logs
|
35 |
+
logs
|
36 |
+
*.log
|
37 |
+
|
38 |
+
# Runtime data
|
39 |
+
pids
|
40 |
+
*.pid
|
41 |
+
*.seed
|
42 |
+
*.pid.lock
|
43 |
+
|
44 |
+
# Optional npm cache directory
|
45 |
+
.npm
|
46 |
+
|
47 |
+
# Optional eslint cache
|
48 |
+
.eslintcache
|
49 |
+
|
50 |
+
# Coverage directory used by tools like istanbul
|
51 |
+
coverage/
|
52 |
+
|
53 |
+
# Temporary folders
|
54 |
+
tmp/
|
55 |
+
temp/
|
56 |
+
.aider*
|
57 |
+
|
58 |
+
# Planning documents
|
59 |
+
HF_DATASET_INTEGRATION_PLAN.md
|
60 |
+
*_PLAN.md
|
61 |
+
IMPLEMENTATION_*.md
|
.parcelrc
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "@parcel/config-default",
|
3 |
+
"transformers": {
|
4 |
+
"*.{js,mjs,jsx,cjs,ts,tsx}": ["@parcel/transformer-js"],
|
5 |
+
"*.css": ["@parcel/transformer-css"]
|
6 |
+
}
|
7 |
+
}
|
.postcssrc.json
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"plugins": {
|
3 |
+
"tailwindcss": {}
|
4 |
+
}
|
5 |
+
}
|
README.md
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Cloze Reader
|
3 |
+
emoji: π
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: purple
|
6 |
+
sdk: docker
|
7 |
+
pinned: false
|
8 |
+
---
|
9 |
+
|
10 |
+
# Cloze Reader
|
11 |
+
|
12 |
+
An interactive cloze reading practice application with AI-powered assistance. Practice reading comprehension by filling in blanks in classic literature passages.
|
13 |
+
|
14 |
+
## Features
|
15 |
+
|
16 |
+
- **Progressive Level System**: Start with 1 blank, advance to 2-3 blanks as you improve
|
17 |
+
- **Smart Hints**: Get word length, first letter, and contextual clues
|
18 |
+
- **AI Chat Help**: Click π¬ for intelligent hints about any blank
|
19 |
+
- **Classic Literature**: Passages from Project Gutenberg's collection
|
20 |
+
- **Level-Appropriate Challenges**: Hints adapt based on your current level
|
21 |
+
|
22 |
+
## How to Use
|
23 |
+
|
24 |
+
1. Read the passage and literary context
|
25 |
+
2. Fill in the blank(s) with appropriate words
|
26 |
+
3. Use hints or chat help if needed
|
27 |
+
4. Submit to see your results and advance levels
|
28 |
+
5. Continue practicing with new passages
|
29 |
+
|
30 |
+
## Level System
|
31 |
+
|
32 |
+
- **Levels 1-2**: 1 blank, hints show first and last letter
|
33 |
+
- **Levels 3-4**: 2 blanks, hints show first letter only
|
34 |
+
- **Level 5+**: 3 blanks, first letter hints
|
35 |
+
|
36 |
+
## Technology
|
37 |
+
|
38 |
+
Built with vanilla JavaScript, powered by AI for intelligent word selection and contextual assistance.
|
app.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from fastapi.responses import FileResponse
|
3 |
+
|
4 |
+
app = FastAPI()
|
5 |
+
|
6 |
+
@app.get("/")
|
7 |
+
async def read_root():
|
8 |
+
return FileResponse("index.html")
|
9 |
+
|
10 |
+
if __name__ == "__main__":
|
11 |
+
import uvicorn
|
12 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
index.html
ADDED
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Cloze Reader</title>
|
7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
+
<link href="https://fonts.googleapis.com/css2?family=Special+Elite&display=swap" rel="stylesheet">
|
9 |
+
<style>
|
10 |
+
body { font-family: 'Special Elite', monospace; background: #faf7f0; }
|
11 |
+
.paper-sheet { background: #fefcf7; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
12 |
+
.cloze-input { border: none; border-bottom: 2px solid #000; background: transparent; text-align: center; font-family: inherit; }
|
13 |
+
.typewriter-button { background: #f5f1e8; border: 2px solid #000; font-family: inherit; padding: 8px 16px; cursor: pointer; }
|
14 |
+
.correct { background-color: rgba(16, 185, 129, 0.1); border-color: #10b981; }
|
15 |
+
.incorrect { background-color: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
|
16 |
+
</style>
|
17 |
+
</head>
|
18 |
+
<body class="min-h-screen p-4">
|
19 |
+
<div class="max-w-4xl mx-auto">
|
20 |
+
<header class="text-center mb-8">
|
21 |
+
<div class="flex items-center justify-center gap-3 mb-2">
|
22 |
+
<span class="text-4xl">π</span>
|
23 |
+
<h1 class="text-4xl font-bold">Cloze Reader</h1>
|
24 |
+
</div>
|
25 |
+
<p class="text-gray-600">Fill in the blanks to practice reading comprehension</p>
|
26 |
+
</header>
|
27 |
+
|
28 |
+
<main id="game-container">
|
29 |
+
<div id="loading" class="text-center py-8">
|
30 |
+
<p class="text-lg">Loading passages...</p>
|
31 |
+
</div>
|
32 |
+
|
33 |
+
<div id="game-area" class="paper-sheet rounded-lg p-6 hidden">
|
34 |
+
<div id="book-info" class="text-sm italic text-gray-600 mb-4"></div>
|
35 |
+
<div id="round-info" class="text-sm bg-amber-100 px-3 py-1 rounded-full inline-block mb-4"></div>
|
36 |
+
<div id="contextualization" class="bg-amber-50 border-l-4 border-amber-500 p-3 mb-4 text-sm"></div>
|
37 |
+
<div id="passage-content" class="text-lg leading-relaxed mb-6"></div>
|
38 |
+
<div id="hints-section" class="bg-yellow-50 border-l-4 border-yellow-500 p-3 mb-4 hidden">
|
39 |
+
<div class="font-semibold mb-2 text-sm">π‘ Hints:</div>
|
40 |
+
<div id="hints-list" class="text-sm space-y-1"></div>
|
41 |
+
</div>
|
42 |
+
<div class="flex gap-4 justify-center flex-wrap">
|
43 |
+
<button id="submit-btn" class="typewriter-button">Submit</button>
|
44 |
+
<button id="next-btn" class="typewriter-button hidden">Next Passage</button>
|
45 |
+
<button id="hint-btn" class="typewriter-button">Show Hints</button>
|
46 |
+
</div>
|
47 |
+
<div id="result" class="mt-4 text-center font-semibold"></div>
|
48 |
+
</div>
|
49 |
+
</main>
|
50 |
+
</div>
|
51 |
+
|
52 |
+
<script>
|
53 |
+
// Simple cloze reader implementation
|
54 |
+
class SimpleClozeReader {
|
55 |
+
constructor() {
|
56 |
+
this.currentLevel = 1;
|
57 |
+
this.currentRound = 1;
|
58 |
+
this.blanks = [];
|
59 |
+
this.hints = [];
|
60 |
+
this.books = [
|
61 |
+
{
|
62 |
+
title: "Pride and Prejudice",
|
63 |
+
author: "Jane Austen",
|
64 |
+
text: "It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife. However little known the feelings or views of such a man may be on his first entering a neighbourhood, this truth is so well fixed in the minds of the surrounding families, that he is considered the rightful property of some one or other of their daughters."
|
65 |
+
},
|
66 |
+
{
|
67 |
+
title: "The Adventures of Tom Sawyer",
|
68 |
+
author: "Mark Twain",
|
69 |
+
text: "Tom! No answer. Tom! No answer. What's gone with that boy, I wonder? You TOM! No answer. The old lady pulled her spectacles down and looked over them about the room; then she put them up and looked out under them. She seldom or never looked through them for so small a thing as a boy."
|
70 |
+
}
|
71 |
+
];
|
72 |
+
this.init();
|
73 |
+
}
|
74 |
+
|
75 |
+
init() {
|
76 |
+
this.setupEventListeners();
|
77 |
+
this.startNewRound();
|
78 |
+
}
|
79 |
+
|
80 |
+
setupEventListeners() {
|
81 |
+
document.getElementById('submit-btn').onclick = () => this.submitAnswers();
|
82 |
+
document.getElementById('next-btn').onclick = () => this.nextRound();
|
83 |
+
document.getElementById('hint-btn').onclick = () => this.toggleHints();
|
84 |
+
}
|
85 |
+
|
86 |
+
startNewRound() {
|
87 |
+
const book = this.books[Math.floor(Math.random() * this.books.length)];
|
88 |
+
const blanksCount = this.currentLevel <= 2 ? 1 : this.currentLevel <= 4 ? 2 : 3;
|
89 |
+
|
90 |
+
// Simple word selection
|
91 |
+
const words = book.text.split(' ');
|
92 |
+
const selectedIndices = [];
|
93 |
+
const contentWords = ['truth', 'man', 'fortune', 'wife', 'feelings', 'neighbourhood', 'families', 'daughters', 'answer', 'boy', 'lady', 'spectacles', 'room'];
|
94 |
+
|
95 |
+
for (let i = 0; i < blanksCount && i < contentWords.length; i++) {
|
96 |
+
const wordIndex = words.findIndex(w => w.toLowerCase().includes(contentWords[i]));
|
97 |
+
if (wordIndex !== -1) selectedIndices.push(wordIndex);
|
98 |
+
}
|
99 |
+
|
100 |
+
// Create blanks
|
101 |
+
this.blanks = selectedIndices.map((index, i) => ({
|
102 |
+
index: i,
|
103 |
+
originalWord: words[index].replace(/[^\w]/g, ''),
|
104 |
+
wordIndex: index
|
105 |
+
}));
|
106 |
+
|
107 |
+
// Create hints
|
108 |
+
this.hints = this.blanks.map((blank, i) => {
|
109 |
+
const word = blank.originalWord;
|
110 |
+
if (this.currentLevel <= 2) {
|
111 |
+
return `${word.length} letters, starts with "${word[0]}", ends with "${word[word.length-1]}"`;
|
112 |
+
} else {
|
113 |
+
return `${word.length} letters, starts with "${word[0]}"`;
|
114 |
+
}
|
115 |
+
});
|
116 |
+
|
117 |
+
// Create display text
|
118 |
+
let displayText = book.text;
|
119 |
+
this.blanks.forEach((blank, i) => {
|
120 |
+
const word = words[blank.wordIndex];
|
121 |
+
const input = `<input type="text" class="cloze-input w-20 mx-1" data-index="${i}" placeholder="${'_'.repeat(Math.max(3, blank.originalWord.length))}">`;
|
122 |
+
displayText = displayText.replace(word, input);
|
123 |
+
});
|
124 |
+
|
125 |
+
// Update UI
|
126 |
+
document.getElementById('book-info').innerHTML = `<strong>${book.title}</strong> by ${book.author}`;
|
127 |
+
document.getElementById('round-info').textContent = `Level ${this.currentLevel} β’ ${blanksCount} blank${blanksCount > 1 ? 's' : ''}`;
|
128 |
+
document.getElementById('contextualization').innerHTML = `π Practice with classic literature from ${book.author}'s "${book.title}"`;
|
129 |
+
document.getElementById('passage-content').innerHTML = displayText;
|
130 |
+
document.getElementById('hints-list').innerHTML = this.hints.map((hint, i) => `<div>${i+1}. ${hint}</div>`).join('');
|
131 |
+
|
132 |
+
// Show game area
|
133 |
+
document.getElementById('loading').classList.add('hidden');
|
134 |
+
document.getElementById('game-area').classList.remove('hidden');
|
135 |
+
document.getElementById('hints-section').classList.add('hidden');
|
136 |
+
document.getElementById('result').textContent = '';
|
137 |
+
document.getElementById('submit-btn').classList.remove('hidden');
|
138 |
+
document.getElementById('next-btn').classList.add('hidden');
|
139 |
+
}
|
140 |
+
|
141 |
+
submitAnswers() {
|
142 |
+
const inputs = document.querySelectorAll('.cloze-input');
|
143 |
+
let correct = 0;
|
144 |
+
|
145 |
+
inputs.forEach((input, i) => {
|
146 |
+
const userAnswer = input.value.trim().toLowerCase();
|
147 |
+
const correctAnswer = this.blanks[i].originalWord.toLowerCase();
|
148 |
+
|
149 |
+
if (userAnswer === correctAnswer) {
|
150 |
+
input.classList.add('correct');
|
151 |
+
correct++;
|
152 |
+
} else {
|
153 |
+
input.classList.add('incorrect');
|
154 |
+
// Show correct answer
|
155 |
+
const span = document.createElement('span');
|
156 |
+
span.className = 'text-green-600 font-semibold ml-2 text-sm';
|
157 |
+
span.textContent = `β ${this.blanks[i].originalWord}`;
|
158 |
+
input.parentNode.appendChild(span);
|
159 |
+
}
|
160 |
+
input.disabled = true;
|
161 |
+
});
|
162 |
+
|
163 |
+
const percentage = Math.round((correct / this.blanks.length) * 100);
|
164 |
+
const passed = correct >= (this.blanks.length === 1 ? 1 : this.blanks.length - 1);
|
165 |
+
|
166 |
+
let message = `Score: ${correct}/${this.blanks.length} (${percentage}%)`;
|
167 |
+
if (passed) {
|
168 |
+
message += ` - Excellent! Advancing to Level ${this.currentLevel + 1}! π`;
|
169 |
+
document.getElementById('result').className = 'mt-4 text-center font-semibold text-green-600';
|
170 |
+
this.currentLevel++;
|
171 |
+
} else {
|
172 |
+
message += ` - Keep practicing! πͺ`;
|
173 |
+
document.getElementById('result').className = 'mt-4 text-center font-semibold text-red-600';
|
174 |
+
}
|
175 |
+
|
176 |
+
document.getElementById('result').textContent = message;
|
177 |
+
document.getElementById('submit-btn').classList.add('hidden');
|
178 |
+
document.getElementById('next-btn').classList.remove('hidden');
|
179 |
+
this.currentRound++;
|
180 |
+
}
|
181 |
+
|
182 |
+
nextRound() {
|
183 |
+
document.querySelectorAll('.cloze-input').forEach(input => {
|
184 |
+
input.classList.remove('correct', 'incorrect');
|
185 |
+
input.disabled = false;
|
186 |
+
input.value = '';
|
187 |
+
});
|
188 |
+
document.querySelectorAll('.text-green-600').forEach(el => el.remove());
|
189 |
+
this.startNewRound();
|
190 |
+
}
|
191 |
+
|
192 |
+
toggleHints() {
|
193 |
+
const hintsSection = document.getElementById('hints-section');
|
194 |
+
const btn = document.getElementById('hint-btn');
|
195 |
+
if (hintsSection.classList.contains('hidden')) {
|
196 |
+
hintsSection.classList.remove('hidden');
|
197 |
+
btn.textContent = 'Hide Hints';
|
198 |
+
} else {
|
199 |
+
hintsSection.classList.add('hidden');
|
200 |
+
btn.textContent = 'Show Hints';
|
201 |
+
}
|
202 |
+
}
|
203 |
+
}
|
204 |
+
|
205 |
+
// Initialize when page loads
|
206 |
+
document.addEventListener('DOMContentLoaded', () => {
|
207 |
+
new SimpleClozeReader();
|
208 |
+
});
|
209 |
+
</script>
|
210 |
+
</body>
|
211 |
+
</html>
|
requirements.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
fastapi==0.104.1
|
2 |
+
uvicorn[standard]==0.24.0
|