Mohammed Foud commited on
Commit
877c261
·
1 Parent(s): 77b5d1f

Add application file

Browse files
Dockerfile CHANGED
@@ -22,8 +22,9 @@ RUN pip install --no-cache-dir --upgrade pip && \
22
  pip install --no-cache-dir --upgrade -r requirements.txt && \
23
  pip install --no-cache-dir --upgrade g4f[all]
24
 
25
- # Copy application code
26
  COPY --chown=user . .
 
27
 
28
  # Expose port
29
  EXPOSE 7860
 
22
  pip install --no-cache-dir --upgrade -r requirements.txt && \
23
  pip install --no-cache-dir --upgrade g4f[all]
24
 
25
+ # Copy application code and .env file
26
  COPY --chown=user . .
27
+ COPY --chown=user .env .env
28
 
29
  # Expose port
30
  EXPOSE 7860
__pycache__/config.cpython-312.pyc CHANGED
Binary files a/__pycache__/config.cpython-312.pyc and b/__pycache__/config.cpython-312.pyc differ
 
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask
2
  from config import Config
3
  from routes.api import api_bp
4
  from routes.views import views_bp
@@ -8,18 +8,7 @@ app.config.from_object(Config)
8
 
9
  # Register blueprints
10
  app.register_blueprint(views_bp)
11
- app.register_blueprint(api_bp)
12
- # app.run(debug=True)
13
- def create_app():
14
- app = Flask(__name__)
15
- app.config.from_object(Config)
16
-
17
- # Register blueprints
18
- app.register_blueprint(views_bp)
19
- app.register_blueprint(api_bp, url_prefix='/api')
20
-
21
- return app
22
 
23
- # if __name__ == '__main__':
24
- # app = create_app()
25
- # app.run(debug=True)
 
1
+ from flask import Flask, url_for
2
  from config import Config
3
  from routes.api import api_bp
4
  from routes.views import views_bp
 
8
 
9
  # Register blueprints
10
  app.register_blueprint(views_bp)
11
+ app.register_blueprint(api_bp, url_prefix='/api')
 
 
 
 
 
 
 
 
 
 
12
 
13
+ if __name__ == '__main__':
14
+ app.run(debug=True)
 
config.py CHANGED
@@ -1,4 +1,8 @@
1
  import os
 
 
 
 
2
 
3
  class Config:
4
  UPLOAD_FOLDER = 'output'
 
1
  import os
2
+ from dotenv import load_dotenv
3
+
4
+ # Load environment variables from .env file
5
+ load_dotenv()
6
 
7
  class Config:
8
  UPLOAD_FOLDER = 'output'
docker-compose.yml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ version: '3'
2
+ services:
3
+ app:
4
+ build: .
5
+ ports:
6
+ - "7860:7860"
7
+ env_file:
8
+ - .env
routes/__pycache__/api.cpython-312.pyc CHANGED
Binary files a/routes/__pycache__/api.cpython-312.pyc and b/routes/__pycache__/api.cpython-312.pyc differ
 
routes/api.py CHANGED
@@ -1,5 +1,5 @@
1
  from typing import List, Tuple
2
- from flask import Blueprint, jsonify, request, Response, send_from_directory, copy_current_request_context
3
  from functools import wraps
4
  import time
5
  import json
@@ -11,10 +11,12 @@ from services.model_provider import ModelProvider
11
  from services.document_generator import DocumentGenerator
12
  from config import Config
13
  from utils.retry_decorator import retry
 
14
 
15
  api_bp = Blueprint('api', __name__)
16
  model_provider = ModelProvider()
17
  doc_generator = DocumentGenerator(Config.UPLOAD_FOLDER)
 
18
 
19
  def sse_stream_required(f):
20
  """Decorator to ensure SSE stream has request context"""
@@ -133,8 +135,15 @@ def stream():
133
  include_references = request.args.get('includeReferences') == 'true'
134
  citation_style = request.args.get('citationStyle')
135
 
 
 
 
 
136
  def generate():
137
  try:
 
 
 
138
  if not research_subject:
139
  yield "data: " + json.dumps({"error": "Research subject is required"}) + "\n\n"
140
  return
@@ -475,8 +484,18 @@ def stream():
475
  "md_file": md_filename
476
  }) + "\n\n"
477
 
 
 
 
 
478
  except Exception as e:
479
- yield "data: " + json.dumps({"error": f"Failed to generate paper: {str(e)}"}) + "\n\n"
 
 
 
 
 
 
480
 
481
  return Response(generate(), mimetype="text/event-stream")
482
 
@@ -488,4 +507,12 @@ def download(filename):
488
  safe_filename,
489
  as_attachment=True
490
  )
 
 
 
 
 
 
 
 
491
 
 
1
  from typing import List, Tuple
2
+ from flask import Blueprint, jsonify, request, Response, send_from_directory, copy_current_request_context, abort, g
3
  from functools import wraps
4
  import time
5
  import json
 
11
  from services.document_generator import DocumentGenerator
12
  from config import Config
13
  from utils.retry_decorator import retry
14
+ import threading
15
 
16
  api_bp = Blueprint('api', __name__)
17
  model_provider = ModelProvider()
18
  doc_generator = DocumentGenerator(Config.UPLOAD_FOLDER)
19
+ _generation_tasks = {}
20
 
21
  def sse_stream_required(f):
22
  """Decorator to ensure SSE stream has request context"""
 
135
  include_references = request.args.get('includeReferences') == 'true'
136
  citation_style = request.args.get('citationStyle')
137
 
138
+ # Generate unique task ID
139
+ task_id = str(uuid.uuid4())
140
+ _generation_tasks[task_id] = {'abort': False}
141
+
142
  def generate():
143
  try:
144
+ # Send task ID to client
145
+ yield "data: " + json.dumps({"task_id": task_id}) + "\n\n"
146
+
147
  if not research_subject:
148
  yield "data: " + json.dumps({"error": "Research subject is required"}) + "\n\n"
149
  return
 
484
  "md_file": md_filename
485
  }) + "\n\n"
486
 
487
+ # Clean up task when done
488
+ if task_id in _generation_tasks:
489
+ del _generation_tasks[task_id]
490
+
491
  except Exception as e:
492
+ # Clean up task on error
493
+ if task_id in _generation_tasks:
494
+ del _generation_tasks[task_id]
495
+ if str(e) == "Generation aborted by user":
496
+ yield "data: " + json.dumps({"status": "aborted"}) + "\n\n"
497
+ else:
498
+ yield "data: " + json.dumps({"error": f"Failed to generate paper: {str(e)}"}) + "\n\n"
499
 
500
  return Response(generate(), mimetype="text/event-stream")
501
 
 
507
  safe_filename,
508
  as_attachment=True
509
  )
510
+
511
+ @api_bp.route('/abort/<task_id>', methods=['POST'])
512
+ def abort_generation(task_id):
513
+ """Abort an ongoing generation task"""
514
+ if task_id in _generation_tasks:
515
+ _generation_tasks[task_id]['abort'] = True
516
+ return jsonify({'status': 'aborted'})
517
+ return jsonify({'status': 'not_found'}), 404
518
 
static/js/main.js ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let currentTaskId = null;
2
+ let currentEventSource = null;
3
+
4
+ document.addEventListener('DOMContentLoaded', function() {
5
+ // Form submission handler
6
+ const form = document.getElementById('paperForm');
7
+ form.addEventListener('submit', startGeneration);
8
+
9
+ // Abort button handler
10
+ const abortBtn = document.getElementById('abortBtn');
11
+ abortBtn.addEventListener('click', abortGeneration);
12
+
13
+ // Structure controls
14
+ const structureRadios = document.querySelectorAll('input[name="structure"]');
15
+ const automaticOptions = document.getElementById('automaticOptions');
16
+
17
+ // References controls
18
+ const includeReferences = document.getElementById('includeReferences');
19
+ const citationStyle = document.getElementById('citationStyle');
20
+ const citationStyleContainer = citationStyle.parentElement;
21
+
22
+ // Chapter count controls
23
+ const chapterCountTypeRadios = document.querySelectorAll('input[name="chapterCountType"]');
24
+ const chapterCountSelect = document.getElementById('chapterCount');
25
+ const customChapterCount = document.getElementById('customChapterCount');
26
+ const chapterCountContainer = chapterCountSelect.parentElement;
27
+
28
+ // Word count controls
29
+ const wordCountTypeRadios = document.querySelectorAll('input[name="wordCountType"]');
30
+ const wordCountSelect = document.getElementById('wordCount');
31
+ const customWordCount = document.getElementById('customWordCount');
32
+ const wordCountContainer = wordCountSelect.parentElement;
33
+
34
+ // Initialize UI state
35
+ initializeControls();
36
+
37
+ // Set up event listeners
38
+ setupEventListeners();
39
+
40
+ function initializeControls() {
41
+ // Structure options
42
+ automaticOptions.style.display =
43
+ document.querySelector('input[name="structure"]:checked').value === 'automatic'
44
+ ? 'block'
45
+ : 'none';
46
+
47
+ // Citation style
48
+ citationStyleContainer.style.display = includeReferences.checked ? 'block' : 'none';
49
+ citationStyle.disabled = !includeReferences.checked;
50
+
51
+ // Chapter count
52
+ const chapterCountType = document.querySelector('input[name="chapterCountType"]:checked').value;
53
+ chapterCountContainer.style.display = chapterCountType === 'manual' ? 'flex' : 'none';
54
+ chapterCountSelect.disabled = chapterCountType !== 'manual';
55
+
56
+ // Word count
57
+ const wordCountType = document.querySelector('input[name="wordCountType"]:checked').value;
58
+ wordCountContainer.style.display = wordCountType === 'manual' ? 'flex' : 'none';
59
+ wordCountSelect.disabled = wordCountType !== 'manual';
60
+
61
+ // Add custom options
62
+ if (!chapterCountSelect.querySelector('option[value="custom"]')) {
63
+ chapterCountSelect.innerHTML += '<option value="custom">Custom...</option>';
64
+ }
65
+ if (!wordCountSelect.querySelector('option[value="custom"]')) {
66
+ wordCountSelect.innerHTML += '<option value="custom">Custom...</option>';
67
+ }
68
+ }
69
+
70
+ function setupEventListeners() {
71
+ // Structure radio changes
72
+ structureRadios.forEach(radio => {
73
+ radio.addEventListener('change', function() {
74
+ automaticOptions.style.display = this.value === 'automatic' ? 'block' : 'none';
75
+ });
76
+ });
77
+
78
+ // References checkbox
79
+ includeReferences.addEventListener('change', function() {
80
+ citationStyleContainer.style.display = this.checked ? 'block' : 'none';
81
+ citationStyle.disabled = !this.checked;
82
+ });
83
+
84
+ // Chapter count type
85
+ chapterCountTypeRadios.forEach(radio => {
86
+ radio.addEventListener('change', function() {
87
+ const isManual = this.value === 'manual';
88
+ chapterCountContainer.style.display = isManual ? 'flex' : 'none';
89
+ chapterCountSelect.disabled = !isManual;
90
+ customChapterCount.classList.add('hidden');
91
+ chapterCountSelect.classList.remove('hidden');
92
+ });
93
+ });
94
+
95
+ // Chapter count select
96
+ chapterCountSelect.addEventListener('change', function() {
97
+ if (this.value === 'custom') {
98
+ this.classList.add('hidden');
99
+ customChapterCount.classList.remove('hidden');
100
+ }
101
+ });
102
+
103
+ // Word count type
104
+ wordCountTypeRadios.forEach(radio => {
105
+ radio.addEventListener('change', function() {
106
+ const isManual = this.value === 'manual';
107
+ wordCountContainer.style.display = isManual ? 'flex' : 'none';
108
+ wordCountSelect.disabled = !isManual;
109
+ customWordCount.classList.add('hidden');
110
+ wordCountSelect.classList.remove('hidden');
111
+ });
112
+ });
113
+
114
+ // Word count select
115
+ wordCountSelect.addEventListener('change', function() {
116
+ if (this.value === 'custom') {
117
+ this.classList.add('hidden');
118
+ customWordCount.classList.remove('hidden');
119
+ }
120
+ });
121
+ }
122
+ });
123
+
124
+ function startGeneration(event) {
125
+ event.preventDefault();
126
+
127
+ const form = event.target;
128
+ const abortBtn = document.getElementById('abortBtn');
129
+ const startBtn = document.getElementById('startBtn');
130
+ const progressContainer = document.getElementById('progressContainer');
131
+ const resultContainer = document.getElementById('resultContainer');
132
+ const errorContainer = document.getElementById('errorContainer');
133
+
134
+ // Show/hide appropriate elements
135
+ startBtn.style.display = 'none';
136
+ abortBtn.style.display = 'block';
137
+ progressContainer.classList.remove('hidden');
138
+ resultContainer.classList.add('hidden');
139
+ errorContainer.classList.add('hidden');
140
+
141
+ // Get form data
142
+ const formData = new FormData(form);
143
+ const queryParams = new URLSearchParams(formData);
144
+
145
+ // Create SSE connection
146
+ currentEventSource = new EventSource(`/api/stream?${queryParams.toString()}`);
147
+
148
+ currentEventSource.onmessage = function(event) {
149
+ const data = JSON.parse(event.data);
150
+
151
+ if (data.task_id) {
152
+ currentTaskId = data.task_id;
153
+ }
154
+
155
+ if (data.status === 'aborted') {
156
+ handleAbort();
157
+ }
158
+
159
+ if (data.error) {
160
+ handleError(data.error);
161
+ }
162
+
163
+ updateProgress(data);
164
+ };
165
+
166
+ currentEventSource.onerror = function() {
167
+ handleError('Connection error occurred');
168
+ };
169
+ }
170
+
171
+ function abortGeneration() {
172
+ if (currentTaskId) {
173
+ fetch(`/api/abort/${currentTaskId}`, {
174
+ method: 'POST'
175
+ }).catch(error => {
176
+ console.error('Failed to abort generation:', error);
177
+ });
178
+
179
+ if (currentEventSource) {
180
+ currentEventSource.close();
181
+ }
182
+ handleAbort();
183
+ }
184
+ }
185
+
186
+ function handleAbort() {
187
+ currentEventSource?.close();
188
+ document.getElementById('abortBtn').style.display = 'none';
189
+ document.getElementById('startBtn').style.display = 'block';
190
+ document.getElementById('progressContainer').classList.add('hidden');
191
+ showError('Generation aborted');
192
+ }
193
+
194
+ function handleError(message) {
195
+ currentEventSource?.close();
196
+ document.getElementById('abortBtn').style.display = 'none';
197
+ document.getElementById('startBtn').style.display = 'block';
198
+ document.getElementById('progressContainer').classList.add('hidden');
199
+ showError(message);
200
+ }
201
+
202
+ function showError(message) {
203
+ const errorContainer = document.getElementById('errorContainer');
204
+ const errorMessage = document.getElementById('errorMessage');
205
+ errorMessage.textContent = message;
206
+ errorContainer.classList.remove('hidden');
207
+ }
208
+
209
+ function updateProgress(data) {
210
+ // Update progress bar
211
+ if (data.progress) {
212
+ const progressBar = document.getElementById('progressBar');
213
+ const progressText = document.getElementById('progressText');
214
+ progressBar.style.width = `${data.progress}%`;
215
+ progressText.textContent = `${Math.round(data.progress)}%`;
216
+ }
217
+
218
+ // Update steps
219
+ if (data.steps) {
220
+ // First render if steps don't exist
221
+ if (!document.getElementById('step-0')) {
222
+ renderSteps(data.steps);
223
+ }
224
+ updateSteps(data.steps);
225
+ }
226
+
227
+ // Update chapter progress if available
228
+ if (data.chapter_progress) {
229
+ updateChapterProgress(data.chapter_progress);
230
+ }
231
+
232
+ // Handle completion
233
+ if (data.status === 'complete' || data.status === 'partial_success') {
234
+ handleCompletion(data);
235
+ }
236
+ }
237
+
238
+ function renderSteps(steps) {
239
+ const progressSteps = document.getElementById('progressSteps');
240
+ progressSteps.innerHTML = ''; // Clear existing steps
241
+
242
+ steps.forEach(step => {
243
+ const stepElement = document.createElement('div');
244
+ stepElement.className = 'flex items-start';
245
+ stepElement.id = `step-${step.id}`;
246
+
247
+ let stepContent = `
248
+ <div class="flex-shrink-0 h-5 w-5 text-gray-400 mt-1">
249
+ <i class="far fa-circle" id="step-icon-${step.id}"></i>
250
+ </div>
251
+ <div class="ml-3">
252
+ <p class="text-sm font-medium text-gray-700" id="step-text-${step.id}">${step.text}</p>
253
+ <p class="text-xs text-gray-500 hidden" id="step-message-${step.id}"></p>
254
+ `;
255
+
256
+ // Add sub-steps if they exist
257
+ if (step.subSteps && step.subSteps.length > 0) {
258
+ stepContent += `<div class="ml-4 mt-2 space-y-2" id="substeps-${step.id}">`;
259
+ step.subSteps.forEach(subStep => {
260
+ stepContent += `
261
+ <div class="flex items-center">
262
+ <div class="flex-shrink-0 h-4 w-4 text-gray-400">
263
+ <i class="far fa-circle" id="substep-icon-${subStep.id}"></i>
264
+ </div>
265
+ <div class="ml-2">
266
+ <p class="text-xs font-medium text-gray-700" id="substep-text-${subStep.id}">${subStep.text}</p>
267
+ <p class="text-xs text-gray-500 hidden" id="substep-message-${subStep.id}"></p>
268
+ </div>
269
+ </div>
270
+ `;
271
+ });
272
+ stepContent += `</div>`;
273
+ }
274
+
275
+ stepContent += `</div>`;
276
+ stepElement.innerHTML = stepContent;
277
+ progressSteps.appendChild(stepElement);
278
+ });
279
+ }
280
+
281
+ function updateSteps(steps) {
282
+ steps.forEach(step => {
283
+ const icon = document.getElementById(`step-icon-${step.id}`);
284
+ const message = document.getElementById(`step-message-${step.id}`);
285
+
286
+ if (icon) {
287
+ icon.className = getStepIconClass(step.status);
288
+ }
289
+
290
+ if (message && step.message) {
291
+ message.textContent = step.message;
292
+ message.classList.remove('hidden');
293
+ }
294
+
295
+ // Update sub-steps if they exist
296
+ if (step.subSteps) {
297
+ step.subSteps.forEach(subStep => {
298
+ const subIcon = document.getElementById(`substep-icon-${subStep.id}`);
299
+ const subMessage = document.getElementById(`substep-message-${subStep.id}`);
300
+
301
+ if (subIcon) {
302
+ subIcon.className = getStepIconClass(subStep.status);
303
+ }
304
+
305
+ if (subMessage && subStep.message) {
306
+ subMessage.textContent = subStep.message;
307
+ subMessage.classList.remove('hidden');
308
+ }
309
+ });
310
+ }
311
+ });
312
+ }
313
+
314
+ function getStepIconClass(status) {
315
+ switch (status) {
316
+ case 'pending': return 'far fa-circle text-gray-400';
317
+ case 'in-progress': return 'fas fa-spinner fa-spin text-indigo-500';
318
+ case 'complete': return 'fas fa-check-circle text-green-500';
319
+ case 'error': return 'fas fa-exclamation-circle text-red-500';
320
+ default: return 'far fa-circle text-gray-400';
321
+ }
322
+ }
323
+
324
+ function handleCompletion(data) {
325
+ const resultContainer = document.getElementById('resultContainer');
326
+ const abortBtn = document.getElementById('abortBtn');
327
+ const startBtn = document.getElementById('startBtn');
328
+
329
+ if (data.docx_file) {
330
+ document.getElementById('downloadDocx').href = `/api/download/${data.docx_file}`;
331
+ }
332
+ if (data.md_file) {
333
+ document.getElementById('downloadMd').href = `/api/download/${data.md_file}`;
334
+ }
335
+
336
+ resultContainer.classList.remove('hidden');
337
+ abortBtn.style.display = 'none';
338
+ startBtn.style.display = 'block';
339
+ }
340
+
341
+ // Handle page unload
342
+ window.addEventListener('beforeunload', function() {
343
+ if (currentTaskId) {
344
+ abortGeneration();
345
+ }
346
+ });
347
+
348
+ function updateChapterProgress(chapterProgress) {
349
+ const container = document.getElementById('chapterProgressContainer');
350
+ const currentChapter = document.getElementById('currentChapter');
351
+ const chapterPercent = document.getElementById('chapterPercent');
352
+ const chapterProgressBar = document.getElementById('chapterProgressBar');
353
+ const chapterTime = document.getElementById('chapterTime');
354
+ const chapterStatus = document.getElementById('chapterStatus');
355
+ const chapterError = document.getElementById('chapterError');
356
+
357
+ container.classList.remove('hidden');
358
+
359
+ if (chapterProgress.chapter) {
360
+ currentChapter.textContent = `Chapter ${chapterProgress.current}/${chapterProgress.total}: ${chapterProgress.chapter}`;
361
+ chapterPercent.textContent = `${Math.round(chapterProgress.percent)}%`;
362
+ chapterProgressBar.style.width = `${chapterProgress.percent}%`;
363
+
364
+ if (chapterProgress.duration) {
365
+ chapterTime.textContent = `Time taken: ${chapterProgress.duration}`;
366
+ }
367
+
368
+ if (chapterProgress.error) {
369
+ chapterStatus.textContent = 'Error';
370
+ chapterError.textContent = chapterProgress.error;
371
+ chapterError.classList.remove('hidden');
372
+ } else {
373
+ chapterStatus.textContent = 'In progress';
374
+ chapterError.classList.add('hidden');
375
+ }
376
+ }
377
+
378
+ if (chapterProgress.complete) {
379
+ chapterStatus.textContent = `Completed ${chapterProgress.total_chapters} chapters`;
380
+ }
381
+ }
templates/index.html CHANGED
@@ -6,6 +6,7 @@
6
  <title>Research Paper Generator</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
9
  </head>
10
  <body class="bg-gray-50 min-h-screen">
11
  <div class="container mx-auto px-4 py-8">
@@ -140,9 +141,11 @@
140
  </div>
141
 
142
  <div class="pt-4">
143
- <button type="submit" id="generateBtn"
144
- class="w-full flex justify-center items-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed">
145
- <i class="fas fa-magic mr-2"></i> Generate Paper
 
 
146
  </button>
147
  </div>
148
  </form>
@@ -217,321 +220,5 @@
217
  </div>
218
  </div>
219
  </div>
220
-
221
- <script>
222
- document.getElementById('paperForm').addEventListener('submit', function(e) {
223
- e.preventDefault();
224
-
225
- const form = e.target;
226
- const generateBtn = document.getElementById('generateBtn');
227
- const progressContainer = document.getElementById('progressContainer');
228
- const chapterProgressContainer = document.getElementById('chapterProgressContainer');
229
- const resultContainer = document.getElementById('resultContainer');
230
- const errorContainer = document.getElementById('errorContainer');
231
- const progressSteps = document.getElementById('progressSteps');
232
- const progressBar = document.getElementById('progressBar');
233
- const progressText = document.getElementById('progressText');
234
-
235
- // Chapter progress elements
236
- const currentChapterEl = document.getElementById('currentChapter');
237
- const chapterPercentEl = document.getElementById('chapterPercent');
238
- const chapterProgressBar = document.getElementById('chapterProgressBar');
239
- const chapterTimeEl = document.getElementById('chapterTime');
240
- const chapterStatusEl = document.getElementById('chapterStatus');
241
- const chapterErrorEl = document.getElementById('chapterError');
242
-
243
- // Reset UI
244
- generateBtn.disabled = true;
245
- progressContainer.classList.remove('hidden');
246
- chapterProgressContainer.classList.add('hidden');
247
- resultContainer.classList.add('hidden');
248
- errorContainer.classList.add('hidden');
249
- progressSteps.innerHTML = '';
250
- progressBar.style.width = '0%';
251
- progressText.textContent = '0%';
252
- chapterProgressBar.style.width = '0%';
253
- chapterPercentEl.textContent = '0%';
254
- chapterErrorEl.classList.add('hidden');
255
-
256
- // Initial steps (will be updated if automatic structure)
257
- let steps = [
258
- {id: 0, text: 'Preparing document structure...', status: 'pending'},
259
- {id: 1, text: 'Generating index/table of contents...', status: 'pending'},
260
- {id: 2, text: 'Determining chapters...', status: 'pending'},
261
- {id: 3, text: 'Writing content...', status: 'pending', subSteps: []},
262
- {id: 4, text: 'Finalizing document...', status: 'pending'},
263
- {id: 5, text: 'Converting to Word format...', status: 'pending'}
264
- ];
265
-
266
- // Function to render steps
267
- function renderSteps() {
268
- progressSteps.innerHTML = '';
269
- steps.forEach(step => {
270
- const stepElement = document.createElement('div');
271
- stepElement.className = 'flex items-start';
272
-
273
- let stepContent = `
274
- <div class="flex-shrink-0 h-5 w-5 text-gray-400 mt-1">
275
- <i class="far fa-circle" id="step-icon-${step.id}"></i>
276
- </div>
277
- <div class="ml-3">
278
- <p class="text-sm font-medium text-gray-700" id="step-text-${step.id}">${step.text}</p>
279
- <p class="text-xs text-gray-500 hidden" id="step-message-${step.id}"></p>
280
- `;
281
-
282
- // Add sub-steps if they exist
283
- if (step.subSteps && step.subSteps.length > 0) {
284
- stepContent += `<div class="ml-4 mt-2 space-y-2" id="substeps-${step.id}">`;
285
- step.subSteps.forEach(subStep => {
286
- stepContent += `
287
- <div class="flex items-center">
288
- <div class="flex-shrink-0 h-4 w-4 text-gray-400">
289
- <i class="far fa-circle" id="substep-icon-${subStep.id}"></i>
290
- </div>
291
- <div class="ml-2">
292
- <p class="text-xs font-medium text-gray-700" id="substep-text-${subStep.id}">${subStep.text}</p>
293
- <p class="text-xs text-gray-500 hidden" id="substep-message-${subStep.id}"></p>
294
- </div>
295
- </div>
296
- `;
297
- });
298
- stepContent += `</div>`;
299
- }
300
-
301
- stepContent += `</div>`;
302
- stepElement.innerHTML = stepContent;
303
- stepElement.id = `step-${step.id}`;
304
- progressSteps.appendChild(stepElement);
305
- });
306
- }
307
-
308
- // Initial render
309
- renderSteps();
310
-
311
- // Get form data
312
- const formData = {
313
- subject: form.subject.value,
314
- model: form.model.value,
315
- structure: form.structure.value
316
- };
317
-
318
- // Create SSE connection
319
- const eventSource = new EventSource(`/stream?subject=${encodeURIComponent(formData.subject)}&model=${encodeURIComponent(formData.model)}&structure=${encodeURIComponent(formData.structure)}`);
320
-
321
- eventSource.onmessage = function(event) {
322
- const data = JSON.parse(event.data);
323
-
324
- if (data.error) {
325
- document.getElementById('errorMessage').textContent = data.error;
326
- errorContainer.classList.remove('hidden');
327
- eventSource.close();
328
- generateBtn.disabled = false;
329
- return;
330
- }
331
-
332
- // Handle chapter progress updates
333
- if (data.chapter_progress) {
334
- chapterProgressContainer.classList.remove('hidden');
335
- const cp = data.chapter_progress;
336
-
337
- if (cp.chapter) {
338
- currentChapterEl.textContent = `Chapter ${cp.current}/${cp.total}: ${cp.chapter}`;
339
- chapterPercentEl.textContent = `${Math.round(cp.percent)}%`;
340
- chapterProgressBar.style.width = `${cp.percent}%`;
341
- chapterStatusEl.textContent = cp.error ? 'Error' : 'In progress';
342
-
343
- if (cp.duration) {
344
- chapterTimeEl.textContent = `Time taken: ${cp.duration}`;
345
- }
346
-
347
- if (cp.error) {
348
- chapterErrorEl.textContent = cp.error;
349
- chapterErrorEl.classList.remove('hidden');
350
- } else {
351
- chapterErrorEl.classList.add('hidden');
352
- }
353
- }
354
-
355
- if (cp.complete) {
356
- chapterStatusEl.textContent = `Completed ${cp.total_chapters} chapters`;
357
- }
358
- }
359
-
360
- if (data.update_steps) {
361
- // Update steps if structure changed (automatic mode)
362
- steps = data.steps;
363
- renderSteps();
364
- }
365
-
366
- if (data.status === 'complete' || data.status === 'partial_success') {
367
- // Show download links
368
- if (data.docx_file) {
369
- document.getElementById('downloadDocx').href = `/download/${data.docx_file}`;
370
- }
371
- if (data.md_file) {
372
- document.getElementById('downloadMd').href = `/download/${data.md_file}`;
373
- }
374
-
375
- resultContainer.classList.remove('hidden');
376
- eventSource.close();
377
- generateBtn.disabled = false;
378
- }
379
-
380
- // Update progress
381
- if (data.progress) {
382
- progressBar.style.width = `${data.progress}%`;
383
- progressText.textContent = `${Math.round(data.progress)}%`;
384
- }
385
-
386
- // Update steps
387
- if (data.steps) {
388
- data.steps.forEach(step => {
389
- const icon = document.getElementById(`step-icon-${step.id}`);
390
- const text = document.getElementById(`step-text-${step.id}`);
391
- const message = document.getElementById(`step-message-${step.id}`);
392
-
393
- if (icon) {
394
- if (step.status === 'pending') {
395
- icon.className = 'far fa-circle text-gray-400';
396
- } else if (step.status === 'in-progress') {
397
- icon.className = 'fas fa-spinner fa-spin text-indigo-500';
398
- } else if (step.status === 'complete') {
399
- icon.className = 'fas fa-check-circle text-green-500';
400
- } else if (step.status === 'error') {
401
- icon.className = 'fas fa-exclamation-circle text-red-500';
402
- }
403
- }
404
-
405
- if (message && step.message) {
406
- message.textContent = step.message;
407
- message.classList.remove('hidden');
408
- }
409
-
410
- // Update sub-steps
411
- if (step.subSteps) {
412
- step.subSteps.forEach(subStep => {
413
- const subIcon = document.getElementById(`substep-icon-${subStep.id}`);
414
- const subText = document.getElementById(`substep-text-${subStep.id}`);
415
- const subMessage = document.getElementById(`substep-message-${subStep.id}`);
416
-
417
- if (subIcon) {
418
- if (subStep.status === 'pending') {
419
- subIcon.className = 'far fa-circle text-gray-400';
420
- } else if (subStep.status === 'in-progress') {
421
- subIcon.className = 'fas fa-spinner fa-spin text-indigo-500';
422
- } else if (subStep.status === 'complete') {
423
- subIcon.className = 'fas fa-check-circle text-green-500';
424
- } else if (subStep.status === 'error') {
425
- subIcon.className = 'fas fa-exclamation-circle text-red-500';
426
- }
427
- }
428
-
429
- if (subMessage && subStep.message) {
430
- subMessage.textContent = subStep.message;
431
- subMessage.classList.remove('hidden');
432
- }
433
- });
434
- }
435
- });
436
- }
437
- };
438
-
439
- eventSource.onerror = function() {
440
- eventSource.close();
441
- generateBtn.disabled = false;
442
- };
443
- });
444
-
445
- document.addEventListener('DOMContentLoaded', function() {
446
- // Get elements
447
- const structureRadios = document.querySelectorAll('input[name="structure"]');
448
- const automaticOptions = document.getElementById('automaticOptions');
449
- const includeReferences = document.getElementById('includeReferences');
450
- const citationStyle = document.getElementById('citationStyle');
451
- const citationStyleContainer = citationStyle.parentElement;
452
-
453
- // Handle structure radio changes
454
- structureRadios.forEach(radio => {
455
- radio.addEventListener('change', function() {
456
- automaticOptions.style.display =
457
- this.value === 'automatic' ? 'block' : 'none';
458
- });
459
- });
460
-
461
- // Initialize automatic options visibility
462
- automaticOptions.style.display =
463
- document.querySelector('input[name="structure"]:checked').value === 'automatic'
464
- ? 'block'
465
- : 'none';
466
-
467
- // Handle references checkbox
468
- includeReferences.addEventListener('change', function() {
469
- citationStyleContainer.style.display = this.checked ? 'block' : 'none';
470
- citationStyle.disabled = !this.checked;
471
- });
472
- // Initialize citation style visibility
473
- citationStyleContainer.style.display = includeReferences.checked ? 'block' : 'none';
474
-
475
- // Chapter count controls
476
- const chapterCountTypeRadios = document.querySelectorAll('input[name="chapterCountType"]');
477
- const chapterCountSelect = document.getElementById('chapterCount');
478
- const customChapterCount = document.getElementById('customChapterCount');
479
- const chapterCountContainer = chapterCountSelect.parentElement;
480
-
481
- chapterCountTypeRadios.forEach(radio => {
482
- radio.addEventListener('change', function() {
483
- const isManual = this.value === 'manual';
484
- chapterCountContainer.style.display = isManual ? 'flex' : 'none';
485
- chapterCountSelect.disabled = !isManual;
486
- customChapterCount.classList.toggle('hidden', true);
487
- chapterCountSelect.classList.toggle('hidden', false);
488
- });
489
- });
490
- // Initialize chapter count visibility
491
- chapterCountContainer.style.display =
492
- document.querySelector('input[name="chapterCountType"]:checked').value === 'manual'
493
- ? 'flex'
494
- : 'none';
495
-
496
- chapterCountSelect.addEventListener('change', function() {
497
- if (this.value === 'custom') {
498
- this.classList.toggle('hidden', true);
499
- customChapterCount.classList.toggle('hidden', false);
500
- }
501
- });
502
-
503
- // Word count controls
504
- const wordCountTypeRadios = document.querySelectorAll('input[name="wordCountType"]');
505
- const wordCountSelect = document.getElementById('wordCount');
506
- const customWordCount = document.getElementById('customWordCount');
507
- const wordCountContainer = wordCountSelect.parentElement;
508
-
509
- wordCountTypeRadios.forEach(radio => {
510
- radio.addEventListener('change', function() {
511
- const isManual = this.value === 'manual';
512
- wordCountContainer.style.display = isManual ? 'flex' : 'none';
513
- wordCountSelect.disabled = !isManual;
514
- customWordCount.classList.toggle('hidden', true);
515
- wordCountSelect.classList.toggle('hidden', false);
516
- });
517
- });
518
- // Initialize word count visibility
519
- wordCountContainer.style.display =
520
- document.querySelector('input[name="wordCountType"]:checked').value === 'manual'
521
- ? 'flex'
522
- : 'none';
523
-
524
- wordCountSelect.addEventListener('change', function() {
525
- if (this.value === 'custom') {
526
- this.classList.toggle('hidden', true);
527
- customWordCount.classList.toggle('hidden', false);
528
- }
529
- });
530
-
531
- // Add custom option to selects
532
- chapterCountSelect.innerHTML += '<option value="custom">Custom...</option>';
533
- wordCountSelect.innerHTML += '<option value="custom">Custom...</option>';
534
- });
535
- </script>
536
  </body>
537
  </html>
 
6
  <title>Research Paper Generator</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <script src="{{ url_for('static', filename='js/main.js') }}" defer></script>
10
  </head>
11
  <body class="bg-gray-50 min-h-screen">
12
  <div class="container mx-auto px-4 py-8">
 
141
  </div>
142
 
143
  <div class="pt-4">
144
+ <button type="submit" id="startBtn" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700">
145
+ <i class="fas fa-play mr-2"></i> Generate Paper
146
+ </button>
147
+ <button type="button" id="abortBtn" style="display: none;" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700">
148
+ <i class="fas fa-stop mr-2"></i> Stop Generation
149
  </button>
150
  </div>
151
  </form>
 
220
  </div>
221
  </div>
222
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  </body>
224
  </html>