Kgshop commited on
Commit
914ebcc
·
verified ·
1 Parent(s): 7ebd7e9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +305 -671
app.py CHANGED
@@ -1,373 +1,296 @@
1
- # --- START OF FILE app (1) (9).py (MODIFIED) ---
2
-
3
  import os
4
  import uuid
5
- from flask import Flask, request, jsonify, Response, send_from_directory
6
- import google.generativeai as genai
7
  import logging
8
  import threading
9
  import time
10
  from datetime import datetime
 
 
11
  from huggingface_hub import HfApi, hf_hub_download
12
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
13
  import requests
14
- import json # Added for load_app_data/save_app_data in generated app
15
-
16
- # Configure logging for the main generator app
17
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
18
-
19
- app = Flask(__name__)
20
-
21
- # INTERNAL API KEY FOR THE GENERATOR ITSELF (FOR GOOGLE GEMINI)
22
- API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns"
23
 
24
- # DIRECTORY FOR STORING GENERATED PYTHON FLASK APPLICATIONS
25
- GENERATED_APPS_DIR = 'generated_apps'
26
 
27
- # REPOSITORY ID FOR DATA SYNCHRONIZATION OF THE *GENERATED* FLASK APPS
28
- GENERATED_APP_REPO_ID = "Kgshop/testsynk"
29
 
30
- if not os.path.exists(GENERATED_APPS_DIR):
31
- os.makedirs(GENERATED_APPS_DIR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- # HTML TEMPLATE FOR THE GENERATOR'S FRONTEND
34
- html_template = """
35
- <!DOCTYPE html>
36
- <html lang="ru">
37
- <head>
38
- <meta charset="UTF-8">
39
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
40
- <title>EVA - Генератор Сайтов</title>
41
- <style>
42
- :root {
43
- --system-gray-100-light: #f2f2f7;
44
- --system-gray-75-light: #f8f8fa;
45
- --system-gray-50-light: #ffffff;
46
- --system-gray-dark-100-light: #000000;
47
- --system-gray-dark-75-light: #1c1c1e;
48
- --system-gray-dark-50-light: #3a3a3c;
49
- --system-gray-light-75-light: #8e8e93;
50
- --system-gray-light-50-light: #aeaeb2;
51
- --system-blue-light: #007aff;
52
- --system-blue-light-hover: #005ecf;
53
- --system-red-light: #ff3b30;
54
- --system-separator-light: rgba(60, 60, 67, 0.29);
55
- --system-separator-opaque-light: #d1d1d6;
56
-
57
- --system-gray-100-dark: #1c1c1e;
58
- --system-gray-75-dark: #2c2c2e;
59
- --system-gray-50-dark: #000000;
60
- --system-gray-dark-100-dark: #ffffff;
61
- --system-gray-dark-75-dark: #f2f2f7;
62
- --system-gray-dark-50-dark: #e5e5ea;
63
- --system-gray-light-75-dark: #8e8e93;
64
- --system-gray-light-50-dark: #636366;
65
- --system-blue-dark: #0a84ff;
66
- --system-blue-dark-hover: #3b9eff;
67
- --system-red-dark: #ff453a;
68
- --system-separator-dark: rgba(84, 84, 88, 0.65);
69
- --system-separator-opaque-dark: #38383a;
70
- }
71
 
72
- @media (prefers-color-scheme: dark) {
73
- :root {
74
- --bg-color: var(--system-gray-50-dark);
75
- --content-bg: var(--system-gray-100-dark);
76
- --text-color: var(--system-gray-dark-100-dark);
77
- --secondary-text-color: var(--system-gray-light-75-dark);
78
- --tertiary-text-color: var(--system-gray-light-50-dark);
79
- --border-color: var(--system-separator-dark);
80
- --border-color-opaque: var(--system-separator-opaque-dark);
81
- --input-bg: var(--system-gray-75-dark);
82
- --primary-color: var(--system-blue-dark);
83
- --primary-color-hover: var(--system-blue-dark-hover);
84
- --error-color: var(--system-red-dark);
85
- }
86
- }
87
 
88
- @media (prefers-color-scheme: light) {
89
- :root {
90
- --bg-color: var(--system-gray-100-light);
91
- --content-bg: var(--system-gray-50-light);
92
- --text-color: var(--system-gray-dark-100-light);
93
- --secondary-text-color: var(--system-gray-light-75-light);
94
- --tertiary-text-color: var(--system-gray-light-50-light);
95
- --border-color: var(--system-separator-light);
96
- --border-color-opaque: var(--system-separator-opaque-light);
97
- --input-bg: var(--system-gray-75-light);
98
- --primary-color: var(--system-blue-light);
99
- --primary-color-hover: var(--system-blue-light-hover);
100
- --error-color: var(--system-red-light);
101
- }
102
- }
103
 
104
- html {
105
- height: -webkit-fill-available;
106
- }
107
 
108
- body {
109
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
110
- margin: 0;
111
- padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom);
112
- background-color: var(--bg-color);
113
- color: var(--text-color);
114
- display: flex;
115
- justify-content: center;
116
- align-items: flex-start;
117
- min-height: 100vh;
118
- min-height: -webkit-fill-available;
119
- line-height: 1.45;
120
- -webkit-font-smoothing: antialiased;
121
- -moz-osx-font-smoothing: grayscale;
122
- }
123
 
124
- .container {
125
- background-color: var(--content-bg);
126
- padding: 25px 30px 30px 30px;
127
- border-radius: 24px;
128
- box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
129
- max-width: 580px;
130
- width: calc(100% - 40px);
131
- box-sizing: border-box;
132
- margin-top: 30px;
133
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
- h1 {
136
- font-size: 32px;
137
- font-weight: 700;
138
- text-align: center;
139
- margin-bottom: 8px;
140
- color: var(--text-color);
141
- letter-spacing: -0.5px;
142
- }
143
 
144
- p.subtitle {
145
- font-size: 17px;
146
- color: var(--secondary-text-color);
147
- text-align: center;
148
- margin-bottom: 35px;
149
- font-weight: 400;
150
- }
151
 
152
- .form-group {
153
- margin-bottom: 28px;
154
- }
 
 
 
 
 
155
 
156
- label.input-label {
157
- display: block;
158
- font-weight: 500;
159
- margin-bottom: 10px;
160
- font-size: 15px;
161
- color: var(--secondary-text-color);
162
- padding-left: 5px;
163
- }
 
 
 
 
164
 
165
- textarea#prompt-input {
166
- width: 100%;
167
- padding: 14px 18px;
168
- border: 1px solid var(--border-color-opaque);
169
- border-radius: 12px;
170
- font-size: 16px;
171
- background-color: var(--input-bg);
172
- color: var(--text-color);
173
- box-sizing: border-box;
174
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
175
- font-family: inherit;
176
- resize: vertical;
177
- min-height: 120px;
178
- }
179
 
180
- textarea#prompt-input:focus {
181
- border-color: var(--primary-color);
182
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent);
183
- outline: none;
184
- }
185
 
186
- button#generate-button {
187
- width: 100%;
188
- padding: 16px;
189
- background-color: var(--primary-color);
190
- color: white;
191
- border: none;
192
- border-radius: 12px;
193
- font-size: 17px;
194
- font-weight: 600;
195
- cursor: pointer;
196
- transition: background-color 0.2s ease, transform 0.1s ease;
197
- margin-top: 15px;
198
- }
199
 
200
- button#generate-button:hover {
201
- background-color: var(--primary-color-hover);
202
- }
 
 
 
 
 
 
203
 
204
- button#generate-button:active {
205
- transform: scale(0.98);
206
- }
207
 
208
- button#generate-button:disabled {
209
- background-color: var(--tertiary-text-color);
210
- cursor: not-allowed;
211
- }
212
 
213
- .output-section {
214
- margin-top: 35px;
215
- }
 
 
 
 
 
 
 
 
216
 
217
- .output-header {
218
- display: flex;
219
- justify-content: space-between;
220
- align-items: center;
221
- margin-bottom: 10px;
222
- }
223
 
224
- label#output-label {
225
- font-weight: 500;
226
- font-size: 15px;
227
- color: var(--secondary-text-color);
228
- padding-left: 5px;
229
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
- button#copy-button {
232
- background-color: transparent;
233
- border: none;
234
- color: var(--primary-color);
235
- font-size: 14px;
236
- font-weight: 500;
237
- cursor: pointer;
238
- padding: 5px 8px;
239
- border-radius: 6px;
240
- transition: background-color 0.2s ease, color 0.2s ease;
241
- display: none;
242
- }
243
 
244
- button#copy-button:hover {
245
- background-color: color-mix(in srgb, var(--primary-color) 15%, transparent);
246
- }
247
 
248
- button#copy-button:active {
249
- background-color: color-mix(in srgb, var(--primary-color) 25%, transparent);
250
- }
 
 
 
251
 
252
- button#copy-button.copied {
253
- color: #34c759;
254
- }
255
- @media (prefers-color-scheme: dark) {
256
- button#copy-button.copied {
257
- color: #30d158;
258
- }
259
- }
260
 
261
- #output-container {
262
- background-color: var(--input-bg);
263
- padding: 18px 20px;
264
- border-radius: 12px;
265
- min-height: 60px;
266
- border: 1px solid var(--border-color);
267
- word-wrap: break-word;
268
- font-size: 15px;
269
- color: var(--text-color);
270
- line-height: 1.5;
271
- transition: border-color 0.2s ease, background-color 0.2s ease;
272
- display: flex;
273
- align-items: center;
274
- justify-content: center;
275
- }
276
-
277
- #output-container a {
278
- color: var(--primary-color);
279
- text-decoration: none;
280
- font-weight: 500;
281
- }
282
 
283
- #output-container a:hover {
284
- text-decoration: underline;
285
- }
286
-
287
-
288
- #output-container.loading::before {
289
- content: "Генерация Python приложения...";
290
- display: block;
291
- text-align: center;
292
- font-style: italic;
293
- color: var(--secondary-text-color);
294
- animation: fadePulse 1.8s infinite ease-in-out;
295
- }
296
 
297
- #output-container.error {
298
- color: var(--error-color);
299
- font-weight: 500;
300
- border-color: color-mix(in srgb, var(--error-color) 50%, transparent);
301
- background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg));
302
- justify-content: flex-start;
303
- }
304
 
305
- @keyframes fadePulse {
306
- 0%, 100% { opacity: 0.6; }
307
- 50% { opacity: 1; }
308
- }
309
 
310
- @media (max-width: 620px) {
311
- body {
312
- padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom);
313
- align-items: flex-start;
314
- }
315
- .container {
316
- padding: 20px 20px 25px 20px;
317
- margin-top: 15px;
318
- border-radius: 20px;
319
- width: calc(100% - 30px);
320
- }
321
- h1 {
322
- font-size: 28px;
323
- }
324
- p.subtitle {
325
- font-size: 16px;
326
- margin-bottom: 25px;
327
- }
328
- .form-group {
329
- margin-bottom: 22px;
330
- }
331
- textarea#prompt-input {
332
- padding: 12px 15px;
333
- min-height: 100px;
334
- }
335
- button#generate-button {
336
- padding: 15px;
337
- font-size: 16px;
338
- }
339
- #output-container {
340
- padding: 15px 18px;
341
- font-size: 14px;
342
- min-height: 50px;
343
- }
344
- .output-section {
345
- margin-top: 30px;
346
- }
347
- }
348
  </style>
349
  </head>
350
  <body>
351
  <div class="container">
352
  <h1>EVA</h1>
353
- <p class="subtitle">Генератор Python приложений на базе ИИ</p>
354
 
355
  <form id="generate-form">
356
  <div class="form-group">
357
- <label for="prompt-input" class="input-label">Опишите Python Flask приложение, которое вы хотите создать</label>
358
- <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай простое Flask-приложение для управления списком задач. Главная страница должна отображать задачи, страница /add - форму для добавления, /delete/<id> - для удаления. Используй JSON-файл как базу данных и функции load_app_data/save_app_data для работы с ним."></textarea>
359
  </div>
360
-
361
- <button type="submit" id="generate-button">Создать приложение</button>
362
  </form>
363
 
364
  <div class="output-section">
365
  <div class="output-header">
366
- <label id="output-label">Ссылка на приложение</label>
367
  <button id="copy-button">Копировать</button>
368
  </div>
369
- <div id="output-container" aria-live="polite">
370
- </div>
371
  </div>
372
  </div>
373
 
@@ -380,401 +303,88 @@ html_template = """
380
 
381
  form.addEventListener('submit', async (event) => {
382
  event.preventDefault();
383
-
384
- if (!promptInput.value.trim()) {
385
- showError("Пожалуйста, опишите приложение, которое вы хотите создать.");
386
- return;
387
- }
388
-
389
- const formData = new FormData(form);
390
-
391
  generateButton.disabled = true;
392
  generateButton.textContent = 'Генерация...';
393
  outputContainer.innerHTML = '';
394
  outputContainer.classList.add('loading');
395
  outputContainer.classList.remove('error');
396
  copyButton.style.display = 'none';
397
- copyButton.textContent = 'Копировать';
398
- copyButton.classList.remove('copied');
399
-
400
 
401
  try {
402
  const response = await fetch('/generate', {
403
  method: 'POST',
404
- body: formData
405
  });
406
-
407
  const result = await response.json();
408
 
409
  if (!response.ok) {
410
  throw new Error(result.error || `Ошибка сервера: ${response.status}`);
411
  }
412
-
 
413
  if (result.download_url) {
414
  const link = document.createElement('a');
415
  link.href = result.download_url;
416
- link.textContent = "Скачать сгенерированное приложение (.py)";
417
- link.target = "_blank";
 
418
  outputContainer.innerHTML = '';
419
  outputContainer.appendChild(link);
 
420
  copyButton.style.display = 'block';
421
  copyButton.dataset.copyText = window.location.origin + result.download_url;
422
- } else if (result.error) {
423
- showError(result.error);
424
  } else {
425
- showError("Не удалось получить ссылку на приложение. Ответ сервера не содержит URL.");
426
  }
427
 
428
  } catch (error) {
429
  console.error("Fetch Error:", error);
430
  showError(`Ошибка: ${error.message}`);
431
- copyButton.style.display = 'none';
432
  } finally {
433
  generateButton.disabled = false;
434
- generateButton.textContent = 'Создать приложение';
435
  outputContainer.classList.remove('loading');
436
  }
437
  });
438
-
439
  copyButton.addEventListener('click', () => {
440
  const textToCopy = copyButton.dataset.copyText;
441
  if (!textToCopy) return;
442
-
443
  navigator.clipboard.writeText(textToCopy).then(() => {
444
  copyButton.textContent = 'Скопировано!';
445
  copyButton.classList.add('copied');
446
- setTimeout(() => {
447
- copyButton.textContent = 'Копировать';
448
- copyButton.classList.remove('copied');
449
- }, 1500);
450
- }).catch(err => {
451
- console.error('Ошибка копирования: ', err);
452
- copyButton.textContent = 'Ошибка';
453
- setTimeout(() => {
454
- copyButton.textContent = 'Копировать';
455
- }, 1500);
456
- });
457
  });
458
 
459
  function showError(message) {
460
- outputContainer.innerHTML = '';
461
- const errorMessageElement = document.createElement('span');
462
- errorMessageElement.textContent = message;
463
- outputContainer.appendChild(errorMessageElement);
464
  outputContainer.classList.add('error');
465
  outputContainer.classList.remove('loading');
466
  copyButton.style.display = 'none';
467
  }
468
-
469
  </script>
470
  </body>
471
  </html>
472
  """
473
 
474
- # INJECTED HUGGING FACE SYNC CODE FOR THE *GENERATED* FLASK APPLICATIONS
475
- _GENERATED_APP_HF_SYNC_SNIPPET = """
476
- # --- Hugging Face Sync System for this Generated App ---
477
- import json
478
- import os
479
- import logging
480
- import threading
481
- import time
482
- from datetime import datetime
483
- from huggingface_hub import HfApi, hf_hub_download
484
- from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
485
- from dotenv import load_dotenv
486
- import requests
487
-
488
- # Load environment variables for this generated app instance
489
- load_dotenv()
490
-
491
- APP_REPO_ID = "{generated_app_repo_id}"
492
- APP_DATA_FILE = 'app_data.json' # Data file specific to this generated app
493
- APP_HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Expect HF_TOKEN to be set in environment for this app
494
- APP_HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Or use APP_HF_TOKEN_WRITE if READ isn't set
495
-
496
- APP_DOWNLOAD_RETRIES = 3
497
- APP_DOWNLOAD_DELAY = 5
498
-
499
- # Configure logging for the generated app
500
- app_logger = logging.getLogger(__name__)
501
- app_logger.setLevel(logging.INFO)
502
- # Prevent adding multiple handlers if this code is reloaded/executed multiple times (e.g. in dev)
503
- if not app_logger.handlers:
504
- handler = logging.StreamHandler()
505
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
506
- handler.setFormatter(formatter)
507
- app_logger.addHandler(handler)
508
-
509
- def _app_download_db_from_hf(specific_file=None, retries=APP_DOWNLOAD_RETRIES, delay=APP_DOWNLOAD_DELAY):
510
- # Ensure tokens are available
511
- token_to_use = APP_HF_TOKEN_READ if APP_HF_TOKEN_READ else APP_HF_TOKEN_WRITE
512
- if not token_to_use:
513
- app_logger.warning("HF_TOKEN_READ/HF_TOKEN not set for generated app. Download might fail for private repos.")
514
-
515
- files_to_download = [specific_file] if specific_file else [APP_DATA_FILE]
516
- app_logger.info(f"Generated app: Attempting download for {files_to_download} from {{APP_REPO_ID}}...")
517
- all_successful = True
518
-
519
- for file_name in files_to_download:
520
- success = False
521
- for attempt in range(retries + 1):
522
- try:
523
- app_logger.info(f"Generated app: Downloading {{file_name}} (Attempt {{attempt + 1}}/{{retries + 1}})...")
524
- local_path = hf_hub_download(
525
- repo_id=APP_REPO_ID,
526
- filename=file_name,
527
- repo_type="dataset",
528
- token=token_to_use,
529
- local_dir=".",
530
- local_dir_use_symlinks=False,
531
- force_download=True,
532
- resume_download=False
533
- )
534
- app_logger.info(f"Generated app: Successfully downloaded {{file_name}} to {{local_path}}.")
535
- success = True
536
- break
537
- except RepositoryNotFoundError:
538
- app_logger.error(f"Generated app: Repository {{APP_REPO_ID}} not found. Download cancelled for all files.")
539
- return False
540
- except HfHubHTTPError as e:
541
- if e.response.status_code == 404:
542
- app_logger.warning(f"Generated app: File {{file_name}} not found in repo {{APP_REPO_ID}} (404). Skipping this file.")
543
- if attempt == 0 and not os.path.exists(file_name):
544
- try:
545
- # Create an empty default JSON file if it's the data file and not found on HF
546
- if file_name == APP_DATA_FILE:
547
- with open(file_name, 'w', encoding='utf-8') as f:
548
- json.dump({{}}, f) # Empty dictionary as default for app data
549
- app_logger.info(f"Generated app: Created empty local file {{file_name}} because it was not found on HF.")
550
- except Exception as create_e:
551
- app_logger.error(f"Generated app: Failed to create empty local file {{file_name}}: {{create_e}}")
552
- success = False
553
- break # Don't retry 404s for the specific file
554
- else:
555
- app_logger.error(f"Generated app: HTTP error downloading {{file_name}} (Attempt {{attempt + 1}}): {{e}}. Retrying in {{delay}}s...")
556
- except requests.exceptions.RequestException as e:
557
- app_logger.error(f"Generated app: Network error downloading {{file_name}} (Attempt {{attempt + 1}}): {{e}}. Retrying in {{delay}}s...")
558
- except Exception as e:
559
- app_logger.error(f"Generated app: Unexpected error downloading {{file_name}} (Attempt {{attempt + 1}}): {{e}}. Retrying in {{delay}}s...", exc_info=True)
560
-
561
- if attempt < retries:
562
- time.sleep(delay)
563
-
564
- if not success:
565
- app_logger.error(f"Generated app: Failed to download {{file_name}} after {{retries + 1}} attempts.")
566
- all_successful = False
567
-
568
- app_logger.info(f"Generated app: Download process finished. Overall success: {{all_successful}}")
569
- return all_successful
570
-
571
- def _app_upload_db_to_hf(specific_file=None):
572
- if not APP_HF_TOKEN_WRITE:
573
- app_logger.warning("Generated app: HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
574
- return
575
- try:
576
- api = HfApi()
577
- files_to_upload = [specific_file] if specific_file else [APP_DATA_FILE]
578
- app_logger.info(f"Generated app: Starting upload of {{files_to_upload}} to HF repo {{APP_REPO_ID}}...")
579
-
580
- for file_name in files_to_upload:
581
- if os.path.exists(file_name):
582
- try:
583
- api.upload_file(
584
- path_or_fileobj=file_name,
585
- path_in_repo=file_name,
586
- repo_id=APP_REPO_ID,
587
- repo_type="dataset",
588
- token=APP_HF_TOKEN_WRITE,
589
- commit_message=f"Sync {{file_name}} from generated app {{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}}"
590
- )
591
- app_logger.info(f"Generated app: File {{file_name}} successfully uploaded to Hugging Face.")
592
- except Exception as e:
593
- app_logger.error(f"Generated app: Error uploading file {{file_name}} to Hugging Face: {{e}}")
594
- else:
595
- app_logger.warning(f"Generated app: File {{file_name}} not found locally, skipping upload.")
596
- app_logger.info("Generated app: Finished uploading files to HF.")
597
- except Exception as e:
598
- app_logger.error(f"Generated app: General error during Hugging Face upload initialization or process: {{e}}", exc_info=True)
599
-
600
- def load_app_data():
601
- default_data = {{}} # AI will assume this is an empty dictionary to start with
602
- try:
603
- with open(APP_DATA_FILE, 'r', encoding='utf-8') as file:
604
- data = json.load(file)
605
- app_logger.info(f"Generated app: Local app data loaded successfully from {{APP_DATA_FILE}}")
606
- if not isinstance(data, dict):
607
- app_logger.warning(f"Generated app: Local {{APP_DATA_FILE}} is not a dictionary. Using default empty data.")
608
- return default_data
609
- return data
610
- except (FileNotFoundError, json.JSONDecodeError):
611
- app_logger.warning(f"Generated app: Local file {{APP_DATA_FILE}} not found or corrupted. Using default empty data.")
612
- # If file not found or corrupted, create an empty one locally
613
- try:
614
- with open(APP_DATA_FILE, 'w', encoding='utf-8') as f:
615
- json.dump(default_data, f)
616
- app_logger.info(f"Generated app: Created empty local file {{APP_DATA_FILE}} for corrupted/missing file.")
617
- except Exception as create_e:
618
- app_logger.error(f"Generated app: Failed to create empty local file {{APP_DATA_FILE}}: {{create_e}}")
619
- return default_data
620
-
621
- def save_app_data(data):
622
- try:
623
- if not isinstance(data, dict):
624
- app_logger.error("Generated app: Attempted to save invalid data structure (not a dict). Aborting save.")
625
- return
626
- with open(APP_DATA_FILE, 'w', encoding='utf-8') as file:
627
- json.dump(data, file, ensure_ascii=False, indent=4)
628
- app_logger.info(f"Generated app: App data successfully saved to {{APP_DATA_FILE}}")
629
- _app_upload_db_to_hf(specific_file=APP_DATA_FILE)
630
- except Exception as e:
631
- app_logger.error(f"Generated app: Error saving app data to {{APP_DATA_FILE}}: {{e}}", exc_info=True)
632
-
633
- def app_periodic_backup():
634
- backup_interval = 1800 # 30 minutes
635
- app_logger.info(f"Generated app: Setting up periodic backup every {{backup_interval}} seconds.")
636
- while True:
637
- time.sleep(backup_interval)
638
- app_logger.info("Generated app: Starting periodic backup...")
639
- _app_upload_db_to_hf(specific_file=APP_DATA_FILE)
640
- app_logger.info("Generated app: Periodic backup finished.")
641
- # --- END Hugging Face Sync System ---
642
- """.format(generated_app_repo_id=GENERATED_APP_REPO_ID)
643
-
644
-
645
- def generate_flask_app_code(user_prompt):
646
- try:
647
- genai.configure(api_key=API_KEY_INTERNAL)
648
- except Exception as e:
649
- logging.error(f"Error configuring GenAI: {e}")
650
- raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
651
-
652
- if not user_prompt or not user_prompt.strip():
653
- raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
654
-
655
- system_instruction = (
656
- "You are an expert Python Flask developer. Your task is to generate the core logic "
657
- "for a single-file Python Flask application based on the user's request. "
658
- "The application should strictly adhere to the following:\n"
659
- "1. Define a Flask application instance: `app = Flask(__name__)`.\n"
660
- "2. Implement basic routes (e.g., `/`, `/add`, `/delete/<id>`) as needed by the user's request.\n"
661
- "3. Manage all application data using two provided functions: `load_app_data()` to read data "
662
- " from `app_data.json`, and `save_app_data(data)` to write data back to `app_data.json`. "
663
- " Assume `load_app_data()` returns an empty dictionary `{}` if the file is new or empty. "
664
- " Your code should define the structure of this dictionary (e.g., `{'items': []}`).\n"
665
- "4. All HTML, CSS, and JavaScript must be embedded directly within Python strings "
666
- " and rendered using `render_template_string()`. Do NOT use `render_template()` with external files, "
667
- " or external CSS/JS links. All visual styles should be in `<style>` tags, all scripts in `<script>` tags.\n"
668
- "5. Ensure any generated HTML is fully self-contained and visually presentable.\n"
669
- "6. Return ONLY the Python code for the Flask application logic. Do NOT include Flask imports, "
670
- " `load_dotenv()`, the `app = Flask(__name__)` line itself, any Hugging Face sync functions, "
671
- " or the `if __name__ == '__main__':` block. These will be added externally. "
672
- " Start your output directly with the first route definition (`@app.route(...)`) or "
673
- " any helper functions, and end just before where `if __name__ == '__main__':` would begin.\n"
674
- "7. Focus on clean, functional Python code that directly implements the user's request "
675
- " using the `load_app_data()` and `save_app_data()` functions.\n"
676
- "8. Include basic error handling and user feedback (e.g., using a simple HTML message)."
677
- )
678
-
679
- full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
680
-
681
- response = None
682
- ai_generated_core_logic = ""
683
-
684
- try:
685
- model = genai.GenerativeModel('learnlm-2.0-flash-experimental')
686
- response = model.generate_content(full_prompt)
687
-
688
- if hasattr(response, 'text') and response.text:
689
- ai_generated_core_logic = response.text
690
- elif response.parts:
691
- ai_generated_core_logic = "".join(part.text for part in response.parts if hasattr(part, 'text'))
692
-
693
- # Attempt to clean up markdown if present
694
- clean_text = ai_generated_core_logic.strip()
695
- if clean_text.startswith("```python"):
696
- clean_text = clean_text[len("```python"):].strip()
697
- if clean_text.endswith("```"):
698
- clean_text = clean_text[:-len("```")].strip()
699
- ai_generated_core_logic = clean_text
700
-
701
- if not ai_generated_core_logic.strip():
702
- if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
703
- reason = response.prompt_feedback.block_reason
704
- raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другой запрос.")
705
- else:
706
- raise ValueError("Модель вернула пустой результат для кода приложения. Попробуйте изменить запрос.")
707
-
708
- # Assemble the full Python Flask application code
709
- full_app_code = (
710
- "from flask import Flask, request, jsonify, render_template_string, redirect, url_for\n"
711
- "import os\n"
712
- "import json\n" # Ensure json is imported for generated app data handling
713
- "# Required for .env loading\n"
714
- "from dotenv import load_dotenv\n"
715
- "# Required for background sync\n"
716
- "import threading\n"
717
- "import time\n"
718
- "# Required for Hugging Face integration\n"
719
- "from huggingface_hub import HfApi, hf_hub_download\n"
720
- "from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError\n"
721
- "import requests\n\n"
722
- + _GENERATED_APP_HF_SYNC_SNIPPET + "\n\n" # Injected HF sync functions
723
- "app = Flask(__name__)\n\n" # Flask app instance
724
- + ai_generated_core_logic + "\n\n" # AI-generated core logic
725
- "if __name__ == '__main__':\n"
726
- " # Initial download of data from Hugging Face for this app instance\n"
727
- " _app_download_db_from_hf(specific_file=APP_DATA_FILE)\n"
728
- " # Load data from the downloaded/local file\n"
729
- " # (load_app_data already handles creation of empty file if needed)\n"
730
- " load_app_data() \n"
731
- " \n"
732
- " # Start periodic backup if write token is available\n"
733
- " if APP_HF_TOKEN_WRITE:\n"
734
- " backup_thread = threading.Thread(target=app_periodic_backup, daemon=True)\n"
735
- " backup_thread.start()\n"
736
- " app_logger.info('Periodic backup thread started for this generated app.')\n"
737
- " else:\n"
738
- " app_logger.warning('Periodic backup will NOT run for this generated app (HF_TOKEN for writing not set).')\n"
739
- " app_logger.info(f'Generated app starting on port 5000.')\n"
740
- " app.run(debug=True, host='0.0.0.0', port=5000)\n" # Standard Flask debug port
741
- )
742
-
743
- return full_app_code
744
-
745
- except Exception as e:
746
- logging.error(f"Error generating content with GenAI: {e}", exc_info=True)
747
- error_message = str(e)
748
- if "API key not valid" in error_message:
749
- raise ValueError("Внутренняя ошибка конфигурации API (неверный ключ Google AI).")
750
- elif "Billing account not found" in error_message or "billing account" in error_message.lower():
751
- raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте настройки оплаты.")
752
- elif "Could not find model" in error_message:
753
- raise ValueError(f"Модель 'learnlm-2.0-flash-experimental' не найдена или недоступна.")
754
- elif "resource has been exhausted" in error_message.lower() or "quota" in error_message.lower():
755
- raise ValueError("Квота запросов исчерпана. Попробуйте позже или проверьте лимиты Google AI.")
756
- elif ("content has been blocked" in error_message.lower() or
757
- (response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason)):
758
- reason = "неизвестна"
759
- if response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
760
- reason = response.prompt_feedback.block_reason.name
761
- elif "safety settings" in error_message.lower():
762
- reason = "настройки безопасности"
763
-
764
- raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
765
- else:
766
- raise ValueError(f"Ошибка при генерации Python-кода приложения: {e}")
767
-
768
-
769
  @app.route('/')
770
  def index():
771
  return Response(html_template, mimetype='text/html')
772
 
773
- @app.route('/generated_apps/<filename>')
774
- def serve_generated_app(filename):
775
- """Serves the generated Python app file for download."""
776
- return send_from_directory(GENERATED_APPS_DIR, filename, as_attachment=True)
777
-
 
 
 
 
 
 
778
 
779
  @app.route('/generate', methods=['POST'])
780
  def handle_generate():
@@ -782,39 +392,63 @@ def handle_generate():
782
  return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
783
 
784
  user_prompt = request.form['prompt']
785
-
786
  if not user_prompt or not user_prompt.strip():
787
  return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
788
 
789
  try:
790
- app_code = generate_flask_app_code(user_prompt)
 
791
 
792
- if not app_code or not app_code.strip():
793
- return jsonify({"error": "Сгенерированный Python-код приложения пуст."}), 500
794
 
795
- filename = f"app_{uuid.uuid4().hex[:8]}.py" # Unique name for the generated app file
796
- filepath = os.path.join(GENERATED_APPS_DIR, filename)
 
 
797
 
798
  with open(filepath, "w", encoding="utf-8") as f:
799
- f.write(app_code)
 
 
 
 
 
 
 
 
 
 
800
 
801
- download_url = f"/generated_apps/{filename}"
 
802
  return jsonify({"download_url": download_url})
803
 
804
  except ValueError as ve:
 
805
  return jsonify({"error": str(ve)}), 400
806
  except Exception as e:
807
- logging.error(f"Unexpected error during app generation: {e}", exc_info=True)
808
- return jsonify({"error": f"Внутренняя ошибка сервера при генерации приложения: {e}"}), 500
 
809
 
 
810
 
811
  if __name__ == '__main__':
812
- # Initial setup for the main generator app (not the generated ones)
813
- logging.info("Main generator application starting up.")
814
- # No HF sync for the generator itself, as it doesn't store its own data on HF in this setup.
 
 
 
 
 
 
 
 
 
 
815
 
816
  port = int(os.environ.get('PORT', 7860))
817
- logging.info(f"Starting main Flask app (generator) on host 0.0.0.0 and port {port}")
818
- app.run(host='0.0.0.0', port=port, debug=False)
819
-
820
- # --- END OF FILE app (1) (9).py (MODIFIED) ---
 
 
 
1
  import os
2
  import uuid
3
+ import json
 
4
  import logging
5
  import threading
6
  import time
7
  from datetime import datetime
8
+ from flask import Flask, request, jsonify, Response, send_from_directory, flash
9
+ import google.generativeai as genai
10
  from huggingface_hub import HfApi, hf_hub_download
11
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
12
  import requests
13
+ from dotenv import load_dotenv
 
 
 
 
 
 
 
 
14
 
15
+ # --- 1. КОНФИГУРАЦИЯ И ИНИЦИАЛИЗАЦИЯ ---
 
16
 
17
+ # Загружаем переменные окружения из файла .env
18
+ load_dotenv()
19
 
20
+ app = Flask(__name__)
21
+ # Секретный ключ нужен для flash-сообщений (хотя мы их здесь не используем, это хорошая практика)
22
+ app.secret_key = os.getenv("FLASK_SECRET_KEY", "a_very_secret_key_for_eva_generator")
23
+
24
+ # --- Конфигурация Hugging Face (из Кода 2) ---
25
+ # Укажите ваш репозиторий на Hugging Face
26
+ REPO_ID = "Kgshop/testsynk"
27
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Токен с правами на запись
28
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE) # Токен для чтения (можно использовать токен на запись)
29
+
30
+ # --- Конфигурация Google AI ---
31
+ # Ключ API теперь берется из переменных окружения
32
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
33
+
34
+ # --- Настройки файловой системы ---
35
+ # Директория для хранения сгенерированных Python-сайтов
36
+ GENERATED_SITES_DIR = 'generated_sites'
37
+ # Имя файла для нашей локальной "базы данных" с метаданными о сайтах
38
+ DATA_FILE = 'generated_sites_db.json'
39
+ # Список файлов для синхронизации с Hugging Face
40
+ SYNC_FILES = [DATA_FILE]
41
+
42
+ # Настройка логирования для отладки
43
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
44
 
45
+ # Создаем директорию для сайтов, если ее нет
46
+ if not os.path.exists(GENERATED_SITES_DIR):
47
+ os.makedirs(GENERATED_SITES_DIR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
+ # --- 2. СИСТЕМА СИНХРОНИЗАЦИИ С HUGGING FACE (из Кода 2) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ DOWNLOAD_RETRIES = 3
53
+ DOWNLOAD_DELAY = 5
 
54
 
55
+ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
56
+ if not HF_TOKEN_READ:
57
+ logging.warning("HF_TOKEN_READ/WRITE not set. Download might fail for private repos.")
58
+
59
+ files_to_download = [specific_file] if specific_file else SYNC_FILES
60
+ logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
61
+ all_successful = True
 
 
 
 
 
 
 
 
62
 
63
+ for file_name in files_to_download:
64
+ success = False
65
+ for attempt in range(retries + 1):
66
+ try:
67
+ logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
68
+ hf_hub_download(
69
+ repo_id=REPO_ID, filename=file_name, repo_type="dataset",
70
+ token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False,
71
+ force_download=True, resume_download=False
72
+ )
73
+ logging.info(f"Successfully downloaded {file_name}.")
74
+ success = True
75
+ break
76
+ except HfHubHTTPError as e:
77
+ if e.response.status_code == 404:
78
+ logging.warning(f"File {file_name} not found in repo {REPO_ID}. Creating empty local file.")
79
+ if not os.path.exists(file_name):
80
+ try:
81
+ if file_name == DATA_FILE:
82
+ with open(file_name, 'w', encoding='utf-8') as f:
83
+ json.dump({'sites': {}}, f) # Структура данных для этого проекта
84
+ logging.info(f"Created empty local file {file_name}.")
85
+ except Exception as create_e:
86
+ logging.error(f"Failed to create empty local file {file_name}: {create_e}")
87
+ success = True # Считаем успехом, т.к. создали пустой файл
88
+ break
89
+ else:
90
+ logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...")
91
+ except Exception as e:
92
+ logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...", exc_info=True)
93
+
94
+ if attempt < retries: time.sleep(delay)
95
 
96
+ if not success:
97
+ logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
98
+ all_successful = False
 
 
 
 
 
99
 
100
+ logging.info(f"Download process finished. Overall success: {all_successful}")
101
+ return all_successful
 
 
 
 
 
102
 
103
+ def upload_db_to_hf(specific_file=None):
104
+ if not HF_TOKEN_WRITE:
105
+ logging.warning("HF_TOKEN_WRITE not set. Skipping upload to Hugging Face.")
106
+ return
107
+ try:
108
+ api = HfApi()
109
+ files_to_upload = [specific_file] if specific_file else SYNC_FILES
110
+ logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
111
 
112
+ for file_name in files_to_upload:
113
+ if os.path.exists(file_name):
114
+ api.upload_file(
115
+ path_or_fileobj=file_name, path_in_repo=file_name,
116
+ repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
117
+ commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
118
+ )
119
+ logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
120
+ else:
121
+ logging.warning(f"File {file_name} not found locally, skipping upload.")
122
+ except Exception as e:
123
+ logging.error(f"Error during Hugging Face upload: {e}", exc_info=True)
124
 
125
+ def periodic_backup():
126
+ backup_interval = 1800 # 30 минут
127
+ logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
128
+ while True:
129
+ time.sleep(backup_interval)
130
+ logging.info("Starting periodic backup...")
131
+ upload_db_to_hf()
132
+ logging.info("Periodic backup finished.")
 
 
 
 
 
 
133
 
134
+ def load_data():
135
+ default_data = {'sites': {}}
136
+ if not os.path.exists(DATA_FILE):
137
+ logging.warning(f"{DATA_FILE} not found locally. Attempting to download from HF.")
138
+ download_db_from_hf(specific_file=DATA_FILE)
139
 
140
+ try:
141
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
142
+ data = json.load(file)
143
+ if not isinstance(data, dict) or 'sites' not in data:
144
+ logging.error(f"Data in {DATA_FILE} is corrupted. Using default.")
145
+ return default_data
146
+ return data
147
+ except (FileNotFoundError, json.JSONDecodeError):
148
+ logging.error(f"Could not read or parse {DATA_FILE}. Using default and creating a new one.")
149
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
150
+ json.dump(default_data, f)
151
+ return default_data
 
152
 
153
+ def save_data(data):
154
+ try:
155
+ with open(DATA_FILE, 'w', encoding='utf-8') as file:
156
+ json.dump(data, file, ensure_ascii=False, indent=4)
157
+ logging.info(f"Data successfully saved to {DATA_FILE}")
158
+ # Запускаем загрузку в HF в отдельном потоке, чтобы не блокировать ответ пользователю
159
+ threading.Thread(target=upload_db_to_hf, args=(DATA_FILE,)).start()
160
+ except Exception as e:
161
+ logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
162
 
 
 
 
163
 
164
+ # --- 3. ЛОГИКА ГЕНЕРАЦИИ КОДА ---
 
 
 
165
 
166
+ def generate_python_flask_app(user_prompt):
167
+ """
168
+ Генерирует код полноценного однофайлового Flask-приложения по запросу пользователя.
169
+ """
170
+ if not GOOGLE_API_KEY:
171
+ raise ValueError("GOOGLE_API_KEY не настроен в переменных окружения.")
172
+
173
+ try:
174
+ genai.configure(api_key=GOOGLE_API_KEY)
175
+ except Exception as e:
176
+ raise ValueError(f"Не удалось настроить Google AI. Проверьте GOOGLE_API_KEY. Ошибка: {e}")
177
 
178
+ if not user_prompt or not user_prompt.strip():
179
+ raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
 
 
 
 
180
 
181
+ # КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: Системный промпт для генерации Python кода
182
+ system_instruction = (
183
+ "You are an expert Python Flask developer. Your task is to generate a complete, single-file Python Flask application "
184
+ "based on the user's request. The entire application must be contained within one single `.py` file.\n"
185
+ "Follow these rules strictly:\n"
186
+ "1. **Self-Contained:** The script must be fully self-contained. All HTML, CSS, and JavaScript must be embedded within Python multi-line string variables (templates). Do not attempt to read external template files.\n"
187
+ "2. **Imports:** Include all necessary imports at the top of the script (e.g., `os`, `json`, `uuid`, `from flask import Flask, request, jsonify, render_template_string, redirect, url_for`).\n"
188
+ "3. **Database:** For data persistence, use a simple JSON file as a database. The script should handle creating, reading, and writing to this JSON file (e.g., `db.json`). The database logic should be part of the generated script.\n"
189
+ "4. **Structure:** The script should define the Flask app, necessary HTML/CSS/JS templates as strings, Flask routes (`@app.route(...)`), and data handling functions. Ensure the app is well-commented and easy to understand.\n"
190
+ "5. **Execution:** The script must include the standard `if __name__ == '__main__':` block to run the Flask development server, making it runnable with a single command `python <filename>.py`.\n"
191
+ "6. **Functionality:** The generated app should be fully functional according to the user's request, visually clean, and user-friendly. Use modern, responsive CSS.\n"
192
+ "7. **Output Format:** Directly output ONLY the raw Python code. Do NOT wrap it in markdown blocks like ```python ... ```. Do not include any explanatory text, titles, or comments before `import` or after the `app.run()` call. The response must start with `import` and end with `)`. "
193
+ "8. **Example App Type:** For a request 'a simple blog', generate a Flask app with routes for viewing all posts, viewing a single post, and a form to add a new post. It should save posts in a `blog_posts.json` file."
194
+ )
195
+
196
+ full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
197
+
198
+ try:
199
+ model = genai.GenerativeModel('gemini-1.5-flash-latest') # Используем современную модель
200
+ response = model.generate_content(full_prompt)
201
 
202
+ # Обработка ответа от новой модели Gemini
203
+ if not response.parts:
204
+ if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
205
+ reason = response.prompt_feedback.block_reason
206
+ raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
207
+ else:
208
+ raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
 
 
 
 
 
209
 
210
+ generated_text = "".join(part.text for part in response.parts)
 
 
211
 
212
+ # Очистка и проверка результата
213
+ clean_text = generated_text.strip()
214
+ if clean_text.lower().startswith("```python"):
215
+ clean_text = clean_text[9:]
216
+ if clean_text.endswith("```"):
217
+ clean_text = clean_text[:-3]
218
 
219
+ if not clean_text.strip().lower().startswith("import"):
220
+ print(f"Warning: Output might not be pure Python. Preview: {clean_text[:200]}")
221
+ # Можно добавить дополнительную логику очистки, если понадобится
 
 
 
 
 
222
 
223
+ return clean_text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
+ except Exception as e:
226
+ logging.error(f"Error generating content with GenAI: {e}")
227
+ error_message = str(e)
228
+ if "API key not valid" in error_message:
229
+ raise ValueError("Внутренняя ошибка: API ключ Google недействителен.")
230
+ elif "quota" in error_message.lower():
231
+ raise ValueError("Квота запросов к Google AI исчерпана. Попробуйте позже.")
232
+ else:
233
+ raise ValueError(f"Ошибка при генерации Python-кода: {e}")
 
 
 
 
234
 
 
 
 
 
 
 
 
235
 
236
+ # --- 4. FRONTEND И ОСНОВНЫЕ ROUTES ---
 
 
 
237
 
238
+ # HTML-шаблон для главной страницы. Обновлен текст и логика JS
239
+ html_template = """
240
+ <!DOCTYPE html>
241
+ <html lang="ru">
242
+ <head>
243
+ <meta charset="UTF-8">
244
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
245
+ <title>EVA - Генератор Python Flask Сайтов</title>
246
+ <style>
247
+ :root {
248
+ --system-gray-100-light: #f2f2f7; --system-gray-75-light: #f8f8fa; --system-gray-50-light: #ffffff; --system-gray-dark-100-light: #000000; --system-gray-dark-75-light: #1c1c1e; --system-gray-dark-50-light: #3a3a3c; --system-gray-light-75-light: #8e8e93; --system-gray-light-50-light: #aeaeb2; --system-blue-light: #007aff; --system-blue-light-hover: #005ecf; --system-red-light: #ff3b30; --system-separator-light: rgba(60, 60, 67, 0.29); --system-separator-opaque-light: #d1d1d6;
249
+ --system-gray-100-dark: #1c1c1e; --system-gray-75-dark: #2c2c2e; --system-gray-50-dark: #000000; --system-gray-dark-100-dark: #ffffff; --system-gray-dark-75-dark: #f2f2f7; --system-gray-dark-50-dark: #e5e5ea; --system-gray-light-75-dark: #8e8e93; --system-gray-light-50-dark: #636366; --system-blue-dark: #0a84ff; --system-blue-dark-hover: #3b9eff; --system-red-dark: #ff453a; --system-separator-dark: rgba(84, 84, 88, 0.65); --system-separator-opaque-dark: #38383a;
250
+ }
251
+ @media (prefers-color-scheme: dark) { :root { --bg-color: var(--system-gray-50-dark); --content-bg: var(--system-gray-100-dark); --text-color: var(--system-gray-dark-100-dark); --secondary-text-color: var(--system-gray-light-75-dark); --tertiary-text-color: var(--system-gray-light-50-dark); --border-color: var(--system-separator-dark); --border-color-opaque: var(--system-separator-opaque-dark); --input-bg: var(--system-gray-75-dark); --primary-color: var(--system-blue-dark); --primary-color-hover: var(--system-blue-dark-hover); --error-color: var(--system-red-dark); } }
252
+ @media (prefers-color-scheme: light) { :root { --bg-color: var(--system-gray-100-light); --content-bg: var(--system-gray-50-light); --text-color: var(--system-gray-dark-100-light); --secondary-text-color: var(--system-gray-light-75-light); --tertiary-text-color: var(--system-gray-light-50-light); --border-color: var(--system-separator-light); --border-color-opaque: var(--system-separator-opaque-light); --input-bg: var(--system-gray-75-light); --primary-color: var(--system-blue-light); --primary-color-hover: var(--system-blue-light-hover); --error-color: var(--system-red-light); } }
253
+ html { height: -webkit-fill-available; }
254
+ body { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom); background-color: var(--bg-color); color: var(--text-color); display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; min-height: -webkit-fill-available; line-height: 1.45; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
255
+ .container { background-color: var(--content-bg); padding: 25px 30px 30px 30px; border-radius: 24px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05); max-width: 580px; width: calc(100% - 40px); box-sizing: border-box; margin-top: 30px; }
256
+ h1 { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 8px; color: var(--text-color); letter-spacing: -0.5px; }
257
+ p.subtitle { font-size: 17px; color: var(--secondary-text-color); text-align: center; margin-bottom: 35px; font-weight: 400; }
258
+ .form-group { margin-bottom: 28px; }
259
+ label.input-label { display: block; font-weight: 500; margin-bottom: 10px; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
260
+ textarea#prompt-input { width: 100%; padding: 14px 18px; border: 1px solid var(--border-color-opaque); border-radius: 12px; font-size: 16px; background-color: var(--input-bg); color: var(--text-color); box-sizing: border-box; transition: border-color 0.2s ease, box-shadow 0.2s ease; font-family: inherit; resize: vertical; min-height: 120px; }
261
+ textarea#prompt-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent); outline: none; }
262
+ button#generate-button { width: 100%; padding: 16px; background-color: var(--primary-color); color: white; border: none; border-radius: 12px; font-size: 17px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; margin-top: 15px; }
263
+ button#generate-button:hover { background-color: var(--primary-color-hover); } button#generate-button:active { transform: scale(0.98); } button#generate-button:disabled { background-color: var(--tertiary-text-color); cursor: not-allowed; }
264
+ .output-section { margin-top: 35px; } .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } label#output-label { font-weight: 500; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
265
+ button#copy-button { background-color: transparent; border: none; color: var(--primary-color); font-size: 14px; font-weight: 500; cursor: pointer; padding: 5px 8px; border-radius: 6px; transition: background-color 0.2s ease, color 0.2s ease; display: none; }
266
+ button#copy-button:hover { background-color: color-mix(in srgb, var(--primary-color) 15%, transparent); } button#copy-button:active { background-color: color-mix(in srgb, var(--primary-color) 25%, transparent); } button#copy-button.copied { color: #34c759; } @media (prefers-color-scheme: dark) { button#copy-button.copied { color: #30d158; } }
267
+ #output-container { background-color: var(--input-bg); padding: 18px 20px; border-radius: 12px; min-height: 60px; border: 1px solid var(--border-color); word-wrap: break-word; font-size: 15px; color: var(--text-color); line-height: 1.5; transition: border-color 0.2s ease, background-color 0.2s ease; display: flex; align-items: center; justify-content: center; }
268
+ #output-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; } #output-container a:hover { text-decoration: underline; }
269
+ #output-container.loading::before { content: "Генерация Python-кода..."; display: block; text-align: center; font-style: italic; color: var(--secondary-text-color); animation: fadePulse 1.8s infinite ease-in-out; }
270
+ #output-container.error { color: var(--error-color); font-weight: 500; border-color: color-mix(in srgb, var(--error-color) 50%, transparent); background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg)); justify-content: flex-start; }
271
+ @keyframes fadePulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
272
+ /* Mobile styles omitted for brevity, they are the same as in original file */
 
 
 
273
  </style>
274
  </head>
275
  <body>
276
  <div class="container">
277
  <h1>EVA</h1>
278
+ <p class="subtitle">Генератор Python Flask сайтов</p>
279
 
280
  <form id="generate-form">
281
  <div class="form-group">
282
+ <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать</label>
283
+ <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай сайт-блог с возможностью добавлять посты (заголовок и текст) и удалять их. Посты должны сохраняться в JSON-файл. Главная страница должна отображать все посты."></textarea>
284
  </div>
285
+ <button type="submit" id="generate-button">Создать сайт</button>
 
286
  </form>
287
 
288
  <div class="output-section">
289
  <div class="output-header">
290
+ <label id="output-label">Ссылка на скачивание</label>
291
  <button id="copy-button">Копировать</button>
292
  </div>
293
+ <div id="output-container" aria-live="polite"></div>
 
294
  </div>
295
  </div>
296
 
 
303
 
304
  form.addEventListener('submit', async (event) => {
305
  event.preventDefault();
306
+ if (!promptInput.value.trim()) { showError("Пожалуйста, опишите сайт."); return; }
307
+
 
 
 
 
 
 
308
  generateButton.disabled = true;
309
  generateButton.textContent = 'Генерация...';
310
  outputContainer.innerHTML = '';
311
  outputContainer.classList.add('loading');
312
  outputContainer.classList.remove('error');
313
  copyButton.style.display = 'none';
 
 
 
314
 
315
  try {
316
  const response = await fetch('/generate', {
317
  method: 'POST',
318
+ body: new FormData(form)
319
  });
 
320
  const result = await response.json();
321
 
322
  if (!response.ok) {
323
  throw new Error(result.error || `Ошибка сервера: ${response.status}`);
324
  }
325
+
326
+ // ИЗМЕНЕНО: Обрабатываем ссылку на скачивание
327
  if (result.download_url) {
328
  const link = document.createElement('a');
329
  link.href = result.download_url;
330
+ link.textContent = "Скачать сгенерированный сайт (.py)";
331
+ link.setAttribute('download', ''); // Атрибут для скачивания
332
+
333
  outputContainer.innerHTML = '';
334
  outputContainer.appendChild(link);
335
+
336
  copyButton.style.display = 'block';
337
  copyButton.dataset.copyText = window.location.origin + result.download_url;
 
 
338
  } else {
339
+ showError(result.error || "Не удалось получить ссылку на скачивание.");
340
  }
341
 
342
  } catch (error) {
343
  console.error("Fetch Error:", error);
344
  showError(`Ошибка: ${error.message}`);
 
345
  } finally {
346
  generateButton.disabled = false;
347
+ generateButton.textContent = 'Создать сайт';
348
  outputContainer.classList.remove('loading');
349
  }
350
  });
351
+
352
  copyButton.addEventListener('click', () => {
353
  const textToCopy = copyButton.dataset.copyText;
354
  if (!textToCopy) return;
 
355
  navigator.clipboard.writeText(textToCopy).then(() => {
356
  copyButton.textContent = 'Скопировано!';
357
  copyButton.classList.add('copied');
358
+ setTimeout(() => { copyButton.textContent = 'Копировать'; copyButton.classList.remove('copied'); }, 1500);
359
+ }).catch(err => { console.error('Ошибка копирования: ', err); });
 
 
 
 
 
 
 
 
 
360
  });
361
 
362
  function showError(message) {
363
+ outputContainer.innerHTML = `<span>${message}</span>`;
 
 
 
364
  outputContainer.classList.add('error');
365
  outputContainer.classList.remove('loading');
366
  copyButton.style.display = 'none';
367
  }
 
368
  </script>
369
  </body>
370
  </html>
371
  """
372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  @app.route('/')
374
  def index():
375
  return Response(html_template, mimetype='text/html')
376
 
377
+ @app.route('/download/<path:filename>')
378
+ def download_generated_site(filename):
379
+ """
380
+ Новый route для скачивания сгенерированных .py файлов.
381
+ """
382
+ logging.info(f"Request to download file: {filename}")
383
+ return send_from_directory(
384
+ GENERATED_SITES_DIR,
385
+ filename,
386
+ as_attachment=True # Этот параметр заставляет браузер скачать файл
387
+ )
388
 
389
  @app.route('/generate', methods=['POST'])
390
  def handle_generate():
 
392
  return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
393
 
394
  user_prompt = request.form['prompt']
 
395
  if not user_prompt or not user_prompt.strip():
396
  return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
397
 
398
  try:
399
+ # 1. Генерируем код Python приложения
400
+ python_code = generate_python_flask_app(user_prompt)
401
 
402
+ if not python_code or not python_code.strip():
403
+ return jsonify({"error": "Сгенерированный код пуст. Попробуйте другой запрос."}), 500
404
 
405
+ # 2. Сохраняем код в .py файл
406
+ site_id = str(uuid.uuid4())
407
+ filename = f"{site_id}.py"
408
+ filepath = os.path.join(GENERATED_SITES_DIR, filename)
409
 
410
  with open(filepath, "w", encoding="utf-8") as f:
411
+ f.write(python_code)
412
+
413
+ # 3. Обновляем нашу базу данных
414
+ data = load_data()
415
+ data['sites'][site_id] = {
416
+ 'id': site_id,
417
+ 'prompt': user_prompt,
418
+ 'filename': filename,
419
+ 'created_at': datetime.now().isoformat()
420
+ }
421
+ save_data(data) # Сохраняем локально и асинхронно отправляем в HF
422
 
423
+ # 4. Возвращаем ссылку на скачивание
424
+ download_url = f"/download/{filename}"
425
  return jsonify({"download_url": download_url})
426
 
427
  except ValueError as ve:
428
+ # Ошибки валидации или от API
429
  return jsonify({"error": str(ve)}), 400
430
  except Exception as e:
431
+ logging.error(f"Unexpected error during site generation: {e}", exc_info=True)
432
+ return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
433
+
434
 
435
+ # --- 5. ЗАПУСК ПРИЛОЖЕНИЯ ---
436
 
437
  if __name__ == '__main__':
438
+ logging.info("Application starting up...")
439
+
440
+ # Первоначальная синхронизация при старте
441
+ logging.info("Performing initial data sync from Hugging Face...")
442
+ download_db_from_hf()
443
+
444
+ # Запуск периодического резервного копирования в фоновом потоке
445
+ if HF_TOKEN_WRITE:
446
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
447
+ backup_thread.start()
448
+ logging.info("Periodic backup thread started.")
449
+ else:
450
+ logging.warning("Periodic backup thread will NOT run (HF_TOKEN_WRITE not set).")
451
 
452
  port = int(os.environ.get('PORT', 7860))
453
+ logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
454
+ app.run(host='0.0.0.0', port=port, debug=False)