diabolic6045 commited on
Commit
77873df
Β·
verified Β·
1 Parent(s): ebc74aa

Upload 11 files

Browse files
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .vercel
2
+ *.log
3
+ *.pyc
4
+ __pycache__
5
+
6
+ # Environments
7
+ .env
8
+ .venv
9
+ env/
10
+ venv/
11
+ ENV/
12
+ env.bak/
13
+ venv.bak/
api/__pycache__/app.cpython-311.pyc ADDED
Binary file (4.43 kB). View file
 
api/app.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask, jsonify, render_template, request
3
+ import google.generativeai as genai
4
+
5
+ app = Flask(__name__, static_folder='static')
6
+
7
+ genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
8
+
9
+ # Enhanced generation config
10
+ generation_config = {
11
+ "temperature": 0.9,
12
+ "top_p": 0.95,
13
+ "top_k": 40,
14
+ "max_output_tokens": 1024,
15
+ }
16
+
17
+ model = genai.GenerativeModel(
18
+ model_name="gemini-1.5-flash-002",
19
+ generation_config=generation_config,
20
+ system_instruction="You are a master storyteller who creates engaging, creative stories in any genre."
21
+ )
22
+
23
+ # Store active chat sessions
24
+ sessions = {}
25
+
26
+ @app.route('/')
27
+ def index():
28
+ return render_template('index.html')
29
+
30
+ @app.route('/start-story', methods=['POST'])
31
+ def start_story():
32
+ try:
33
+ data = request.json
34
+ genre = data.get('genre', 'fantasy')
35
+ theme = data.get('theme', 'adventure')
36
+ session_id = os.urandom(16).hex()
37
+
38
+ prompt = f"Create the beginning of a {genre} story with a {theme} theme. Make it engaging, easy to read, with a good flow and approximately 300 words. also make sure you use easy level of english and simple words."
39
+
40
+ chat_session = model.start_chat(history=[])
41
+ response = chat_session.send_message(prompt)
42
+
43
+ sessions[session_id] = {
44
+ 'chat_session': chat_session,
45
+ 'genre': genre,
46
+ 'theme': theme,
47
+ 'branch_point': 1
48
+ }
49
+ return jsonify({
50
+ 'story': response.text,
51
+ 'session_id': session_id
52
+ })
53
+ except Exception as e:
54
+ return jsonify({'error': str(e)}), 500
55
+
56
+ @app.route('/continue-story', methods=['POST'])
57
+ def continue_story():
58
+ try:
59
+ data = request.json
60
+ session_id = data.get('session_id')
61
+ branch_choice = data.get('branch_choice', 'main')
62
+
63
+ if session_id not in sessions:
64
+ return jsonify({'error': 'Invalid session'}), 400
65
+
66
+ session = sessions[session_id]
67
+ chat_session = session['chat_session']
68
+
69
+ if branch_choice == 'branch':
70
+ prompt = "Continue the story but introduce an unexpected twist that creates a new story branch."
71
+ else:
72
+ prompt = "Continue the story naturally from where it left off."
73
+
74
+ response = chat_session.send_message(f"{prompt} Add approximately 300 more words.")
75
+
76
+ # Generate branch options every 3 chunks
77
+ branch_options = None
78
+ if session['branch_point'] % 3 == 0:
79
+ branch_options = [
80
+ "Continue the main story",
81
+ "Explore an alternative path"
82
+ ]
83
+
84
+ session['branch_point'] += 1
85
+
86
+ return jsonify({
87
+ 'continuation': response.text,
88
+ 'branch_options': branch_options
89
+ })
90
+ except Exception as e:
91
+ return jsonify({'error': str(e)}), 500
92
+
93
+ # if __name__ == '__main__':
94
+ app.run(host='0.0.0.0', port=9200)
api/static/css/style.css ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root[data-theme="light"] {
2
+ --bg-primary: #f8f9fa;
3
+ --bg-secondary: #ffffff;
4
+ --text-primary: #2c3e50;
5
+ --text-secondary: #6c757d;
6
+ --accent: #3498db;
7
+ --border: #e9ecef;
8
+ --shadow: rgba(0,0,0,0.1);
9
+ --menu-bg: rgba(255, 255, 255, 0.95);
10
+ }
11
+
12
+ :root[data-theme="dark"] {
13
+ --bg-primary: #1a1a1a;
14
+ --bg-secondary: #121212;
15
+ --text-primary: #e9ecef;
16
+ --text-secondary: #adb5bd;
17
+ --accent: #61dafb;
18
+ --border: #404040;
19
+ --shadow: rgba(0, 0, 0, 0.3);
20
+ --menu-bg: rgba(18, 18, 18, 0.95);
21
+ }
22
+
23
+ * {
24
+ box-sizing: border-box;
25
+ margin: 0;
26
+ padding: 0;
27
+ transition: background-color 0.3s, color 0.3s;
28
+ }
29
+
30
+ body {
31
+ font-family: 'Crimson Text', Georgia, serif;
32
+ line-height: 1.8;
33
+ background-color: var(--bg-primary);
34
+ color: var(--text-primary);
35
+ min-height: 100vh;
36
+ }
37
+
38
+ .menu-toggle {
39
+ position: fixed;
40
+ top: 1rem;
41
+ right: 1rem;
42
+ z-index: 1001;
43
+ background: none;
44
+ border: none;
45
+ color: var(--text-primary);
46
+ font-size: 1.5rem;
47
+ cursor: pointer;
48
+ width: 40px;
49
+ height: 40px;
50
+ border-radius: 50%;
51
+ background-color: var(--bg-secondary);
52
+ box-shadow: 0 2px 10px var(--shadow);
53
+ }
54
+
55
+ .settings-panel {
56
+ position: fixed;
57
+ top: 0;
58
+ right: -300px;
59
+ width: 300px;
60
+ height: 100vh;
61
+ background-color: var(--menu-bg);
62
+ padding: 2rem;
63
+ box-shadow: -5px 0 15px var(--shadow);
64
+ transition: right 0.3s ease;
65
+ z-index: 1000;
66
+ backdrop-filter: blur(10px);
67
+ }
68
+
69
+ .settings-panel.active {
70
+ right: 0;
71
+ }
72
+
73
+ .settings-header {
74
+ margin-bottom: 2rem;
75
+ text-align: center;
76
+ }
77
+
78
+ .settings-content {
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: 0.7rem;
82
+ }
83
+
84
+ select {
85
+ width: 100%;
86
+ padding: 0.8rem;
87
+ border: 2px solid var(--border);
88
+ border-radius: 8px;
89
+ background-color: var(--bg-secondary);
90
+ color: var(--text-primary);
91
+ font-size: 1rem;
92
+ cursor: pointer;
93
+ outline: none;
94
+ }
95
+
96
+ .theme-toggle {
97
+ background: none;
98
+ border: 2px solid var(--border);
99
+ color: var(--text-primary);
100
+ cursor: pointer;
101
+ padding: 0.8rem;
102
+ border-radius: 8px;
103
+ width: 100%;
104
+ font-size: 1rem;
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ gap: 0.5rem;
109
+ }
110
+
111
+ .story-container {
112
+ max-width: 800px;
113
+ margin: 0 auto;
114
+ padding: 2rem;
115
+ padding-top: 4rem;
116
+ }
117
+
118
+ .story-paragraph {
119
+ font-size: 1.2rem;
120
+ margin-bottom: 1.5rem;
121
+ line-height: 1.8;
122
+ opacity: 0;
123
+ transform: translateY(20px);
124
+ animation: fadeIn 0.8s ease-out forwards;
125
+ letter-spacing: 0.3px;
126
+ white-space: pre-wrap; /* Preserve whitespace and wrap text */
127
+ font-family: inherit; /* Use the same font as body */
128
+ background: none; /* Remove pre element's default background */
129
+ border: none; /* Remove pre element's default border */
130
+ overflow-x: hidden; /* Prevent horizontal scrolling */
131
+ width: 100%; /* Take full width of container */
132
+ }
133
+
134
+ .loading {
135
+ display: flex;
136
+ justify-content: center;
137
+ align-items: center;
138
+ padding: 2rem;
139
+ color: var(--text-secondary);
140
+ }
141
+
142
+ .loading-dots {
143
+ display: flex;
144
+ gap: 0.5rem;
145
+ }
146
+
147
+ .loading-dots span {
148
+ width: 8px;
149
+ height: 8px;
150
+ background-color: var(--accent);
151
+ border-radius: 50%;
152
+ animation: bounce 0.5s infinite alternate;
153
+ }
154
+
155
+ .loading-dots span:nth-child(2) { animation-delay: 0.2s; }
156
+ .loading-dots span:nth-child(3) { animation-delay: 0.4s; }
157
+
158
+ @keyframes fadeIn {
159
+ to {
160
+ opacity: 1;
161
+ transform: translateY(0);
162
+ }
163
+ }
164
+
165
+ @keyframes bounce {
166
+ to {
167
+ transform: translateY(-8px);
168
+ }
169
+ }
170
+
171
+ @media (max-width: 768px) {
172
+ .story-container {
173
+ padding: 1rem;
174
+ padding-top: 4rem;
175
+ }
176
+
177
+ .story-paragraph {
178
+ font-size: 1.1rem;
179
+ line-height: 1.7;
180
+ }
181
+
182
+ .settings-panel {
183
+ width: 100%;
184
+ right: -100%;
185
+ }
186
+ }
187
+
188
+ /* Reading Progress Bar */
189
+ .progress-bar {
190
+ position: fixed;
191
+ top: 0;
192
+ left: 0;
193
+ width: 100%;
194
+ height: 4px;
195
+ background-color: var(--border);
196
+ z-index: 1002;
197
+ }
198
+
199
+ .progress {
200
+ height: 100%;
201
+ background-color: var(--accent);
202
+ width: 0%;
203
+ transition: width 0.1s;
204
+ }
205
+
206
+ /* Font size controls */
207
+ .font-size-controls {
208
+ display: flex;
209
+ gap: 0.5rem;
210
+ margin-top: 1rem;
211
+ }
212
+
213
+ .font-size-btn {
214
+ padding: 0.5rem 1rem;
215
+ border: 1px solid var(--border);
216
+ background: var(--bg-secondary);
217
+ color: var(--text-primary);
218
+ border-radius: 4px;
219
+ cursor: pointer;
220
+ }
221
+ /* Reading Time Indicator */
222
+ .reading-info {
223
+ position: fixed;
224
+ bottom: 1rem;
225
+ right: 1rem;
226
+ background-color: var(--bg-secondary);
227
+ padding: 0.5rem 1rem;
228
+ border-radius: 20px;
229
+ box-shadow: 0 2px 10px var(--shadow);
230
+ font-size: 0.9rem;
231
+ color: var(--text-secondary);
232
+ z-index: 1000;
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 0.5rem;
236
+ opacity: 0.8;
237
+ transition: opacity 0.3s;
238
+ }
239
+
240
+ .reading-info:hover {
241
+ opacity: 1;
242
+ }
243
+
244
+ /* Text-to-Speech Controls */
245
+ .tts-controls {
246
+ position: fixed;
247
+ bottom: 1rem;
248
+ right: 1rem;
249
+ display: flex;
250
+ gap: 0.5rem;
251
+ z-index: 1000;
252
+ }
253
+
254
+ .tts-btn {
255
+ width: 40px;
256
+ height: 40px;
257
+ border-radius: 50%;
258
+ background-color: var(--bg-secondary);
259
+ color: var(--text-primary);
260
+ border: none;
261
+ cursor: pointer;
262
+ box-shadow: 0 2px 10px var(--shadow);
263
+ display: flex;
264
+ align-items: center;
265
+ justify-content: center;
266
+ transition: transform 0.2s;
267
+ }
268
+
269
+ .tts-btn:hover {
270
+ transform: scale(1.1);
271
+ }
272
+
273
+ .tts-btn.active {
274
+ color: var(--accent);
275
+ }
276
+
277
+ /* Genre-specific themes */
278
+ .theme-group {
279
+ display: none;
280
+ }
281
+
282
+ .theme-group.active {
283
+ display: block;
284
+ }
285
+
286
+ /* Enhanced Settings Panel */
287
+ .settings-section {
288
+ margin-top: 1rem;
289
+ padding: 0.7rem;
290
+ background-color: var(--bg-secondary);
291
+ border-radius: 8px;
292
+ box-shadow: 0 2px 5px var(--shadow);
293
+ }
294
+
295
+ .appearance-controls {
296
+ display: flex;
297
+ flex-direction: column;
298
+ gap: 1rem;
299
+ }
300
+
301
+ .font-family-select {
302
+ width: 100%;
303
+ padding: 0.5rem;
304
+ border: 1px solid var(--border);
305
+ border-radius: 4px;
306
+ background-color: var(--bg-primary);
307
+ color: var(--text-primary);
308
+ }
309
+
310
+ .font-size-controls {
311
+ display: flex;
312
+ gap: 0.5rem;
313
+ justify-content: center;
314
+ }
315
+
316
+ .font-size-btn {
317
+ padding: 0.5rem 1rem;
318
+ border: 1px solid var(--border);
319
+ background: var(--bg-secondary);
320
+ color: var(--text-primary);
321
+ border-radius: 4px;
322
+ cursor: pointer;
323
+ transition: all 0.2s;
324
+ }
325
+
326
+ .font-size-btn:hover {
327
+ background: var(--accent);
328
+ color: var(--bg-secondary);
329
+ }
330
+
331
+ .theme-toggle {
332
+ width: 100%;
333
+ padding: 0.5rem;
334
+ display: flex;
335
+ align-items: center;
336
+ justify-content: center;
337
+ gap: 0.5rem;
338
+ background: var(--bg-secondary);
339
+ border: 1px solid var(--border);
340
+ color: var(--text-primary);
341
+ border-radius: 4px;
342
+ cursor: pointer;
343
+ transition: all 0.2s;
344
+ }
345
+
346
+ .theme-toggle:hover {
347
+ background: var(--accent);
348
+ color: var(--bg-secondary);
349
+ }
350
+
351
+ /* Mobile Optimizations */
352
+ @media (max-width: 768px) {
353
+ .settings-panel {
354
+ width: 100%;
355
+ right: -100%;
356
+ }
357
+
358
+ .reading-info {
359
+ bottom: 4rem;
360
+ right: 1rem;
361
+ font-size: 0.8rem;
362
+ }
363
+
364
+ .appearance-controls {
365
+ gap: 0.8rem;
366
+ }
367
+
368
+ .font-size-controls {
369
+ justify-content: space-between;
370
+ }
371
+
372
+ .font-size-btn {
373
+ flex: 1;
374
+ }
375
+ }
376
+
377
+ /* Swipe Indicator */
378
+ .swipe-indicator {
379
+ position: fixed;
380
+ bottom: 50%;
381
+ left: 50%;
382
+ transform: translate(-50%, 50%);
383
+ background-color: var(--bg-secondary);
384
+ padding: 1rem;
385
+ border-radius: 10px;
386
+ box-shadow: 0 2px 10px var(--shadow);
387
+ display: none;
388
+ animation: fadeInOut 2s ease-in-out;
389
+ z-index: 1000;
390
+ }
391
+
392
+ @keyframes fadeInOut {
393
+ 0%, 100% { opacity: 0; }
394
+ 50% { opacity: 1; }
395
+ }
396
+ /* Add new styles for initial loading animation */
397
+ .initial-loading {
398
+ display: flex;
399
+ flex-direction: column;
400
+ align-items: center;
401
+ justify-content: center;
402
+ padding: 3rem;
403
+ text-align: center;
404
+ color: var(--text-secondary);
405
+ }
406
+
407
+ .initial-loading h3 {
408
+ margin-bottom: 1.5rem;
409
+ color: var(--text-primary);
410
+ }
411
+
412
+ .quill-loading {
413
+ width: 50px;
414
+ height: 50px;
415
+ margin-bottom: 1rem;
416
+ background: var(--accent);
417
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M15.78 15.84S18.64 13 19.61 12c3.07-3.07 4.24-6.29 2.83-7.7s-4.63-.24-7.7 2.83l-3.84 3.84M9.63 8.88S6.77 11.74 5.8 12.73c-3.07 3.07-4.24 6.29-2.83 7.7s4.63.24 7.7-2.83l3.84-3.84'/%3E%3C/svg%3E") center/contain no-repeat;
418
+ animation: write 1s infinite;
419
+ }
420
+
421
+ @keyframes write {
422
+ 0% { transform: translateX(-15px) rotate(-45deg); }
423
+ 50% { transform: translateX(15px) rotate(-45deg); }
424
+ 100% { transform: translateX(-15px) rotate(-45deg); }
425
+ }
426
+ .app-logo {
427
+ width: 4px; /* Adjust size as needed */
428
+ height: 4px;
429
+ object-fit: contain;
430
+ margin-right: 8px;
431
+ }
432
+
433
+ .app-name {
434
+ position: fixed;
435
+ top: 1rem;
436
+ left: 1rem;
437
+ display: flex;
438
+ align-items: center;
439
+ gap: 0.5rem;
440
+ text-decoration: none;
441
+ color: var(--text-primary);
442
+ font-size: 1rem;
443
+ font-weight: 600;
444
+ z-index: 1001;
445
+ padding: 0.5rem 1rem;
446
+ background: var(--bg-secondary);
447
+ border-radius: 20px;
448
+ box-shadow: 0 2px 10px var(--shadow);
449
+ transition: transform 0.2s;
450
+ }
451
+
452
+ .app-name:hover {
453
+ transform: translateY(-2px);
454
+ }
455
+
456
+ .app-icon {
457
+ font-size: 1rem;
458
+ }
459
+
460
+ /* Add these styles */
461
+ .app-name, .reading-info, .tts-controls {
462
+ transition: opacity 0.3s, visibility 0.3s;
463
+ }
464
+
465
+ .hide-ui .app-name,
466
+ .hide-ui .reading-info,
467
+ .hide-ui .tts-controls {
468
+ opacity: 0;
469
+ visibility: hidden;
470
+ }
api/static/images/logo.webp ADDED
api/static/js/story.js ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ let currentSessionId = null;
3
+ let isGenerating = false;
4
+ let lastScrollPosition = 0;
5
+ let baseFontSize = 1.2;
6
+ let isTTSActive = false;
7
+ let speechSynthesis = window.speechSynthesis;
8
+ let speechUtterance = null;
9
+
10
+ // Genre-Theme Mapping
11
+ const genreThemes = {
12
+ fantasy: [
13
+ { value: 'magic', label: 'Magic & Wizardry' },
14
+ { value: 'dragons', label: 'Dragons & Creatures' },
15
+ { value: 'quest', label: 'Epic Quest' },
16
+ { value: 'mythology', label: 'Mythology & Legends' },
17
+ { value: 'medieval', label: 'Medieval Adventure' }
18
+ ],
19
+ 'sci-fi': [
20
+ { value: 'space', label: 'Space Exploration' },
21
+ { value: 'ai', label: 'Artificial Intelligence' },
22
+ { value: 'dystopia', label: 'Dystopian Future' },
23
+ { value: 'aliens', label: 'First Contact' },
24
+ { value: 'cyberpunk', label: 'Cyberpunk' }
25
+ ],
26
+ mystery: [
27
+ { value: 'detective', label: 'Detective Story' },
28
+ { value: 'thriller', label: 'Psychological Thriller' },
29
+ { value: 'crime', label: 'Crime Investigation' },
30
+ { value: 'supernatural', label: 'Supernatural Mystery' }
31
+ ],
32
+ romance: [
33
+ { value: 'contemporary', label: 'Contemporary Romance' },
34
+ { value: 'historical', label: 'Historical Romance' },
35
+ { value: 'paranormal', label: 'Paranormal Romance' },
36
+ { value: 'comedy', label: 'Romantic Comedy' }
37
+ ],
38
+ horror: [
39
+ { value: 'supernatural', label: 'Supernatural Horror' },
40
+ { value: 'psychological', label: 'Psychological Horror' },
41
+ { value: 'gothic', label: 'Gothic Horror' },
42
+ { value: 'cosmic', label: 'Cosmic Horror' }
43
+ ],
44
+ historical: [
45
+ { value: 'ancient', label: 'Ancient Civilizations' },
46
+ { value: 'medieval', label: 'Medieval Times' },
47
+ { value: 'renaissance', label: 'Renaissance' },
48
+ { value: 'modern', label: 'Modern History' }
49
+ ],
50
+ adventure: [
51
+ { value: 'exploration', label: 'Exploration' },
52
+ { value: 'survival', label: 'Survival' },
53
+ { value: 'treasure', label: 'Treasure Hunt' },
54
+ { value: 'journey', label: 'Epic Journey' }
55
+ ]
56
+ };
57
+
58
+ // Add this new function to calculate and update the progress bar
59
+ function updateProgressBar() {
60
+ const winScroll = window.pageYOffset || document.documentElement.scrollTop;
61
+ const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
62
+ const scrolled = (winScroll / height) * 100;
63
+ document.getElementById('progress').style.width = scrolled + '%';
64
+ }
65
+
66
+ // Add scroll event listener to update progress bar
67
+ window.addEventListener('scroll', () => {
68
+ updateProgressBar();
69
+ });
70
+
71
+ // Update the existing scroll event listener to include both progress bar and infinite scroll
72
+ window.addEventListener('scroll', async () => {
73
+ // Update progress bar
74
+ updateProgressBar();
75
+
76
+ // Existing infinite scroll logic
77
+ if (isGenerating || !currentSessionId) return;
78
+
79
+ const scrollPosition = window.innerHeight + window.scrollY;
80
+ const documentHeight = document.documentElement.offsetHeight;
81
+
82
+ if (scrollPosition > documentHeight - 300 && scrollPosition > lastScrollPosition) {
83
+ isGenerating = true;
84
+ addLoadingIndicator();
85
+
86
+ try {
87
+ const response = await fetch('/continue-story', {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({
91
+ session_id: currentSessionId,
92
+ branch_choice: 'main'
93
+ })
94
+ });
95
+
96
+ const data = await response.json();
97
+ if (data.error) throw new Error(data.error);
98
+
99
+ removeLoadingIndicator();
100
+ addStoryParagraph(data.continuation);
101
+ } catch (error) {
102
+ console.error('Error:', error);
103
+ removeLoadingIndicator();
104
+ }
105
+
106
+ isGenerating = false;
107
+ lastScrollPosition = scrollPosition;
108
+ }
109
+ });
110
+
111
+ function addStoryParagraph(text) {
112
+ const container = document.getElementById('story-container');
113
+
114
+ // Create a pre element to preserve formatting
115
+ const pre = document.createElement('pre');
116
+ pre.className = 'story-paragraph';
117
+ pre.style.fontSize = `${baseFontSize}rem`;
118
+ pre.textContent = text;
119
+
120
+ container.appendChild(pre);
121
+ updateReadingTime();
122
+ updateProgressBar();
123
+ }
124
+
125
+ // Menu Toggle
126
+ const menuToggle = document.getElementById('menuToggle');
127
+ const settingsPanel = document.getElementById('settingsPanel');
128
+
129
+ menuToggle.addEventListener('click', () => {
130
+ settingsPanel.classList.toggle('active');
131
+ document.body.classList.toggle('hide-ui');
132
+ });
133
+
134
+ // Close menu when clicking outside
135
+ document.addEventListener('click', (e) => {
136
+ if (!settingsPanel.contains(e.target) && e.target !== menuToggle) {
137
+ settingsPanel.classList.remove('active');
138
+ document.body.classList.remove('hide-ui');
139
+ }
140
+ });
141
+
142
+ // Theme Toggle
143
+ const themeToggle = document.getElementById('theme-toggle');
144
+ const root = document.documentElement;
145
+ const themeIcon = document.getElementById('theme-icon');
146
+
147
+ themeToggle.addEventListener('click', () => {
148
+ const currentTheme = root.getAttribute('data-theme');
149
+ const newTheme = currentTheme === 'light' ? 'dark' : 'light';
150
+ root.setAttribute('data-theme', newTheme);
151
+ themeIcon.textContent = newTheme === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
152
+ localStorage.setItem('theme', newTheme);
153
+ });
154
+
155
+ // Load saved theme
156
+ const savedTheme = localStorage.getItem('theme') || 'light';
157
+ root.setAttribute('data-theme', savedTheme);
158
+ themeIcon.textContent = savedTheme === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
159
+
160
+ // Update themes based on genre selection
161
+ function updateThemes(genre) {
162
+ const themeSelect = document.getElementById('theme');
163
+ themeSelect.innerHTML = '';
164
+
165
+ genreThemes[genre].forEach(theme => {
166
+ const option = document.createElement('option');
167
+ option.value = theme.value;
168
+ option.textContent = theme.label;
169
+ themeSelect.appendChild(option);
170
+ });
171
+ }
172
+
173
+ // Reading Time Estimation
174
+ function updateReadingTime() {
175
+ const text = document.getElementById('story-container').textContent;
176
+ const wordCount = text.trim().split(/\s+/).length;
177
+ const minutes = Math.ceil(wordCount / 200); // Average reading speed
178
+ document.getElementById('readingTime').textContent = `${minutes} min read`;
179
+ }
180
+
181
+ // Font size controls
182
+ function changeFontSize(delta) {
183
+ baseFontSize = Math.max(0.8, Math.min(1.6, baseFontSize + delta * 0.1));
184
+ document.querySelectorAll('.story-paragraph').forEach(p => {
185
+ p.style.fontSize = `${baseFontSize}rem`;
186
+ });
187
+ localStorage.setItem('fontSize', baseFontSize);
188
+ }
189
+
190
+ function updateFontFamily(fontFamily) {
191
+ document.querySelectorAll('.story-paragraph').forEach(p => {
192
+ p.style.fontFamily = fontFamily;
193
+ });
194
+ document.body.style.fontFamily = `${fontFamily}, Georgia, serif`;
195
+ }
196
+
197
+ // Text-to-Speech Functions
198
+ function setupTTS() {
199
+ const ttsToggle = document.getElementById('ttsToggle');
200
+
201
+ ttsToggle.addEventListener('click', () => {
202
+ if (!isTTSActive) {
203
+ startTTS();
204
+ } else {
205
+ stopTTS();
206
+ }
207
+ });
208
+ }
209
+
210
+ function startTTS() {
211
+ if (speechSynthesis && !isTTSActive) {
212
+ const text = document.getElementById('story-container').textContent;
213
+ speechUtterance = new SpeechSynthesisUtterance(text);
214
+ speechUtterance.rate = 0.9;
215
+ speechSynthesis.speak(speechUtterance);
216
+ isTTSActive = true;
217
+ document.getElementById('ttsToggle').classList.add('active');
218
+ }
219
+ }
220
+
221
+ function stopTTS() {
222
+ if (speechSynthesis) {
223
+ speechSynthesis.cancel();
224
+ isTTSActive = false;
225
+ document.getElementById('ttsToggle').classList.remove('active');
226
+ }
227
+ }
228
+
229
+ // Touch Gestures
230
+ function setupTouchGestures() {
231
+ let touchStartX = 0;
232
+ let touchEndX = 0;
233
+
234
+ document.addEventListener('touchstart', e => {
235
+ touchStartX = e.changedTouches[0].screenX;
236
+ }, false);
237
+
238
+ document.addEventListener('touchend', e => {
239
+ touchEndX = e.changedTouches[0].screenX;
240
+ handleSwipe();
241
+ }, false);
242
+
243
+ function handleSwipe() {
244
+ const swipeThreshold = 100;
245
+ const diff = touchEndX - touchStartX;
246
+
247
+ if (Math.abs(diff) > swipeThreshold) {
248
+ if (diff > 0) {
249
+ // Swipe right - open settings
250
+ settingsPanel.classList.add('active');
251
+ document.body.classList.add('hide-ui');
252
+ } else {
253
+ // Swipe left - close settings
254
+ settingsPanel.classList.remove('active');
255
+ document.body.classList.remove('hide-ui');
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ // Story generation and display
262
+ async function startStory() {
263
+ const genre = document.getElementById('genre').value;
264
+ const theme = document.getElementById('theme').value;
265
+
266
+ try {
267
+ const storyContainer = document.getElementById('story-container');
268
+ // Show initial loading animation only if this is the first story
269
+ if (!currentSessionId) {
270
+ storyContainer.innerHTML = `
271
+ <div class="initial-loading">
272
+ <div class="quill-loading"></div>
273
+ <h3>Crafting your story...</h3>
274
+ <p>Our AI storyteller is weaving a tale just for you</p>
275
+ </div>
276
+ `;
277
+ }
278
+
279
+ const response = await fetch('/start-story', {
280
+ method: 'POST',
281
+ headers: { 'Content-Type': 'application/json' },
282
+ body: JSON.stringify({ genre, theme })
283
+ });
284
+
285
+ const data = await response.json();
286
+ if (data.error) throw new Error(data.error);
287
+
288
+ currentSessionId = data.session_id;
289
+ storyContainer.innerHTML = '';
290
+ addStoryParagraph(data.story);
291
+ settingsPanel.classList.remove('active');
292
+ updateReadingTime();
293
+ stopTTS();
294
+ } catch (error) {
295
+ console.error('Error:', error);
296
+ }
297
+ }
298
+
299
+ function addStoryParagraph(text) {
300
+ const p = document.createElement('p');
301
+ p.className = 'story-paragraph';
302
+ p.style.fontSize = `${baseFontSize}rem`;
303
+ p.textContent = text;
304
+ document.getElementById('story-container').appendChild(p);
305
+ updateReadingTime();
306
+ }
307
+
308
+ function addLoadingIndicator() {
309
+ const loading = document.createElement('div');
310
+ loading.className = 'loading';
311
+ loading.innerHTML = `
312
+ <div class="loading-dots">
313
+ <span></span>
314
+ <span></span>
315
+ <span></span>
316
+ </div>
317
+ `;
318
+ document.getElementById('story-container').appendChild(loading);
319
+ }
320
+
321
+ function removeLoadingIndicator() {
322
+ const loading = document.querySelector('.loading');
323
+ if (loading) loading.remove();
324
+ }
325
+
326
+ // Infinite scroll
327
+ window.addEventListener('scroll', async () => {
328
+ if (isGenerating || !currentSessionId) return;
329
+
330
+ const scrollPosition = window.innerHeight + window.scrollY;
331
+ const documentHeight = document.documentElement.offsetHeight;
332
+
333
+ if (scrollPosition > documentHeight - 300 && scrollPosition > lastScrollPosition) {
334
+ isGenerating = true;
335
+ addLoadingIndicator();
336
+
337
+ try {
338
+ const response = await fetch('/continue-story', {
339
+ method: 'POST',
340
+ headers: { 'Content-Type': 'application/json' },
341
+ body: JSON.stringify({
342
+ session_id: currentSessionId,
343
+ branch_choice: 'main'
344
+ })
345
+ });
346
+
347
+ const data = await response.json();
348
+ if (data.error) throw new Error(data.error);
349
+
350
+ removeLoadingIndicator();
351
+ addStoryParagraph(data.continuation);
352
+ } catch (error) {
353
+ console.error('Error:', error);
354
+ removeLoadingIndicator();
355
+ }
356
+
357
+ isGenerating = false;
358
+ lastScrollPosition = scrollPosition;
359
+ }
360
+ });
361
+
362
+ // Initialize
363
+ document.addEventListener('DOMContentLoaded', () => {
364
+ // Setup initial themes
365
+ updateThemes(document.getElementById('genre').value);
366
+
367
+ // Setup genre change listener
368
+ document.getElementById('genre').addEventListener('change', (e) => {
369
+ updateThemes(e.target.value);
370
+ startStory();
371
+ });
372
+
373
+ // Setup theme change listener
374
+ document.getElementById('theme').addEventListener('change', startStory);
375
+
376
+ // Setup TTS
377
+ setupTTS();
378
+
379
+ // Setup touch gestures
380
+ setupTouchGestures();
381
+
382
+ // Setup font family change listener
383
+ document.getElementById('fontFamily').addEventListener('change', (e) => {
384
+ updateFontFamily(e.target.value);
385
+ });
386
+
387
+ // Load saved font family
388
+ const savedFontFamily = localStorage.getItem('fontFamily');
389
+ if (savedFontFamily) {
390
+ document.getElementById('fontFamily').value = savedFontFamily;
391
+ updateFontFamily(savedFontFamily);
392
+ }
393
+
394
+ // Start initial story
395
+ startStory();
396
+ });
api/templates/index.html ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Lore Keeper</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
8
+ <link rel="icon" type="image/png" href="{{ url_for('static', filename='images/logo.webp') }}">
9
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
10
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
11
+ </head>
12
+ <body>
13
+ <a href="#" class="app-name">
14
+ <span class="app-icon">πŸ“š</span>
15
+ <span>Lore Keeper</span>
16
+ </a>
17
+ <div class="progress-bar">
18
+ <div class="progress" id="progress"></div>
19
+ </div>
20
+
21
+ <button class="menu-toggle" id="menuToggle">☰</button>
22
+
23
+ <div class="settings-panel" id="settingsPanel">
24
+ <div class="settings-header">
25
+ <h2>Story Settings</h2>
26
+ </div>
27
+ <div class="settings-content">
28
+ <div class="settings-section">
29
+ <h3>Genre</h3>
30
+ <select id="genre">
31
+ <option value="fantasy">Fantasy</option>
32
+ <option value="sci-fi">Science Fiction</option>
33
+ <option value="mystery">Mystery</option>
34
+ <option value="romance">Romance</option>
35
+ <option value="horror">Horror</option>
36
+ <option value="historical">Historical Fiction</option>
37
+ <option value="adventure">Adventure</option>
38
+ </select>
39
+ </div>
40
+
41
+ <div class="settings-section">
42
+ <h3>Theme</h3>
43
+ <select id="theme"></select>
44
+ </div>
45
+
46
+ <div class="settings-section">
47
+ <h3>Appearance</h3>
48
+ <div class="appearance-controls">
49
+ <select id="fontFamily" class="font-family-select">
50
+ <option value="Crimson Text">Crimson Text</option>
51
+ <option value="Georgia">Georgia</option>
52
+ <option value="Palatino">Palatino</option>
53
+ <option value="Baskerville">Baskerville</option>
54
+ </select>
55
+
56
+ <div class="font-size-controls">
57
+ <button class="font-size-btn" onclick="changeFontSize(-1)">A-</button>
58
+ <button class="font-size-btn" onclick="changeFontSize(1)">A+</button>
59
+ </div>
60
+
61
+ <button class="theme-toggle" id="theme-toggle">
62
+ <span id="theme-icon">πŸŒ™</span>
63
+ <span>Toggle Theme</span>
64
+ </button>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- Add Reading Info -->
71
+ <div class="reading-info">
72
+ <span class="material-icons">schedule</span>
73
+ <span id="readingTime">0 min read</span>
74
+ </div>
75
+
76
+ <!-- Add Text-to-Speech Controls -->
77
+ <div class="tts-controls">
78
+ <button class="tts-btn" id="ttsToggle" title="Toggle Text-to-Speech">
79
+ <span class="material-icons">volume_up</span>
80
+ </button>
81
+ </div>
82
+
83
+ <!-- Add Swipe Indicator -->
84
+ <div class="swipe-indicator" id="swipeIndicator">
85
+ Swipe for more options
86
+ </div>
87
+
88
+ <div class="story-container" id="story-container"></div>
89
+
90
+ <script src="{{ url_for('static', filename='js/story.js') }}"></script>
91
+ </body>
92
+ </html>
data/banner.webp ADDED
readme.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ“š Lore Keeper
2
+
3
+ ![Lore Keeper Banner](./data/banner.webp)
4
+
5
+ An elegant AI-powered storytelling platform that creates personalized stories across multiple genres using Google's Gemini AI.
6
+
7
+ ## ✨ Features
8
+
9
+ ### 🎭 Genre & Theme Selection
10
+ - Multiple genres including Fantasy, Sci-Fi, Mystery, Romance, Horror, Historical Fiction, and Adventure
11
+ - Customizable themes within each genre
12
+ - Dynamic story generation based on selected preferences
13
+
14
+ ### πŸ“– Reading Experience
15
+ - Clean, distraction-free reading interface
16
+ - Progress bar tracking
17
+ - Estimated reading time
18
+ - Infinite scroll for continuous story generation
19
+ - Text-to-Speech functionality
20
+ - Customizable font settings
21
+ - Multiple font families
22
+ - Adjustable font size
23
+ - Dark/Light theme toggle
24
+
25
+ ### πŸ“± Mobile-Friendly
26
+ - Responsive design
27
+ - Touch gestures support
28
+ - Swipe navigation for settings
29
+ - Mobile-optimized UI elements
30
+
31
+ ### 🎨 Customization
32
+ - Theme switching (Light/Dark mode)
33
+ - Font family selection
34
+ - Text size adjustment
35
+ - Reading progress tracking
36
+
37
+ ## πŸš€ Getting Started
38
+
39
+ ### Prerequisites
40
+ - Python 3.8+
41
+ - Flask
42
+ - Google Generative AI API key
43
+
44
+ ### Installation
45
+
46
+ 1. Clone the repository
47
+ ```bash
48
+ git clone https://github.com/yourusername/lore-keeper.git
49
+ cd lore-keeper
50
+ ```
51
+ 2. Install dependencies
52
+ ```bash
53
+ pip install -r requirements.txt
54
+ ```
55
+ 3. Set up Google Generative AI API key
56
+ - Obtain an API key from Google Cloud Console
57
+ - Set the `GOOGLE_API_KEY` environment variable
58
+ ```bash
59
+ export GOOGLE_API_KEY='your_api_key_here'
60
+ ```
61
+ 4. Run the application
62
+ ```bash
63
+ python api/app.py
64
+ ```
65
+
66
+ ## πŸ› οΈ Technology Stack
67
+
68
+ - **Frontend**: HTML5, CSS3, JavaScript
69
+ - **Backend**: Flask (Python)
70
+ - **AI**: Google Gemini 1.5 Flash
71
+ - **Styling**: Custom CSS with CSS Variables
72
+ - **Fonts**: Google Fonts (Crimson Text)
73
+
74
+ ## 🎯 Usage
75
+
76
+ 1. Select your preferred genre and theme
77
+ 2. Start reading the generated story
78
+ 3. Scroll to automatically generate more content
79
+ 4. Use the settings panel to customize your reading experience
80
+ 5. Toggle Text-to-Speech for audio narration
81
+
82
+ ## 🎨 Customization Options
83
+
84
+ ### Themes
85
+ - Light Mode
86
+ - Dark Mode
87
+
88
+ ### Fonts
89
+ - Crimson Text
90
+ - Georgia
91
+ - Palatino
92
+ - Baskerville
93
+
94
+ ### Reading Features
95
+ - Adjustable font size
96
+ - Progress tracking
97
+ - Reading time estimation
98
+ - Text-to-Speech
99
+
100
+ ## πŸ“± Mobile Features
101
+
102
+ - Swipe gestures for navigation
103
+ - Mobile-optimized layout
104
+ - Touch-friendly controls
105
+ - Responsive design
106
+
107
+ ## 🀝 Contributing
108
+
109
+ Contributions are welcome! Please feel free to submit a Pull Request.
110
+
111
+ ## πŸ™ Acknowledgments
112
+
113
+ - Google Generative AI for the story generation
114
+ - Flask community for the excellent web framework
115
+
116
+ ---
117
+
118
+ <p align="center">Made with ❀️ for storytelling enthusiasts</p>
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Flask
2
+ google-generativeai
3
+ Flask-Cors
vercel.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "rewrites": [
3
+ { "source": "/(.*)", "destination": "/api/app" }
4
+ ]
5
+ }