Kgshop commited on
Commit
0927a6e
·
verified ·
1 Parent(s): 3ce946e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +779 -337
app.py CHANGED
@@ -1,55 +1,362 @@
 
 
1
  import os
2
  import uuid
3
- import subprocess
4
- import sys
5
- import shutil
6
- from flask import Flask, request, jsonify, Response # Removed send_from_directory
 
 
 
7
  import google.generativeai as genai
 
 
8
  from dotenv import load_dotenv
9
- import logging
10
- import socket # For finding available port
11
 
12
- load_dotenv() # Load .env file for API keys
 
13
 
14
  app = Flask(__name__)
 
 
15
 
16
- # --- Configuration ---
17
- GOOGLE_AI_API_KEY = os.getenv("GOOGLE_AI_API_KEY")
 
 
18
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
19
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Can be the same as HF_TOKEN if fine-grained access is not used
20
 
21
- REPO_ID = "Kgshop/testsynk" # Target Hugging Face repository
 
 
22
 
23
- GENERATED_APPS_DIR = 'generated_apps'
24
- BASE_PORT_FOR_GENERATED_APPS = 7861 # Start assigning ports from here for generated apps
25
 
26
- # In-memory store for running generated app processes
27
- running_generated_apps = {} # {app_id: {'process': process_obj, 'port': port, 'url': url}}
 
 
28
 
29
- # --- Logging ---
30
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
31
 
32
- if not os.path.exists(GENERATED_APPS_DIR):
33
- os.makedirs(GENERATED_APPS_DIR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- if not GOOGLE_AI_API_KEY:
36
- logging.warning("GOOGLE_AI_API_KEY not found in environment variables. AI generation will fail.")
37
- if not HF_TOKEN_WRITE:
38
- logging.warning("HF_TOKEN (for writing) not found. Generated apps might not be able to save/upload data to Hugging Face.")
39
- if not HF_TOKEN_READ and HF_TOKEN_WRITE:
40
- logging.info("HF_TOKEN_READ not found, will use HF_TOKEN (write token) for read operations in generated apps.")
41
- elif not HF_TOKEN_READ and not HF_TOKEN_WRITE:
42
- logging.warning("Neither HF_TOKEN nor HF_TOKEN_READ found. Generated apps will likely fail HF operations.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
 
44
 
45
- # --- Main UI Template (Modified) ---
46
- html_template = """
47
  <!DOCTYPE html>
48
  <html lang="ru">
49
  <head>
50
  <meta charset="UTF-8">
51
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
52
- <title>EVA - Генератор Python Веб-Приложений</title>
 
53
  <style>
54
  :root {
55
  --system-gray-100-light: #f2f2f7;
@@ -105,7 +412,7 @@ html_template = """
105
  --secondary-text-color: var(--system-gray-light-75-light);
106
  --tertiary-text-color: var(--system-gray-light-50-light);
107
  --border-color: var(--system-separator-light);
108
- --border-color-opaque: var(--system-separator-opaque-light);
109
  --input-bg: var(--system-gray-75-light);
110
  --primary-color: var(--system-blue-light);
111
  --primary-color-hover: var(--system-blue-light-hover);
@@ -114,7 +421,7 @@ html_template = """
114
  }
115
 
116
  html {
117
- height: -webkit-fill-available;
118
  }
119
 
120
  body {
@@ -138,7 +445,7 @@ html_template = """
138
  padding: 25px 30px 30px 30px;
139
  border-radius: 24px;
140
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
141
- max-width: 580px;
142
  width: calc(100% - 40px);
143
  box-sizing: border-box;
144
  margin-top: 30px;
@@ -190,9 +497,9 @@ html_template = """
190
  }
191
 
192
  textarea#prompt-input:focus {
193
- border-color: var(--primary-color);
194
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent);
195
- outline: none;
196
  }
197
 
198
  button#generate-button {
@@ -227,10 +534,10 @@ html_template = """
227
  }
228
 
229
  .output-header {
230
- display: flex;
231
- justify-content: space-between;
232
- align-items: center;
233
- margin-bottom: 10px;
234
  }
235
 
236
  label#output-label {
@@ -250,24 +557,24 @@ html_template = """
250
  padding: 5px 8px;
251
  border-radius: 6px;
252
  transition: background-color 0.2s ease, color 0.2s ease;
253
- display: none; /* Initially hidden */
254
  }
255
 
256
  button#copy-button:hover {
257
  background-color: color-mix(in srgb, var(--primary-color) 15%, transparent);
258
  }
259
-
260
  button#copy-button:active {
261
  background-color: color-mix(in srgb, var(--primary-color) 25%, transparent);
262
  }
263
-
264
  button#copy-button.copied {
265
- color: #34c759;
266
  }
267
  @media (prefers-color-scheme: dark) {
268
- button#copy-button.copied {
269
- color: #30d158;
270
- }
271
  }
272
 
273
  #output-container {
@@ -285,7 +592,7 @@ html_template = """
285
  align-items: center;
286
  justify-content: center;
287
  }
288
-
289
  #output-container a {
290
  color: var(--primary-color);
291
  text-decoration: none;
@@ -296,9 +603,8 @@ html_template = """
296
  text-decoration: underline;
297
  }
298
 
299
-
300
  #output-container.loading::before {
301
- content: "Генерация Python приложения...";
302
  display: block;
303
  text-align: center;
304
  font-style: italic;
@@ -319,7 +625,142 @@ html_template = """
319
  50% { opacity: 1; }
320
  }
321
 
322
- @media (max-width: 620px) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  body {
324
  padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom);
325
  align-items: flex-start;
@@ -333,55 +774,105 @@ html_template = """
333
  h1 {
334
  font-size: 28px;
335
  }
336
- p.subtitle {
337
  font-size: 16px;
338
  margin-bottom: 25px;
339
  }
340
  .form-group {
341
  margin-bottom: 22px;
342
  }
343
- textarea#prompt-input {
344
  padding: 12px 15px;
345
  min-height: 100px;
346
- }
347
  button#generate-button {
348
- padding: 15px;
349
- font-size: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  }
351
- #output-container {
352
- padding: 15px 18px;
353
- font-size: 14px;
354
- min-height: 50px;
355
- }
356
- .output-section {
357
- margin-top: 30px;
358
- }
359
  }
360
  </style>
361
  </head>
362
  <body>
363
  <div class="container">
364
  <h1>EVA</h1>
365
- <p class="subtitle">Генератор Python Веб-Приложений с ИИ и Hugging Face</p>
 
 
 
 
 
 
 
 
366
 
367
  <form id="generate-form">
368
  <div class="form-group">
369
- <label for="prompt-input" class="input-label">Опишите веб-приложение, которое вы хотите создать</label>
370
- <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай простой блог с возможностью добавлять посты (заголовок, текст). Посты должны сохраняться. Используй минималистичный диза��н."></textarea>
371
  </div>
372
 
373
- <button type="submit" id="generate-button">Создать приложение</button>
374
  </form>
375
 
376
  <div class="output-section">
377
- <div class="output-header">
378
- <label id="output-label">Ссылка на запущенное приложение</label>
379
- <button id="copy-button">Копировать</button>
380
- </div>
381
  <div id="output-container" aria-live="polite">
382
- <!-- Сообщение о том, что нужно ввести для начала -->
383
  </div>
384
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  </div>
386
 
387
  <script>
@@ -395,7 +886,7 @@ html_template = """
395
  event.preventDefault();
396
 
397
  if (!promptInput.value.trim()) {
398
- showError("Пожалуйста, опишите приложение, которое вы хотите создать.");
399
  return;
400
  }
401
 
@@ -403,7 +894,7 @@ html_template = """
403
 
404
  generateButton.disabled = true;
405
  generateButton.textContent = 'Генерация...';
406
- outputContainer.innerHTML = ''; // Clear previous content
407
  outputContainer.classList.add('loading');
408
  outputContainer.classList.remove('error');
409
  copyButton.style.display = 'none';
@@ -425,33 +916,29 @@ html_template = """
425
  if (result.site_url) {
426
  const link = document.createElement('a');
427
  link.href = result.site_url;
428
- link.textContent = "Открыть сгенерированное приложение";
429
- link.target = "_blank"; // Open in new tab
430
- outputContainer.innerHTML = ''; // Clear loading message
431
  outputContainer.appendChild(link);
432
  copyButton.style.display = 'block';
433
- copyButton.dataset.copyText = result.site_url; // No need for window.location.origin
434
- if (result.app_id) {
435
- const appIdText = document.createElement('p');
436
- appIdText.textContent = `ID приложения: ${result.app_id}. Его данные будут храниться в Hugging Face репозитории ${result.repo_id} в папке sites_data/${result.app_id}/.`;
437
- appIdText.style.fontSize = "0.8em";
438
- appIdText.style.marginTop = "10px";
439
- appIdText.style.textAlign = "center";
440
- outputContainer.appendChild(appIdText);
441
- }
442
-
443
  } else if (result.error) {
444
  showError(result.error);
445
  } else {
446
- showError("Не удалось получить ссылку на приложение. Ответ сервера не содержит URL.");
447
  }
448
 
449
  } catch (error) {
450
  console.error("Fetch Error:", error);
451
  showError(`Ошибка: ${error.message}`);
 
452
  } finally {
453
  generateButton.disabled = false;
454
- generateButton.textContent = 'Создать приложение';
455
  outputContainer.classList.remove('loading');
456
  }
457
  });
@@ -464,291 +951,246 @@ html_template = """
464
  copyButton.textContent = 'Скопировано!';
465
  copyButton.classList.add('copied');
466
  setTimeout(() => {
467
- copyButton.textContent = 'Копировать';
468
- copyButton.classList.remove('copied');
469
  }, 1500);
470
  }).catch(err => {
471
  console.error('Ошибка копирования: ', err);
472
  copyButton.textContent = 'Ошибка';
473
- setTimeout(() => {
474
- copyButton.textContent = 'Копировать';
475
  }, 1500);
476
  });
477
  });
478
 
479
  function showError(message) {
480
- outputContainer.innerHTML = '';
481
- const errorMessageElement = document.createElement('span');
482
- errorMessageElement.textContent = message;
483
- outputContainer.appendChild(errorMessageElement);
484
- outputContainer.classList.add('error');
485
- outputContainer.classList.remove('loading');
486
- copyButton.style.display = 'none';
487
  }
 
488
  </script>
489
  </body>
490
  </html>
491
  """
492
 
493
- # --- Helper function to find an available port ---
494
- def find_available_port(start_port):
495
- port = start_port
496
- while True:
497
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
498
- if s.connect_ex(('localhost', port)) != 0:
499
- return port
500
- port += 1
501
- if port > 65535: # Exhausted common ports
502
- raise RuntimeError("Could not find an available port.")
503
-
504
- # --- AI Code Generation Function (Heavily Modified) ---
505
- def generate_python_app_from_prompt(user_prompt):
506
- if not GOOGLE_AI_API_KEY:
507
- raise ValueError("GOOGLE_AI_API_KEY не настроен. Генерация невозможна.")
508
- try:
509
- genai.configure(api_key=GOOGLE_AI_API_KEY)
510
- except Exception as e:
511
- logging.error(f"Error configuring GenAI: {e}")
512
- raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
513
-
514
- if not user_prompt or not user_prompt.strip():
515
- raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
 
517
- # This is the core instruction to the AI
518
- system_instruction = f"""
519
- You are an expert Python Flask web developer. Your task is to generate a complete, single-file Python Flask application based on the user's request.
520
- The application MUST be self-contained in one `.py` file.
521
-
522
- **Core Requirements for the Generated Python Flask App:**
523
- 1. **Single File:** All Python code, HTML templates (as Python triple-quoted strings), CSS (embedded in `<style>` tags within HTML strings), and JavaScript (embedded in `<script>` tags within HTML strings, preferably before `</body>`) must be in this single `.py` file.
524
- 2. **Flask Setup:**
525
- * Import necessary modules: `os, uuid, json, threading, time, datetime, logging, shutil, socket`.
526
- * Flask imports: `Flask, request, jsonify, Response, render_template_string, redirect, url_for`.
527
- * Hugging Face imports: `HfApi, hf_hub_download, delete_files` (from `huggingface_hub`), `RepositoryNotFoundError, HfHubHTTPError` (from `huggingface_hub.utils`).
528
- * Werkzeug imports: `secure_filename` (from `werkzeug.utils`).
529
- * The app should get its `PORT` from the `PORT` environment variable.
530
- * The app should get its unique `APP_ID` from the `APP_ID` environment variable. This `APP_ID` is crucial for namespacing data on Hugging Face.
531
- * The Hugging Face `REPO_ID` is fixed: "{REPO_ID}".
532
- * `HF_TOKEN_WRITE` and `HF_TOKEN_READ` should be obtained from environment variables.
533
- 3. **Data Persistence with Hugging Face:**
534
- * The application must manage its primary data (e.g., blog posts, to-do items) using a local JSON file named `app_data.json` (store this name in a variable like `LOCAL_DATA_FILE`).
535
- * This `LOCAL_DATA_FILE` must be backed up to and restored from the Hugging Face Hub repository (`{REPO_ID}`).
536
- * The path for this data file in the Hugging Face repo MUST be `sites_data/{{APP_ID}}/app_data.json` (store this in a variable like `DATA_FILE_PATH_IN_REPO`).
537
- * Implement the following Python functions for data persistence:
538
- * `download_file_from_hf_to_local(repo_file_path, local_file_path, retries=3, delay=5)`: Downloads a file from `repo_file_path` on HF to `local_file_path`. Handles 404s by returning `False`. Uses `shutil.move` to place the downloaded file correctly from a temporary download location.
539
- * `upload_local_file_to_hf(local_file_path, repo_file_path, commit_message=None)`: Uploads `local_file_path` to `repo_file_path` on HF.
540
- * `load_app_data()`:
541
- * Tries to load data from `LOCAL_DATA_FILE`.
542
- * If local fails, calls `download_file_from_hf_to_local` for `DATA_FILE_PATH_IN_REPO`.
543
- * If download also fails (e.g., 404 on first run for this `APP_ID`), it initializes `app_data` with a default structure (e.g., `{{'items': []}}` or based on user prompt's site type) and then calls `save_app_data()` to create it locally and on HF.
544
- * Stores data in a global variable (e.g., `app_data`).
545
- * `save_app_data()`: Saves the global `app_data` to `LOCAL_DATA_FILE` and then calls `upload_local_file_to_hf` to sync to HF.
546
- * `periodic_app_data_backup()`: A thread function that periodically calls `upload_local_file_to_hf` for `LOCAL_DATA_FILE`.
547
- * On startup (`if __name__ == '__main__':`), the app must call `load_app_data()` and start the `periodic_app_data_backup` thread if `HF_TOKEN_WRITE` is available.
548
- 4. **File Uploads (if applicable to the user's request):**
549
- * If the user requests features involving file uploads (e.g., images for posts):
550
- * Uploaded files must be stored on Hugging Face in the path `sites_data/{{APP_ID}}/uploads/{{filename_in_repo}}`.
551
- * Implement a helper function like `handle_file_upload(flask_file_storage_object, desired_filename_in_repo=None)`:
552
- * Takes a Flask `FileStorage` object.
553
- * Saves it temporarily locally.
554
- * Uploads it to the constructed HF path using `upload_local_file_to_hf`.
555
- * Returns the public Hugging Face URL of the uploaded file (e.g., `https://huggingface.co/datasets/{REPO_ID}/resolve/main/sites_data/{{APP_ID}}/uploads/{{filename_in_repo}}`) or an error.
556
- * Cleans up the local temporary file.
557
- 5. **Logging:**
558
- * Implement basic logging using the `logging` module. Prefix log messages with the `APP_ID` for clarity: `logging.basicConfig(level=logging.INFO, format=f'%(asctime)s - %(levelname)s - {{APP_ID}} - %(message)s')`.
559
- 6. **Structure:**
560
- * Start with all necessary imports.
561
- * Define HF configuration constants/variables (`APP_ID` from env, `REPO_ID`, `HF_TOKEN_WRITE` from env, `HF_TOKEN_READ` from env, `LOCAL_DATA_FILE`, `DATA_FILE_PATH_IN_REPO`, `UPLOADED_FILES_PATH_IN_REPO_BASE`).
562
- * Define logging setup.
563
- * Implement all Hugging Face helper functions (`download_file_from_hf_to_local`, `upload_local_file_to_hf`, `load_app_data`, `save_app_data`, `periodic_app_data_backup`, and `handle_file_upload` if needed).
564
- * Define the Flask `app = Flask(__name__)`.
565
- * Define a global variable for application data, e.g., `app_data = {{}}`.
566
- * Define HTML templates as multi-line strings (e.g., `INDEX_HTML_TEMPLATE = """..."""`).
567
- * Define Flask routes (`@app.route(...)`) as requested by the user, using `render_template_string` and interacting with `app_data` and `save_app_data()`.
568
- * The main execution block: `if __name__ == '__main__':` should:
569
- * Get `PORT` from `os.environ.get("PORT", 5001)`.
570
- * Set `APP_ID = os.environ.get("APP_ID", "default_generated_app_" + str(uuid.uuid4()))`.
571
- * Call `load_app_data()`.
572
- * Start the `periodic_app_data_backup` thread.
573
- * Run the Flask app: `app.run(host='0.0.0.0', port=port, debug=False)`.
574
- 7. **Output Format:** Directly output ONLY the Python code starting with import statements and ending with `app.run(...)`. Do not include any explanatory text, markdown formatting (like ```python), or anything else before or after the Python code itself. Make the site visually appealing and functional.
575
-
576
- User's request for the website: "{user_prompt}"
577
  """
578
-
579
- full_prompt = system_instruction # Removed redundant f-string here
580
-
581
- response = None
582
- try:
583
- # model = genai.GenerativeModel('gemini-1.5-flash-latest') # Or your preferred model
584
- model = genai.GenerativeModel('learnlm-2.0-flash-experimental') # Using the one from original code
585
-
586
- logging.info("Generating Python app code with AI...")
587
- response = model.generate_content(full_prompt)
588
-
589
- generated_text = ""
590
- if hasattr(response, 'text') and response.text:
591
- generated_text = response.text
592
- elif hasattr(response, 'parts') and response.parts: # Check if parts exist
593
- generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
594
-
595
- # This resolve logic might be specific or potentially not needed with newer client libraries
596
- if not generated_text.strip().lower().startswith("import ") and not generated_text.strip().lower().startswith("#"):
597
- try:
598
- # response.resolve() # This method might not exist or work as expected.
599
- # GenAI client usually resolves content automatically.
600
- # If it's consistently returning partials, the model or parameters might need adjustment.
601
- if hasattr(response, 'text') and response.text: # Re-check after potential resolve
602
- generated_text = response.text
603
- elif hasattr(response, 'parts') and response.parts:
604
- generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
605
- except Exception as resolve_e:
606
- logging.warning(f"response.resolve() failed or did not change output: {resolve_e}")
607
-
608
-
609
- if not generated_text.strip():
610
- if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
611
- reason = response.prompt_feedback.block_reason
612
- raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
613
- else:
614
- raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
615
-
616
- clean_text = generated_text.strip()
617
- if clean_text.startswith("```python"):
618
- clean_text = clean_text[9:]
619
- if clean_text.endswith("```"):
620
- clean_text = clean_text[:-3]
621
- generated_text = clean_text.strip()
622
- elif clean_text.startswith("```"): # More generic markdown block
623
- clean_text = clean_text[3:]
624
- if clean_text.endswith("```"):
625
- clean_text = clean_text[:-3]
626
- generated_text = clean_text.strip()
627
-
628
- # A very basic check for Python code
629
- if not (clean_text.lower().startswith("import ") or clean_text.lower().startswith("#") or "def " in clean_text.lower() or "app = flask(" in clean_text.lower()):
630
- logging.warning(f"Output might not be Python code. Preview: {clean_text[:300]}")
631
- # Consider raising an error if it's clearly not Python
632
-
633
- return generated_text
634
-
635
- except Exception as e:
636
- logging.error(f"Error generating content with GenAI: {e}", exc_info=True)
637
- error_message = str(e)
638
- # ... (error handling from original code, slightly adapted) ...
639
- if "API key not valid" in error_message or "API_KEY_INVALID" in error_message:
640
- raise ValueError("Неверный или отсутствующий GOOGLE_AI_API_KEY.")
641
- elif "Billing account not found" in error_message or "billing account" in error_message.lower():
642
- raise ValueError("Проблема с биллингом аккаунта Google Cloud.")
643
- elif "Could not find model" in error_message:
644
- raise ValueError(f"Модель не найдена или недоступна.")
645
- elif "resource has been exhausted" in error_message.lower() or "quota" in error_message.lower():
646
- raise ValueError("Квота запросов Google AI исчерпана. Попробуйте позже.")
647
- elif ("content has been blocked" in error_message.lower() or
648
- (response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason)):
649
- reason = "неизвестна"
650
- if response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
651
- reason = str(response.prompt_feedback.block_reason) # Make sure it's a string
652
- elif "safety settings" in error_message.lower():
653
- reason = "настройки безопасности"
654
- raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
655
- else:
656
- raise ValueError(f"Ошибка при генерации Python-кода приложения: {e}")
657
 
 
658
 
659
  @app.route('/')
660
  def index():
661
- return Response(html_template, mimetype='text/html')
662
-
663
- # Removed /sites/<filename> route as we are not serving static HTML files this way anymore.
 
 
 
 
664
 
665
  @app.route('/generate', methods=['POST'])
666
  def handle_generate():
 
 
 
 
667
  if 'prompt' not in request.form:
668
  return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
669
 
670
  user_prompt = request.form['prompt']
671
 
672
  if not user_prompt or not user_prompt.strip():
673
- return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
674
 
675
  try:
676
- python_app_code = generate_python_app_from_prompt(user_prompt)
677
-
678
- if not python_app_code or not python_app_code.strip():
679
- return jsonify({"error": "Сгенерированный Python-код пуст."}), 500
680
-
681
- app_id = str(uuid.uuid4())
682
- filename = f"{app_id}.py"
683
- filepath = os.path.join(GENERATED_APPS_DIR, filename)
684
-
685
- with open(filepath, "w", encoding="utf-8") as f:
686
- f.write(python_app_code)
687
-
688
- # Find an available port for the new app
689
- assigned_port = find_available_port(BASE_PORT_FOR_GENERATED_APPS + len(running_generated_apps))
690
-
691
- # Prepare environment for the subprocess
692
- env = os.environ.copy()
693
- env['PORT'] = str(assigned_port)
694
- env['APP_ID'] = app_id
695
- env['REPO_ID'] = REPO_ID # Pass repo_id as well
696
- if HF_TOKEN_WRITE: env['HF_TOKEN'] = HF_TOKEN_WRITE # For older name compatibility
697
- if HF_TOKEN_WRITE: env['HF_TOKEN_WRITE'] = HF_TOKEN_WRITE
698
- if HF_TOKEN_READ: env['HF_TOKEN_READ'] = HF_TOKEN_READ
699
- elif HF_TOKEN_WRITE: env['HF_TOKEN_READ'] = HF_TOKEN_WRITE # Use write token if read token not set
700
-
701
- # Launch the generated Python Flask app as a subprocess
702
- logging.info(f"Attempting to launch generated app {app_id} on port {assigned_port} from {filepath}")
703
-
704
- # Use sys.executable to ensure it's the same Python interpreter
705
- # Add current directory to PYTHONPATH for the subprocess to find modules if needed, though single-file apps shouldn't need this.
706
- # env['PYTHONPATH'] = os.getcwd() + os.pathsep + env.get('PYTHONPATH', '')
707
-
708
- process = subprocess.Popen([sys.executable, filepath], env=env)
709
-
710
- # Give it a moment to start (optional, but can help avoid race conditions for immediate access)
711
- # time.sleep(3) # Consider if this is necessary
712
-
713
- site_url = f"http://localhost:{assigned_port}/" # Assuming generated app runs on localhost for now
714
- running_generated_apps[app_id] = {'process': process, 'port': assigned_port, 'url': site_url}
715
-
716
- logging.info(f"Generated app {app_id} launched. URL: {site_url}")
717
- return jsonify({"site_url": site_url, "app_id": app_id, "repo_id": REPO_ID})
718
 
719
- except ValueError as ve:
720
- logging.error(f"ValueError during generation: {ve}")
721
- return jsonify({"error": str(ve)}), 400
722
- except RuntimeError as re: # For port finding issues
723
- logging.error(f"RuntimeError: {re}")
724
- return jsonify({"error": str(re)}), 500
725
- except Exception as e:
726
- logging.error(f"Unexpected error during app generation or launch: {e}", exc_info=True)
727
- return jsonify({"error": f"Внутренняя ошибка сервера при генерации или запуске приложения: {e}"}), 500
728
 
729
- # TODO: Add a way to stop/manage generated apps, e.g., an admin endpoint or on generator shutdown.
730
- # For now, they will keep running until the main generator app is stopped or they crash.
731
 
732
- def cleanup_running_apps():
733
- logging.info("Shutting down generator. Terminating running generated applications...")
734
- for app_id, app_info in running_generated_apps.items():
735
- logging.info(f"Terminating app {app_id} (PID: {app_info['process'].pid})")
736
- try:
737
- app_info['process'].terminate() # Send SIGTERM
738
- app_info['process'].wait(timeout=5) # Wait for graceful shutdown
739
- except subprocess.TimeoutExpired:
740
- logging.warning(f"App {app_id} did not terminate gracefully. Sending SIGKILL.")
741
- app_info['process'].kill() # Force kill
742
- except Exception as e:
743
- logging.error(f"Error terminating app {app_id}: {e}")
744
- logging.info("All tracked generated applications have been issued termination signals.")
745
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
 
747
  if __name__ == '__main__':
748
- import atexit
749
- atexit.register(cleanup_running_apps) # Register cleanup function
 
 
 
 
 
 
 
 
 
 
 
 
 
750
 
751
  port = int(os.environ.get('PORT', 7860))
752
- debug_mode = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
753
- logging.info(f"Starting EVA Generator on host 0.0.0.0 and port {port}, Debug: {debug_mode}")
754
- app.run(host='0.0.0.0', port=port, debug=debug_mode)
 
1
+ --- START OF FILE app (1) (9).py ---
2
+
3
  import os
4
  import uuid
5
+ import json
6
+ import logging
7
+ import threading
8
+ import time
9
+ from datetime import datetime
10
+
11
+ from flask import Flask, request, jsonify, Response, render_template_string, flash, redirect, url_for
12
  import google.generativeai as genai
13
+ from huggingface_hub import HfApi, hf_hub_download
14
+ from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
15
  from dotenv import load_dotenv
 
 
16
 
17
+ # Загружаем переменные окружения из файла .env
18
+ load_dotenv()
19
 
20
  app = Flask(__name__)
21
+ # Устанавливаем секретный ключ для работы flash-сообщений
22
+ app.secret_key = os.getenv("FLASK_SECRET_KEY", "your_unique_secret_key_for_eva_app")
23
 
24
+ # --- Конфигурация Hugging Face ---
25
+ # ID репозитория на Hugging Face для хранения метаданных сайтов
26
+ REPO_ID = "Kgshop/testsynk"
27
+ # Токены для доступа к Hugging Face (для записи и чтения)
28
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
29
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
30
 
31
+ # Файл, который будет синхронизироваться с Hugging Face. В нем хранятся метаданные всех сгенерированных сайтов.
32
+ DATA_FILE = 'generated_sites_metadata.json'
33
+ SYNC_FILES = [DATA_FILE]
34
 
35
+ # Каталог для потенциальных временных файлов или статических ассетов (в данной реализации используется минимально)
36
+ GENERATED_SITES_DIR = 'generated_sites'
37
 
38
+ # Настройки для скачивания/загрузки
39
+ DOWNLOAD_RETRIES = 3
40
+ DOWNLOAD_DELAY = 5
41
+ BACKUP_INTERVAL_SECONDS = 1800 # 30 минут для периодического бэкапа
42
 
43
+ # Настройка логирования
44
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
45
 
46
+ # Убедимся, что каталог для сгенерированных сайтов существует (хотя сами сайты теперь не сохраняются как отдельные файлы)
47
+ if not os.path.exists(GENERATED_SITES_DIR):
48
+ os.makedirs(GENERATED_SITES_DIR)
49
+
50
+ # --- Утилитарные функции для работы с Hugging Face (адаптировано из Кода 2) ---
51
+
52
+ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
53
+ """
54
+ Скачивает файлы базы данных с Hugging Face Hub.
55
+ Если файл не найден на HF и локально отсутствует, создает пустой.
56
+ """
57
+ if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
58
+ logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE не установлены. Скачивание может завершиться неудачей для приватных репозиториев.")
59
+
60
+ token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
61
+
62
+ files_to_download = [specific_file] if specific_file else SYNC_FILES
63
+ logging.info(f"Попытка скачивания файлов {files_to_download} из репозитория {REPO_ID}...")
64
+ all_successful = True
65
+
66
+ for file_name in files_to_download:
67
+ success = False
68
+ for attempt in range(retries + 1):
69
+ try:
70
+ logging.info(f"Скачивание {file_name} (Попытка {attempt + 1}/{retries + 1})...")
71
+ local_path = hf_hub_download(
72
+ repo_id=REPO_ID,
73
+ filename=file_name,
74
+ repo_type="dataset",
75
+ token=token_to_use,
76
+ local_dir=".",
77
+ local_dir_use_symlinks=False,
78
+ force_download=True,
79
+ resume_download=False
80
+ )
81
+ logging.info(f"Файл {file_name} успешно скачан в {local_path}.")
82
+ success = True
83
+ break
84
+ except RepositoryNotFoundError:
85
+ logging.error(f"Репозиторий {REPO_ID} не ��айден. Скачивание отменено для всех файлов.")
86
+ return False
87
+ except HfHubHTTPError as e:
88
+ if e.response.status_code == 404:
89
+ logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID} (404). Пропускаем этот файл.")
90
+ if attempt == 0 and not os.path.exists(file_name):
91
+ try:
92
+ if file_name == DATA_FILE:
93
+ with open(file_name, 'w', encoding='utf-8') as f:
94
+ json.dump({}, f) # Создаем пустой JSON-объект для метаданных
95
+ logging.info(f"Создан пустой локальный файл {file_name}, так как он не был найден на HF.")
96
+ except Exception as create_e:
97
+ logging.error(f"Не удалось создать пустой локальный файл {file_name}: {create_e}")
98
+ success = False # Все равно считаем, что для этого файла не удалось
99
+ break
100
+ else:
101
+ logging.error(f"HTTP-ошибка при скачивании {file_name} (Попытка {attempt + 1}): {e}. Повторная попытка через {delay}с...")
102
+ except Exception as e:
103
+ logging.error(f"Неожиданная ошибка при скачивании {file_name} (Попытка {attempt + 1}): {e}. Повторная попытка через {delay}с...", exc_info=True)
104
+
105
+ if attempt < retries:
106
+ time.sleep(delay)
107
+
108
+ if not success:
109
+ logging.error(f"Не удалось скачать {file_name} после {retries + 1} попыток.")
110
+ all_successful = False
111
+
112
+ logging.info(f"Процесс скачивания завершен. Общий успех: {all_successful}")
113
+ return all_successful
114
+
115
+ def upload_db_to_hf(specific_file=None):
116
+ """
117
+ Загружает файлы базы данных на Hugging Face Hub.
118
+ """
119
+ if not HF_TOKEN_WRITE:
120
+ logging.warning("HF_TOKEN (для записи) не установлен. Пропускаем загрузку на Hugging Face.")
121
+ return
122
+
123
+ try:
124
+ api = HfApi()
125
+ files_to_upload = [specific_file] if specific_file else SYNC_FILES
126
+ logging.info(f"Начало загрузки файлов {files_to_upload} в репозиторий HF {REPO_ID}...")
127
+
128
+ for file_name in files_to_upload:
129
+ if os.path.exists(file_name):
130
+ try:
131
+ api.upload_file(
132
+ path_or_fileobj=file_name,
133
+ path_in_repo=file_name,
134
+ repo_id=REPO_ID,
135
+ repo_type="dataset",
136
+ token=HF_TOKEN_WRITE,
137
+ commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
138
+ )
139
+ logging.info(f"Файл {file_name} успешно загружен на Hugging Face.")
140
+ except Exception as e:
141
+ logging.error(f"Ошибка загрузки файла {file_name} на Hugging Face: {e}")
142
+ else:
143
+ logging.warning(f"Файл {file_name} не найден локально, пропускаем загрузку.")
144
+ logging.info("Загрузка файлов на HF завершена.")
145
+ except Exception as e:
146
+ logging.error(f"Общая ошибка при инициализации или процессе загрузки Hugging Face: {e}", exc_info=True)
147
+
148
+ def periodic_backup():
149
+ """
150
+ Запускает периодическое резервное копирование данных на Hugging Face.
151
+ """
152
+ logging.info(f"Настройка периодического резервного копирования каждые {BACKUP_INTERVAL_SECONDS} секунд.")
153
+ while True:
154
+ time.sleep(BACKUP_INTERVAL_SECONDS)
155
+ logging.info("Начало периодического резервного копирования...")
156
+ upload_db_to_hf()
157
+ logging.info("Периодическое резервное копирование завершено.")
158
+
159
+ # --- Функции для сохранения/загрузки метаданных сгенерированных сайтов (адаптировано из Кода 2) ---
160
+
161
+ def load_site_metadata():
162
+ """
163
+ Загружает метаданные сгенерированных сайтов из DATA_FILE.
164
+ Если файл не найден или поврежден, пытается скачать с Hugging Face.
165
+ """
166
+ default_data = {}
167
+ try:
168
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
169
+ data = json.load(file)
170
+ logging.info(f"Локальные метаданные сайта успешно загружены из {DATA_FILE}")
171
+ if not isinstance(data, dict):
172
+ logging.warning(f"Локальный файл {DATA_FILE} не является словарем. Попытка скачивания.")
173
+ raise FileNotFoundError # Считаем поврежденным, пытаемся скачать
174
+ return data
175
+ except FileNotFoundError:
176
+ logging.warning(f"Локальный файл {DATA_FILE} не найден. Попытка скачивания с HF.")
177
+ except json.JSONDecodeError:
178
+ logging.error(f"Ошибка декодирования JSON в локальном файле {DATA_FILE}. Файл может быть поврежден. Попытка скачивания.")
179
+
180
+ if download_db_from_hf(specific_file=DATA_FILE):
181
+ try:
182
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
183
+ data = json.load(file)
184
+ logging.info(f"Метаданные сайта успешно загружены из {DATA_FILE} после скачивания.")
185
+ if not isinstance(data, dict):
186
+ logging.error(f"Скачанный файл {DATA_FILE} не является словарем. Используем по умолчанию.")
187
+ return default_data
188
+ return data
189
+ except (FileNotFoundError, json.JSONDecodeError) as e:
190
+ logging.error(f"Ошибка загрузки {DATA_FILE} даже после успешного скачивания: {e}. Используем по умолчанию.")
191
+ return default_data
192
+ except Exception as e:
193
+ logging.error(f"Неизвестная ошибка при загрузке скачанного файла {DATA_FILE}: {e}. Используем по умолчанию.", exc_info=True)
194
+ return default_data
195
+ else:
196
+ logging.error(f"Не удалось скачать {DATA_FILE} с HF после нескольких попыток. Используем пустую структуру данных по умолчанию.")
197
+ if not os.path.exists(DATA_FILE):
198
+ try:
199
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
200
+ json.dump(default_data, f)
201
+ logging.info(f"Создан пустой локальный файл {DATA_FILE} после неудачного скачивания.")
202
+ except Exception as create_e:
203
+ logging.error(f"Не удалось создать пустой локальный файл {DATA_FILE}: {create_e}")
204
+ return default_data
205
+
206
+ def save_site_metadata(data):
207
+ """
208
+ Сохраняет метаданные сгенерированных сайтов в DATA_FILE и загружает на Hugging Face.
209
+ """
210
+ try:
211
+ if not isinstance(data, dict):
212
+ logging.error("Попытка сохранить недопустимую структуру данных (не словарь) для метаданных сайта. Отмена сохранения.")
213
+ return
214
+ with open(DATA_FILE, 'w', encoding='utf-8') as file:
215
+ json.dump(data, file, ensure_ascii=False, indent=2) # indent=2 для читаемости JSON
216
+ logging.info(f"Метаданные сайта успешно сохранены в {DATA_FILE}")
217
+ upload_db_to_hf(specific_file=DATA_FILE)
218
+ except Exception as e:
219
+ logging.error(f"Ошибка сохранения метаданных сайта в {DATA_FILE}: {e}", exc_info=True)
220
+
221
+ # --- Конфигурация Google Generative AI и промпт ---
222
+
223
+ # Google API ключ для генерации контента
224
+ API_KEY_INTERNAL = os.getenv("GOOGLE_API_KEY")
225
+
226
+ def generate_site_json_from_prompt(user_prompt):
227
+ """
228
+ Генерирует JSON-объект, описывающий структуру сайта, на основе запроса пользователя,
229
+ используя Google Generative AI.
230
+ """
231
+ try:
232
+ genai.configure(api_key=API_KEY_INTERNAL)
233
+ except Exception as e:
234
+ logging.error(f"Ошибка настройки GenAI: {e}")
235
+ raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
236
+
237
+ if not user_prompt or not user_prompt.strip():
238
+ raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
239
+
240
+ system_instruction = (
241
+ "Ты экспертный ИИ, который разрабатывает простые, функциональные одностраничные веб-приложения. "
242
+ "Когда пользователь описывает веб-сайт, сгенерируй JSON-объект, который строго соответствует следующей схеме. "
243
+ "Этот JSON-объект будет использоваться Flask-приложением для динамического рендеринга сайта. "
244
+ "Не включай HTML, Markdown или пояснительный текст за пределами JSON. Выводи только JSON-строку. "
245
+ "JSON должен быть валидным и напрямую парсируемым. "
246
+ "Для URL-адресов изображений используй общие сервисы-заполнители, такие как `https://placehold.co/600x400?text=Image` или аналогичные, "
247
+ "либо опиши их текстом, если заполнители не подходят. "
248
+ "Убедись, что любой текстовый контент, содержащий символы новой строки, представлен с помощью `\\n` в JSON-строке."
249
+ "\n\nJSON Schema:\n"
250
+ "```json\n"
251
+ "{\n"
252
+ " \"site_title\": \"string\",\n"
253
+ " \"main_heading\": \"string\",\n"
254
+ " \"tagline\": \"string\",\n"
255
+ " \"sections\": [\n"
256
+ " {\n"
257
+ " \"title\": \"string\",\n"
258
+ " \"content\": \"string\",\n"
259
+ " \"type\": \"text\" // or \"list\", \"contact\", \"image_gallery\"\n"
260
+ " }\n"
261
+ " ],\n"
262
+ " \"data_items\": [ // Это действует как \"база данных\" для простых списков/продуктов/элементов портфолио\n"
263
+ " {\n"
264
+ " \"id\": \"unique_string_or_number\",\n"
265
+ " \"name\": \"string\",\n"
266
+ " \"description\": \"string\",\n"
267
+ " \"price\": \"number (optional)\",\n"
268
+ " \"image_url\": \"string (optional, используй заполнитель или общее изображение)\",\n"
269
+ " \"category\": \"string (optional)\",\n"
270
+ " \"fields\": { \"key\": \"value\" } // произвольные дополнительные поля\n"
271
+ " }\n"
272
+ " ],\n"
273
+ " \"contact_info\": { // Дополнительная контактная информация\n"
274
+ " \"email\": \"string (optional)\",\n"
275
+ " \"phone\": \"string (optional)\",\n"
276
+ " \"address\": \"string (optional)\"\n"
277
+ " },\n"
278
+ " \"footer_text\": \"string (optional)\"\n"
279
+ "}\n"
280
+ "```\n"
281
+ "- `sections`: Массив блоков контента.\n"
282
+ " - `type: \"text\"` для общего текстового контента.\n"
283
+ " - `type: \"list\"`, если нужно отобразить элементы из `data_items`. Убедись, что `data_items` заполнен, если используется этот тип.\n"
284
+ " - `type: \"contact\"`, если это раздел контактов, будет использоваться `contact_info`. В `content` может быть дополнительный текст.\n"
285
+ " - `type: \"image_gallery\"`, если запрошена галерея. Для изображений используй URL-адреса заполнителей. Массив `sections[].images` может быть добавлен, если необходимо.\n"
286
+ "- `data_items`: Массив для простых продуктов, услуг, элементов портфолио и т.д. Они будут отображаться в виде карточек, если присутствует раздел типа 'list'.\n"
287
+ "- Убедись, что все строки в JSON правильно экранированы, особенно символы новой строки (используй `\\n`).\n"
288
+ "- Выводи ТОЛЬКО JSON-код. Не включай никакого пояснительного текста или форматирования Markdown (например, ```json) за пределами JSON."
289
+ )
290
+
291
+ full_prompt = f"{system_instruction}\n\nПользовательский запрос: \"{user_prompt}\""
292
+
293
+ response = None
294
+ try:
295
+ # Используем 'gemini-1.5-flash-latest' для лучшего следования JSON-формату
296
+ model = genai.GenerativeModel('gemini-1.5-flash-latest')
297
+ response = model.generate_content(full_prompt)
298
+
299
+ generated_text = ""
300
+ if hasattr(response, 'text') and response.text:
301
+ generated_text = response.text
302
+ elif response.parts:
303
+ generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
304
+
305
+ if not generated_text.strip():
306
+ if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
307
+ reason = response.prompt_feedback.block_reason
308
+ raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другой запрос.")
309
+ else:
310
+ raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
311
 
312
+ # Удаляем возможное форматирование Markdown (```json...```)
313
+ clean_text = generated_text.strip()
314
+ if clean_text.startswith("```json"):
315
+ clean_text = clean_text[7:]
316
+ if clean_text.endswith("```"):
317
+ clean_text = clean_text[:-3]
318
+ generated_text = clean_text.strip()
319
+
320
+ # Пытаемся распарсить как JSON для валидации
321
+ parsed_json = json.loads(generated_text)
322
+ return parsed_json
323
+
324
+ except json.JSONDecodeError as jde:
325
+ logging.error(f"Вывод ИИ был невалидным JSON: {generated_text[:500]}... Ошибка: {jde}")
326
+ raise ValueError("Модель сгенерировала невалидный JSON. Пожалуйста, попробуйте еще раз или измените запрос.")
327
+ except Exception as e:
328
+ logging.error(f"Ошибка генерации контента с GenAI: {e}", exc_info=True)
329
+ error_message = str(e)
330
+ if "API key not valid" in error_message or "Invalid API key" in error_message:
331
+ raise ValueError("Внутренняя ошибка конфигурации API. Проверьте ключ Google API.")
332
+ elif "Billing account not found" in error_message or "billing account" in error_message.lower():
333
+ raise ValueError("Проблема с биллингом аккаунта Google Cloud. Возможно, аккаунт не настроен или лимит исчерпан.")
334
+ elif "Could not find model" in error_message:
335
+ raise ValueError(f"Модель 'gemini-1.5-flash-latest' не найдена или недоступна. Возможно, используйте 'learnlm-2.0-flash-experimental'.")
336
+ elif "resource has been exhausted" in error_message.lower() or "quota" in error_message.lower():
337
+ raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
338
+ elif ("content has been blocked" in error_message.lower() or
339
+ (response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason)):
340
+ reason = "неизвестна"
341
+ if response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
342
+ reason = response.prompt_feedback.block_reason
343
+ elif "safety settings" in error_message.lower():
344
+ reason = "настройки безопасности"
345
+ raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
346
+ else:
347
+ raise ValueError(f"Ошибка при генерации данных сайта: {e}")
348
 
349
+ # --- HTML-шаблоны для UI EVA и для сгенерированных сайтов ---
350
 
351
+ # Основной HTML-шаблон для главной страницы EVA, включающий форму и список сгенерированных сайтов
352
+ index_page_template = """
353
  <!DOCTYPE html>
354
  <html lang="ru">
355
  <head>
356
  <meta charset="UTF-8">
357
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
358
+ <title>EVA - Генератор Сайтов</title>
359
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
360
  <style>
361
  :root {
362
  --system-gray-100-light: #f2f2f7;
 
412
  --secondary-text-color: var(--system-gray-light-75-light);
413
  --tertiary-text-color: var(--system-gray-light-50-light);
414
  --border-color: var(--system-separator-light);
415
+ --border-color-opaque: var(--system-separator-opaque-light);
416
  --input-bg: var(--system-gray-75-light);
417
  --primary-color: var(--system-blue-light);
418
  --primary-color-hover: var(--system-blue-light-hover);
 
421
  }
422
 
423
  html {
424
+ height: -webkit-fill-available;
425
  }
426
 
427
  body {
 
445
  padding: 25px 30px 30px 30px;
446
  border-radius: 24px;
447
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
448
+ max-width: 780px; /* Increased max-width */
449
  width: calc(100% - 40px);
450
  box-sizing: border-box;
451
  margin-top: 30px;
 
497
  }
498
 
499
  textarea#prompt-input:focus {
500
+ border-color: var(--primary-color);
501
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent);
502
+ outline: none;
503
  }
504
 
505
  button#generate-button {
 
534
  }
535
 
536
  .output-header {
537
+ display: flex;
538
+ justify-content: space-between;
539
+ align-items: center;
540
+ margin-bottom: 10px;
541
  }
542
 
543
  label#output-label {
 
557
  padding: 5px 8px;
558
  border-radius: 6px;
559
  transition: background-color 0.2s ease, color 0.2s ease;
560
+ display: none;
561
  }
562
 
563
  button#copy-button:hover {
564
  background-color: color-mix(in srgb, var(--primary-color) 15%, transparent);
565
  }
566
+
567
  button#copy-button:active {
568
  background-color: color-mix(in srgb, var(--primary-color) 25%, transparent);
569
  }
570
+
571
  button#copy-button.copied {
572
+ color: #34c759;
573
  }
574
  @media (prefers-color-scheme: dark) {
575
+ button#copy-button.copied {
576
+ color: #30d158;
577
+ }
578
  }
579
 
580
  #output-container {
 
592
  align-items: center;
593
  justify-content: center;
594
  }
595
+
596
  #output-container a {
597
  color: var(--primary-color);
598
  text-decoration: none;
 
603
  text-decoration: underline;
604
  }
605
 
 
606
  #output-container.loading::before {
607
+ content: "Генерация сайта...";
608
  display: block;
609
  text-align: center;
610
  font-style: italic;
 
625
  50% { opacity: 1; }
626
  }
627
 
628
+ .site-list-section {
629
+ margin-top: 50px;
630
+ border-top: 1px solid var(--border-color);
631
+ padding-top: 30px;
632
+ }
633
+
634
+ .site-list-section h2 {
635
+ font-size: 24px;
636
+ font-weight: 600;
637
+ text-align: center;
638
+ margin-bottom: 25px;
639
+ color: var(--text-color);
640
+ }
641
+
642
+ .site-cards-grid {
643
+ display: grid;
644
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
645
+ gap: 25px;
646
+ }
647
+
648
+ .site-card {
649
+ background-color: var(--system-gray-75-light);
650
+ border-radius: 16px;
651
+ box-shadow: 0 4px 15px rgba(0,0,0,0.05);
652
+ padding: 20px;
653
+ display: flex;
654
+ flex-direction: column;
655
+ justify-content: space-between;
656
+ border: 1px solid var(--border-color-opaque);
657
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
658
+ }
659
+ .site-card:hover {
660
+ transform: translateY(-3px);
661
+ box-shadow: 0 6px 20px rgba(0,0,0,0.1);
662
+ }
663
+ @media (prefers-color-scheme: dark) {
664
+ .site-card {
665
+ background-color: var(--system-gray-75-dark);
666
+ }
667
+ }
668
+
669
+
670
+ .site-card h3 {
671
+ font-size: 20px;
672
+ font-weight: 600;
673
+ color: var(--primary-color);
674
+ margin-bottom: 10px;
675
+ }
676
+
677
+ .site-card p {
678
+ font-size: 14px;
679
+ color: var(--secondary-text-color);
680
+ margin-bottom: 8px;
681
+ }
682
+
683
+ .site-card .actions {
684
+ margin-top: 15px;
685
+ display: flex;
686
+ gap: 10px;
687
+ }
688
+
689
+ .site-card .actions a, .site-card .actions button {
690
+ flex: 1;
691
+ padding: 10px 15px;
692
+ border-radius: 10px;
693
+ font-size: 14px;
694
+ font-weight: 500;
695
+ text-align: center;
696
+ text-decoration: none;
697
+ cursor: pointer;
698
+ transition: background-color 0.2s ease, transform 0.1s ease;
699
+ }
700
+
701
+ .site-card .actions a {
702
+ background-color: var(--primary-color);
703
+ color: white;
704
+ border: none;
705
+ }
706
+
707
+ .site-card .actions a:hover {
708
+ background-color: var(--primary-color-hover);
709
+ }
710
+
711
+ .site-card .actions button {
712
+ background-color: var(--system-red-light);
713
+ color: white;
714
+ border: none;
715
+ }
716
+ @media (prefers-color-scheme: dark) {
717
+ .site-card .actions button {
718
+ background-color: var(--system-red-dark);
719
+ }
720
+ }
721
+
722
+ .site-card .actions button:hover {
723
+ background-color: color-mix(in srgb, var(--system-red-light) 80%, black);
724
+ }
725
+ @media (prefers-color-scheme: dark) {
726
+ .site-card .actions button:hover {
727
+ background-color: color-mix(in srgb, var(--system-red-dark) 80%, black);
728
+ }
729
+ }
730
+
731
+ .site-card .actions button:active {
732
+ transform: scale(0.98);
733
+ }
734
+
735
+ .flash-messages {
736
+ margin-top: 20px;
737
+ padding: 15px 20px;
738
+ border-radius: 12px;
739
+ font-size: 15px;
740
+ font-weight: 500;
741
+ text-align: center;
742
+ }
743
+
744
+ .flash-messages.success {
745
+ background-color: #d4edda;
746
+ color: #155724;
747
+ border: 1px solid #c3e6cb;
748
+ }
749
+
750
+ .flash-messages.error {
751
+ background-color: #f8d7da;
752
+ color: #721c24;
753
+ border: 1px solid #f5c6cb;
754
+ }
755
+
756
+ .flash-messages.warning {
757
+ background-color: #fff3cd;
758
+ color: #856404;
759
+ border: 1px solid #ffeeba;
760
+ }
761
+
762
+
763
+ @media (max-width: 768px) {
764
  body {
765
  padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom);
766
  align-items: flex-start;
 
774
  h1 {
775
  font-size: 28px;
776
  }
777
+ p.subtitle {
778
  font-size: 16px;
779
  margin-bottom: 25px;
780
  }
781
  .form-group {
782
  margin-bottom: 22px;
783
  }
784
+ textarea#prompt-input {
785
  padding: 12px 15px;
786
  min-height: 100px;
787
+ }
788
  button#generate-button {
789
+ padding: 15px;
790
+ font-size: 16px;
791
+ }
792
+ #output-container {
793
+ padding: 15px 18px;
794
+ font-size: 14px;
795
+ min-height: 50px;
796
+ }
797
+ .output-section {
798
+ margin-top: 30px;
799
+ }
800
+ .site-list-section {
801
+ margin-top: 35px;
802
+ padding-top: 25px;
803
+ }
804
+ .site-list-section h2 {
805
+ font-size: 20px;
806
+ margin-bottom: 20px;
807
+ }
808
+ .site-cards-grid {
809
+ grid-template-columns: 1fr; /* Stack columns on small screens */
810
+ }
811
+ .site-card {
812
+ padding: 18px;
813
+ }
814
+ .site-card h3 {
815
+ font-size: 18px;
816
  }
 
 
 
 
 
 
 
 
817
  }
818
  </style>
819
  </head>
820
  <body>
821
  <div class="container">
822
  <h1>EVA</h1>
823
+ <p class="subtitle">Генератор сайтов на базе ИИ</p>
824
+
825
+ {% with messages = get_flashed_messages(with_categories=true) %}
826
+ {% if messages %}
827
+ {% for category, message in messages %}
828
+ <div class="flash-messages {{ category }}">{{ message }}</div>
829
+ {% endfor %}
830
+ {% endif %}
831
+ {% endwith %}
832
 
833
  <form id="generate-form">
834
  <div class="form-group">
835
+ <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать</label>
836
+ <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай одностраничный сайт-портфолио для веб-дизайнера по имени Алия, с секциями 'Обо мне', 'Мои работы' и 'Контакты'. Используй современный минималистичный дизайн и добавь несколько примеров работ с описанием и ценой."></textarea>
837
  </div>
838
 
839
+ <button type="submit" id="generate-button">Создать сайт</button>
840
  </form>
841
 
842
  <div class="output-section">
843
+ <div class="output-header">
844
+ <label id="output-label">Ссылка на сгенерированный сайт</label>
845
+ <button id="copy-button">Копировать</button>
846
+ </div>
847
  <div id="output-container" aria-live="polite">
 
848
  </div>
849
  </div>
850
+
851
+ <div class="site-list-section">
852
+ <h2>Ваши сгенерированные сайты</h2>
853
+ {% if generated_sites %}
854
+ <div class="site-cards-grid">
855
+ {% for site_id, site_info in generated_sites.items() %}
856
+ <div class="site-card">
857
+ <h3>{{ site_info.ai_generated_data.site_title | default('Без названия') }}</h3>
858
+ <p><strong>ID:</strong> {{ site_id }}</p>
859
+ <p><strong>Создан:</strong> {{ site_info.timestamp }}</p>
860
+ <p><strong>Основной заголовок:</strong> {{ site_info.ai_generated_data.main_heading | default('N/A') }}</p>
861
+ <div class="actions">
862
+ <a href="{{ url_for('serve_generated_site', site_id=site_id) }}" target="_blank">Открыть</a>
863
+ <form method="POST" action="{{ url_for('delete_site') }}" style="display:inline;">
864
+ <input type="hidden" name="site_id" value="{{ site_id }}">
865
+ <button type="submit" onclick="return confirm('Вы уверены, что хотите удалить этот сайт?');">Удалить</button>
866
+ </form>
867
+ </div>
868
+ </div>
869
+ {% endfor %}
870
+ </div>
871
+ {% else %}
872
+ <p style="text-align: center; color: var(--secondary-text-color);">У вас пока нет сгенерированных сайтов. Начните с создания нового!</p>
873
+ {% endif %}
874
+ </div>
875
+
876
  </div>
877
 
878
  <script>
 
886
  event.preventDefault();
887
 
888
  if (!promptInput.value.trim()) {
889
+ showError("Пожалуйста, опишите сайт, который вы хотите создать.");
890
  return;
891
  }
892
 
 
894
 
895
  generateButton.disabled = true;
896
  generateButton.textContent = 'Генерация...';
897
+ outputContainer.innerHTML = '';
898
  outputContainer.classList.add('loading');
899
  outputContainer.classList.remove('error');
900
  copyButton.style.display = 'none';
 
916
  if (result.site_url) {
917
  const link = document.createElement('a');
918
  link.href = result.site_url;
919
+ link.textContent = "Открыть сгенерированный сайт";
920
+ link.target = "_blank";
921
+ outputContainer.innerHTML = '';
922
  outputContainer.appendChild(link);
923
  copyButton.style.display = 'block';
924
+ copyButton.dataset.copyText = window.location.origin + result.site_url;
925
+ // Перезагрузка страницы для отображения нового сайта в списке
926
+ setTimeout(() => {
927
+ window.location.reload();
928
+ }, 1000);
 
 
 
 
 
929
  } else if (result.error) {
930
  showError(result.error);
931
  } else {
932
+ showError("Не удалось получить ссылку на сайт. Ответ сервера не содержит URL.");
933
  }
934
 
935
  } catch (error) {
936
  console.error("Fetch Error:", error);
937
  showError(`Ошибка: ${error.message}`);
938
+ copyButton.style.display = 'none';
939
  } finally {
940
  generateButton.disabled = false;
941
+ generateButton.textContent = 'Создать сайт';
942
  outputContainer.classList.remove('loading');
943
  }
944
  });
 
951
  copyButton.textContent = 'Скопировано!';
952
  copyButton.classList.add('copied');
953
  setTimeout(() => {
954
+ copyButton.textContent = 'Копировать';
955
+ copyButton.classList.remove('copied');
956
  }, 1500);
957
  }).catch(err => {
958
  console.error('Ошибка копирования: ', err);
959
  copyButton.textContent = 'Ошибка';
960
+ setTimeout(() => {
961
+ copyButton.textContent = 'Копировать';
962
  }, 1500);
963
  });
964
  });
965
 
966
  function showError(message) {
967
+ outputContainer.innerHTML = '';
968
+ const errorMessageElement = document.createElement('span');
969
+ errorMessageElement.textContent = message;
970
+ outputContainer.appendChild(errorMessageElement);
971
+ outputContainer.classList.add('error');
972
+ outputContainer.classList.remove('loading');
973
+ copyButton.style.display = 'none';
974
  }
975
+
976
  </script>
977
  </body>
978
  </html>
979
  """
980
 
981
+ # Шаблон для рендеринга динамически сгенерированных сайтов
982
+ dynamic_site_template = """
983
+ <!DOCTYPE html>
984
+ <html lang="ru">
985
+ <head>
986
+ <meta charset="UTF-8">
987
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
988
+ <title>{{ site_data.site_title | default('Сгенерированный Сайт') }}</title>
989
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
990
+ <style>
991
+ body { font-family: 'Inter', sans-serif; margin: 0; padding: 0; background-color: #f4f7f6; color: #333; line-height: 1.6; }
992
+ .container { max-width: 900px; margin: 30px auto; background-color: #fff; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); overflow: hidden; }
993
+ header { background-color: #007bff; color: white; padding: 40px 20px; text-align: center; }
994
+ header h1 { margin: 0; font-size: 2.8em; font-weight: 700; }
995
+ header p { font-size: 1.2em; opacity: 0.9; margin-top: 10px; }
996
+ .section-content { padding: 30px; border-bottom: 1px solid #eee; }
997
+ .section-content:last-of-type { border-bottom: none; }
998
+ .section-content h2 { color: #007bff; font-size: 2em; margin-bottom: 20px; text-align: center; }
999
+ .text-content p { margin-bottom: 15px; font-size: 1.1em; line-height: 1.8; }
1000
+ .list-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-top: 20px; }
1001
+ .list-item { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
1002
+ .list-item img { max-width: 100%; height: 180px; object-fit: contain; border-radius: 5px; margin-bottom: 15px; background-color: white; padding: 5px; border: 1px solid #f0f0f0;}
1003
+ .list-item h3 { color: #333; margin-top: 0; font-size: 1.4em; margin-bottom: 10px; }
1004
+ .list-item p { font-size: 0.9em; color: #666; margin-bottom: 10px; }
1005
+ .list-item .price { font-size: 1.2em; font-weight: 600; color: #28a745; margin-top: 10px; }
1006
+ .contact-section p { font-size: 1.1em; margin-bottom: 10px; }
1007
+ .contact-section a { color: #007bff; text-decoration: none; }
1008
+ .contact-section a:hover { text-decoration: underline; }
1009
+ footer { background-color: #333; color: white; text-align: center; padding: 20px; font-size: 0.9em; margin-top: 20px; }
1010
+ @media (max-width: 768px) {
1011
+ header h1 { font-size: 2em; }
1012
+ header p { font-size: 1em; }
1013
+ .section-content { padding: 20px; }
1014
+ .section-content h2 { font-size: 1.6em; }
1015
+ }
1016
+ </style>
1017
+ </head>
1018
+ <body>
1019
+ <div class="container">
1020
+ <header>
1021
+ <h1>{{ site_data.main_heading | default('Добро пожаловать!') }}</h1>
1022
+ <p>{{ site_data.tagline | default('Ваш сгенерированный сайт готов.') }}</p>
1023
+ </header>
1024
+
1025
+ {% for section in site_data.sections %}
1026
+ <div class="section-content">
1027
+ <h2>{{ section.title }}</h2>
1028
+ {% if section.type == 'text' %}
1029
+ <div class="text-content">
1030
+ <p>{{ section.content | replace('\\n', '<br>') | safe }}</p>
1031
+ </div>
1032
+ {% elif section.type == 'list' and site_data.data_items %}
1033
+ <div class="list-grid">
1034
+ {% for item in site_data.data_items %}
1035
+ <div class="list-item">
1036
+ {% if item.image_url %}<img src="{{ item.image_url }}" alt="{{ item.name }}">{% endif %}
1037
+ <h3>{{ item.name }}</h3>
1038
+ <p>{{ item.description | default('') | replace('\\n', '<br>') | safe }}</p>
1039
+ {% if item.price %}<div class="price">{{ "%.2f"|format(item.price) }}</div>{% endif %}
1040
+ {% if item.category %}<p style="font-size: 0.8em; color: #999;">Категория: {{ item.category }}</p>{% endif %}
1041
+ {% for key, value in item.fields.items() %}
1042
+ <p style="font-size: 0.85em; color: #555;"><strong>{{ key|capitalize }}:</strong> {{ value }}</p>
1043
+ {% endfor %}
1044
+ </div>
1045
+ {% endfor %}
1046
+ </div>
1047
+ {% elif section.type == 'contact' and site_data.contact_info %}
1048
+ <div class="contact-section text-content">
1049
+ {% if site_data.contact_info.email %}<p><strong>Email:</strong> <a href="mailto:{{ site_data.contact_info.email }}">{{ site_data.contact_info.email }}</a></p>{% endif %}
1050
+ {% if site_data.contact_info.phone %}<p><strong>Телефон:</strong> <a href="tel:{{ site_data.contact_info.phone }}">{{ site_data.contact_info.phone }}</a></p>{% endif %}
1051
+ {% if site_data.contact_info.address %}<p><strong>Адрес:</strong> {{ site_data.contact_info.address }}</p>{% endif %}
1052
+ <p>{{ section.content | default('') | replace('\\n', '<br>') | safe }}</p>
1053
+ </div>
1054
+ {% elif section.type == 'image_gallery' %}
1055
+ <div class="list-grid">
1056
+ {% if section.images %} {# Assuming images can be passed directly in the section object for gallery #}
1057
+ {% for image_url in section.images %}
1058
+ <div class="list-item">
1059
+ <img src="{{ image_url }}" alt="Галерея">
1060
+ </div>
1061
+ {% endfor %}
1062
+ {% else %}
1063
+ <p style="text-align: center; color: #999;">Галерея изображений пока пуста.</p>
1064
+ {% endif %}
1065
+ </div>
1066
+ <p>{{ section.content | default('') | replace('\\n', '<br>') | safe }}</p>
1067
+ {% endif %}
1068
+ </div>
1069
+ {% endfor %}
1070
 
1071
+ <footer>
1072
+ <p>{{ site_data.footer_text | default(site_data.site_title + ' © ' + now.year|string + '. Все права защищены.') }}</p>
1073
+ </footer>
1074
+ </div>
1075
+ </body>
1076
+ </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1077
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1078
 
1079
+ # --- Flask Маршруты ---
1080
 
1081
  @app.route('/')
1082
  def index():
1083
+ """
1084
+ Главная страница EVA, отображает форму генерации и список сгенерированных сайтов.
1085
+ """
1086
+ generated_sites = load_site_metadata()
1087
+ # Сортируем сайты по времени создания (новые сверху) для удобства отображения
1088
+ sorted_sites = dict(sorted(generated_sites.items(), key=lambda item: item[1].get('timestamp', ''), reverse=True))
1089
+ return render_template_string(index_page_template, generated_sites=sorted_sites)
1090
 
1091
  @app.route('/generate', methods=['POST'])
1092
  def handle_generate():
1093
+ """
1094
+ Обрабатывает запрос на генерацию нового сайта.
1095
+ Вызывает AI для получения JSON-описания сайта, сохраняет его и возвращает ссылку.
1096
+ """
1097
  if 'prompt' not in request.form:
1098
  return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
1099
 
1100
  user_prompt = request.form['prompt']
1101
 
1102
  if not user_prompt or not user_prompt.strip():
1103
+ return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
1104
 
1105
  try:
1106
+ # Получаем JSON-структуру данных сайта от AI
1107
+ site_data_json = generate_site_json_from_prompt(user_prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1108
 
1109
+ if not site_data_json:
1110
+ return jsonify({"error": "Сгенерированные данные сайта пусты."}), 500
 
 
 
 
 
 
 
1111
 
1112
+ site_id = str(uuid.uuid4()) # Генерируем уникальный ID для нового сайта
1113
+ generated_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1114
 
1115
+ # Загружаем текущие метаданные, добавляем новый сайт и сохраняем
1116
+ site_metadata = load_site_metadata()
1117
+ site_metadata[site_id] = {
1118
+ "timestamp": generated_at,
1119
+ "ai_generated_data": site_data_json
1120
+ }
1121
+ save_site_metadata(site_metadata)
1122
+
1123
+ # Формируем URL для доступа к сгенерированному сайту
1124
+ site_url = url_for('serve_generated_site', site_id=site_id)
1125
+ return jsonify({"site_url": site_url})
 
 
1126
 
1127
+ except ValueError as ve:
1128
+ logging.error(f"Ошибка генерации (ValueError): {ve}")
1129
+ return jsonify({"error": str(ve)}), 400
1130
+ except Exception as e:
1131
+ logging.error(f"Неожиданная ошибка во время генерации сайта: {e}", exc_info=True)
1132
+ return jsonify({"error": f"Внутренняя ошибка сервера при генерации сайта: {e}"}), 500
1133
+
1134
+ @app.route('/generated_site/<site_id>')
1135
+ def serve_generated_site(site_id):
1136
+ """
1137
+ Отображает динамически сгенерированный сайт по его ID.
1138
+ Загружает JSON-данные сайта и рендерит их с помощью предопределенного шаблона.
1139
+ """
1140
+ site_metadata = load_site_metadata()
1141
+ site_info = site_metadata.get(site_id)
1142
+
1143
+ if not site_info:
1144
+ flash(f"Сайт с ID '{site_id}' не найден.", 'error')
1145
+ return redirect(url_for('index'))
1146
+
1147
+ site_data = site_info.get('ai_generated_data')
1148
+ if not site_data:
1149
+ flash(f"Данные для сайта с ID '{site_id}' повреждены.", 'error')
1150
+ return redirect(url_for('index'))
1151
+
1152
+ # Передаем объект datetime для использования года в футере
1153
+ return render_template_string(dynamic_site_template, site_data=site_data, now=datetime.now())
1154
+
1155
+ @app.route('/delete_site', methods=['POST'])
1156
+ def delete_site():
1157
+ """
1158
+ Удаляет сгенерированный сайт по его ID.
1159
+ """
1160
+ site_id_to_delete = request.form.get('site_id')
1161
+ if not site_id_to_delete:
1162
+ flash("ID сайта для удаления не предоставлен.", 'error')
1163
+ return redirect(url_for('index'))
1164
+
1165
+ site_metadata = load_site_metadata()
1166
+ if site_id_to_delete in site_metadata:
1167
+ del site_metadata[site_id_to_delete]
1168
+ save_site_metadata(site_metadata) # Сохраняем изменения и синхронизируем с HF
1169
+ flash(f"Сайт с ID '{site_id_to_delete}' успешно удален.", 'success')
1170
+ else:
1171
+ flash(f"Сайт с ID '{site_id_to_delete}' не найден.", 'warning')
1172
+
1173
+ return redirect(url_for('index'))
1174
+
1175
+ # --- Инициализация и запуск приложения ---
1176
 
1177
  if __name__ == '__main__':
1178
+ logging.info("Приложение запускается. Выполняется первоначальная загрузка/скачивание данных...")
1179
+ download_db_from_hf() # Попытка первоначального скачивания файла метаданных
1180
+ load_site_metadata() # Загружаем его (или создаем по умолчанию, если не найден/скачан)
1181
+ logging.info("Первоначальная загрузка данных завершена.")
1182
+
1183
+ if API_KEY_INTERNAL is None:
1184
+ logging.error("Переменная окружения GOOGLE_API_KEY не установлена. Генерация AI будет завершаться ошибкой.")
1185
+ flash("Внимание: Google AI API ключ не настроен. Генерация сайтов будет недоступна.", "warning")
1186
+
1187
+ if HF_TOKEN_WRITE:
1188
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1189
+ backup_thread.start()
1190
+ logging.info("Поток периодического резервного копирования запущен.")
1191
+ else:
1192
+ logging.warning("Периодическое резервное копирование НЕ будет выполняться (HF_TOKEN для записи не установлен).")
1193
 
1194
  port = int(os.environ.get('PORT', 7860))
1195
+ logging.info(f"Запуск Flask-приложения на хосте 0.0.0.0 и порту {port}")
1196
+ app.run(debug=False, host='0.0.0.0', port=port)