Kgshop commited on
Commit
02746dc
·
verified ·
1 Parent(s): 79d2248

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +450 -875
app.py CHANGED
@@ -1,365 +1,283 @@
1
  import os
2
  import uuid
 
 
3
  import json
4
  import logging
5
  import threading
6
  import time
7
  from datetime import datetime
8
-
9
- from flask import Flask, request, jsonify, Response, render_template_string, flash, redirect, url_for
10
- import google.generativeai as genai
11
- from huggingface_hub import HfApi, hf_hub_download
12
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
 
13
  from dotenv import load_dotenv
 
 
 
 
14
 
15
- # --- REQUIREMENTS.TXT ---
16
- # flask
17
- # google-generativeai
18
- # huggingface-hub
19
- # python-dotenv
20
- #
21
- # Установите зависимости, выполнив:
22
- # pip install -r requirements.txt
23
- # Или вручную:
24
- # pip install Flask google-generativeai huggingface-hub python-dotenv
25
- #
26
- # --- .ENV FILE CONFIGURATION ---
27
- # Создайте файл .env в той же директории, что и app.py, со следующим содержимым:
28
- # GOOGLE_API_KEY="ВАШ_GOOGLE_GEMINI_API_КЛЮЧ"
29
- # HF_TOKEN="ВАШ_HUGGING_FACE_WRITE_TOKEN"
30
- # HF_TOKEN_READ="ВАШ_HUGGING_FACE_READ_TOKEN" # Может быть тем же, что и HF_TOKEN, если у вас есть права на запись
31
- # FLASK_SECRET_KEY="СЛУЧАЙНАЯ_СЕКРЕТНАЯ_СТРОКА_ДЛЯ_FLASK"
32
- # -----------------------------
33
-
34
  load_dotenv()
35
 
36
  app = Flask(__name__)
37
- app.secret_key = os.getenv("FLASK_SECRET_KEY", "fallback_secret_key_if_not_set")
38
 
 
39
  REPO_ID = "Kgshop/testsynk"
40
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
41
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
42
 
43
- DATA_FILE = 'generated_sites_metadata.json'
44
- SYNC_FILES = [DATA_FILE]
45
 
46
- DOWNLOAD_RETRIES = 3
47
- DOWNLOAD_DELAY = 5
48
- BACKUP_INTERVAL_SECONDS = 1800 # 30 минут
49
 
 
 
 
 
 
50
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
51
 
52
- if not os.path.exists('generated_sites'):
53
- os.makedirs('generated_sites')
54
 
55
- def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
56
- if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
57
- logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE не установлены. Скачивание может завершиться неудачей для приватных репозиториев.")
58
- token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
59
- files_to_download = [specific_file] if specific_file else SYNC_FILES
60
- logging.info(f"Попытка скачивания файлов {files_to_download} из репозитория {REPO_ID}...")
61
- all_successful = True
62
- for file_name in files_to_download:
63
- success = False
64
- for attempt in range(retries + 1):
65
- try:
66
- logging.info(f"Скачивание {file_name} (Попытка {attempt + 1}/{retries + 1})...")
67
- hf_hub_download(
68
- repo_id=REPO_ID,
69
- filename=file_name,
70
- repo_type="dataset",
71
- token=token_to_use,
72
- local_dir=".",
73
- local_dir_use_symlinks=False,
74
- force_download=True,
75
- resume_download=False
76
- )
77
- logging.info(f"Файл {file_name} успешно скачан.")
78
- success = True
79
- break
80
- except RepositoryNotFoundError:
81
- logging.error(f"Репозиторий {REPO_ID} не найден. Скачивание отменено для всех файлов.")
82
- return False
83
- except HfHubHTTPError as e:
84
- if e.response.status_code == 404:
85
- logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID} (404). Пропускаем этот файл.")
86
- if attempt == 0 and not os.path.exists(file_name):
87
- try:
88
- if file_name == DATA_FILE:
89
- with open(file_name, 'w', encoding='utf-8') as f:
90
- json.dump({}, f)
91
- logging.info(f"Создан пустой локальный файл {file_name}, так как он не был найден на HF.")
92
- except Exception as create_e:
93
- logging.error(f"Не удалось создать пустой локальный файл {file_name}: {create_e}")
94
- success = False
95
- break
96
- else:
97
- logging.error(f"HTTP-ошибка при скачивании {file_name} (Попытка {attempt + 1}): {e}. Повторная попытка через {delay}с...")
98
- except Exception as e:
99
- logging.error(f"Неожиданная ошибка при скачивании {file_name} (Попытка {attempt + 1}): {e}. Повторная попытка через {delay}с...", exc_info=True)
100
- if attempt < retries:
101
- time.sleep(delay)
102
- if not success:
103
- logging.error(f"Не удалось скачать {file_name} после {retries + 1} попыток.")
104
- all_successful = False
105
- logging.info(f"Процесс скачивания завершен. Общий успех: {all_successful}")
106
- return all_successful
107
-
108
- def upload_db_to_hf(specific_file=None):
109
  if not HF_TOKEN_WRITE:
110
- logging.warning("HF_TOKEN (для записи) не установлен. Пропускаем загрузку на Hugging Face.")
111
- return
112
  try:
113
- api = HfApi()
114
- files_to_upload = [specific_file] if specific_file else SYNC_FILES
115
- logging.info(f"Начало загрузки файлов {files_to_upload} в репозиторий HF {REPO_ID}...")
116
- for file_name in files_to_upload:
117
- if os.path.exists(file_name):
118
- try:
119
- api.upload_file(
120
- path_or_fileobj=file_name,
121
- path_in_repo=file_name,
122
- repo_id=REPO_ID,
123
- repo_type="dataset",
124
- token=HF_TOKEN_WRITE,
125
- commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
126
- )
127
- logging.info(f"Файл {file_name} успешно загружен на Hugging Face.")
128
- except Exception as e:
129
- logging.error(f"Ошибка загрузки файла {file_name} на Hugging Face: {e}")
130
- else:
131
- logging.warning(f"Файл {file_name} не найден локально, пропускаем загрузку.")
132
- logging.info("Загрузка файлов на HF завершена.")
133
  except Exception as e:
134
- logging.error(f"Общая ошибка при инициализации или процессе загрузки Hugging Face: {e}", exc_info=True)
 
135
 
136
- def periodic_backup():
137
- logging.info(f"Настройка периодического резервного копирования каждые {BACKUP_INTERVAL_SECONDS} секунд.")
138
- while True:
139
- time.sleep(BACKUP_INTERVAL_SECONDS)
140
- logging.info("Начало периодического резервного копирования...")
141
- upload_db_to_hf()
142
- logging.info("Периодическое резервное копирование завершено.")
143
 
144
- def load_site_metadata():
145
- default_data = {}
146
- try:
147
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
148
- data = json.load(file)
149
- logging.info(f"Локальные метаданные сайта успешно загружены из {DATA_FILE}")
150
- if not isinstance(data, dict):
151
- logging.warning(f"Локальный файл {DATA_FILE} не является словарем. Попытка скачивания.")
152
- raise FileNotFoundError
153
- return data
154
- except FileNotFoundError:
155
- logging.warning(f"Локальный файл {DATA_FILE} не найден. Попытка скачивания с HF.")
156
- except json.JSONDecodeError:
157
- logging.error(f"Ошибка декодирования JSON в локальном файле {DATA_FILE}. Файл может быть поврежден. Попытка скачивания.")
158
- if download_db_from_hf(specific_file=DATA_FILE):
159
  try:
160
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
161
- data = json.load(file)
162
- logging.info(f"Метаданные сайта успешно загружены из {DATA_FILE} после скачивания.")
163
- if not isinstance(data, dict):
164
- logging.error(f"Скачанный файл {DATA_FILE} не является словарем. Используем по умолчанию.")
165
- return default_data
166
- return data
167
- except (FileNotFoundError, json.JSONDecodeError) as e:
168
- logging.error(f"Ошибка загрузки {DATA_FILE} даже после успешного скачивания: {e}. Используем по умолчанию.")
169
- return default_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  except Exception as e:
171
- logging.error(f"Неизвестная ошибка при загрузке скачанного файла {DATA_FILE}: {e}. Используем по умолчанию.", exc_info=True)
172
- return default_data
173
- else:
174
- logging.error(f"Не удалось скачать {DATA_FILE} с HF после нескольких попыток. Используем пустую структуру данных по умолчанию.")
175
- if not os.path.exists(DATA_FILE):
176
- try:
177
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
178
- json.dump(default_data, f)
179
- logging.info(f"Создан пустой локальный файл {DATA_FILE} после неудачного скачивания.")
180
- except Exception as create_e:
181
- logging.error(f"Не удалось создать пустой локальный файл {DATA_FILE}: {create_e}")
182
- return default_data
183
-
184
- def save_site_metadata(data):
 
185
  try:
186
- if not isinstance(data, dict):
187
- logging.error("Попытка сохранить недопустимую структуру данных (не словарь) для метаданных сайта. Отмена сохранения.")
188
- return
189
- with open(DATA_FILE, 'w', encoding='utf-8') as file:
190
- json.dump(data, file, ensure_ascii=False, indent=2)
191
- logging.info(f"Метаданные сайта успешно сохранены в {DATA_FILE}")
192
- upload_db_to_hf(specific_file=DATA_FILE)
 
 
 
 
 
 
 
 
193
  except Exception as e:
194
- logging.error(f"Ошибка сохранения метаданных сайта в {DATA_FILE}: {e}", exc_info=True)
195
 
196
- API_KEY_INTERNAL = os.getenv("GOOGLE_API_KEY")
197
-
198
- def generate_site_json_from_prompt(user_prompt):
 
 
199
  try:
200
- genai.configure(api_key=API_KEY_INTERNAL)
 
 
 
 
 
 
 
 
201
  except Exception as e:
202
- logging.error(f"Ошибка настройки GenAI: {e}")
203
- raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
204
- if not user_prompt or not user_prompt.strip():
205
- raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
206
 
207
- system_instruction = (
208
- "Ты экспертный ИИ, который разрабатывает простые, функциональные одностраничные веб-приложения. "
209
- "Когда пользователь описывает веб-сайт, сгенерируй JSON-объект, который строго соответствует следующей схеме. "
210
- "Этот JSON-объект будет использоваться Flask-приложением для динамического рендеринга сайта. "
211
- "Не включай HTML, Markdown или пояснительный текст за пределами JSON. Выводи только JSON-строку. "
212
- "JSON должен быть валидным и напрямую парсируемым. "
213
- "Для URL-адресов изображений используй общие сервисы-заполнители, такие как `https://placehold.co/600x400?text=Image` или `https://picsum.photos/600/400` или аналогичные, "
214
- "либо опиши их текстом, если заполнители не подходят. "
215
- "Убедись, что любой текстовый контент, содержащий символы новой строки, представлен с помощью `\\n` в JSON-строке."
216
- "\n\nJSON Schema:\n"
217
- "```json\n"
218
- "{\n"
219
- " \"site_title\": \"string\",\n"
220
- " \"main_heading\": \"string\",\n"
221
- " \"tagline\": \"string\",\n"
222
- " \"theme\": \"string\", // Suggested values: \"modern\", \"minimalist\", \"bold\", \"corporate\", \"creative\", \"playful\"\n"
223
- " \"primary_color\": \"string\", // Hex color, e.g., \"#3498db\"\n"
224
- " \"secondary_color\": \"string\", // Hex color, e.g., \"#2ecc71\"\n"
225
- " \"font_family_heading\": \"string\", // CSS font-family, e.g., \"'Inter', sans-serif\"\n"
226
- " \"font_family_body\": \"string\", // CSS font-family, e.g., \"'Open Sans', sans-serif\"\n"
227
- " \"sections\": [\n"
228
- " {\n"
229
- " \"type\": \"hero\", // Large introductory section\n"
230
- " \"title\": \"string\",\n"
231
- " \"subtitle\": \"string\",\n"
232
- " \"image_url\": \"string (optional, placeholder URL)\",\n"
233
- " \"cta_text\": \"string (optional)\", // Call to action button text\n"
234
- " \"cta_link\": \"string (optional)\" // Call to action button link\n"
235
- " },\n"
236
- " {\n"
237
- " \"type\": \"text\", // General text content section\n"
238
- " \"title\": \"string\",\n"
239
- " \"content\": \"string\"\n"
240
- " },\n"
241
- " {\n"
242
- " \"type\": \"list\", // Displays items from 'data_items'\n"
243
- " \"title\": \"string\",\n"
244
- " \"description\": \"string (optional)\",\n"
245
- " \"filter_category\": \"string (optional)\" // If set, only show data_items with this category\n"
246
- " },\n"
247
- " {\n"
248
- " \"type\": \"features\", // Grid of features, draws from data_items or custom section.items\n"
249
- " \"title\": \"string\",\n"
250
- " \"description\": \"string (optional)\",\n"
251
- " \"items\": [ // Can specify items directly or draw from data_items\n"
252
- " {\"name\": \"string\", \"description\": \"string\", \"icon\": \"string (e.g. 'fas fa-star')\"}\n"
253
- " ]\n"
254
- " },\n"
255
- " {\n"
256
- " \"type\": \"testimonials\", // Testimonials section, draws from data_items\n"
257
- " \"title\": \"string\",\n"
258
- " \"description\": \"string (optional)\"\n"
259
- " },\n"
260
- " {\n"
261
- " \"type\": \"pricing\", // Pricing section, draws from data_items\n"
262
- " \"title\": \"string\",\n"
263
- " \"description\": \"string (optional)\"\n"
264
- " },\n"
265
- " {\n"
266
- " \"type\": \"contact\", // Contact section with form structure\n"
267
- " \"title\": \"string\",\n"
268
- " \"content\": \"string (optional, for introductory text)\",\n"
269
- " \"email\": \"string (optional)\",\n"
270
- " \"phone\": \"string (optional)\",\n"
271
- " \"address\": \"string (optional)\",\n"
272
- " \"form_fields\": [ // Defines the form structure, no backend processing\n"
273
- " {\"label\": \"string\", \"type\": \"text\"}, // or \"email\", \"textarea\"\n"
274
- " {\"label\": \"Ваше сообщение\", \"type\": \"textarea\"}\n"
275
- " ]\n"
276
- " },\n"
277
- " {\n"
278
- " \"type\": \"image_gallery\", // Gallery of images\n"
279
- " \"title\": \"string\",\n"
280
- " \"description\": \"string (optional)\",\n"
281
- " \"images\": [\"url_string\", \"url_string\"] // Array of placeholder image URLs\n"
282
- " }\n"
283
- " ],\n"
284
- " \"data_items\": [ // Acts as a 'database' for lists, features, testimonials, pricing plans\n"
285
- " {\n"
286
- " \"id\": \"unique_string_or_number\",\n"
287
- " \"name\": \"string\", // For lists, features, pricing plan name, person name for testimonial\n"
288
- " \"description\": \"string\", // For lists, features, testimonial text, pricing features\n"
289
- " \"price\": \"number (optional)\", // For lists, pricing plans\n"
290
- " \"image_url\": \"string (optional, placeholder URL)\", // For lists, image gallery, testimonial avatar\n"
291
- " \"category\": \"string (optional)\", // For filtering lists\n"
292
- " \"icon\": \"string (optional, e.g. 'fas fa-star')\", // For features\n"
293
- " \"author\": \"string (optional)\", // For testimonials\n"
294
- " \"role\": \"string (optional)\", // For testimonials\n"
295
- " \"fields\": { \"key\": \"value\" } // Arbitrary additional fields\n"
296
- " }\n"
297
- " ],\n"
298
- " \"footer_text\": \"string (optional)\"\n"
299
- "}\n"
300
- "```\n"
301
- "- `sections`: Массив блоков контента. AI должно варьировать типы секций в соответствии с запросом.\n"
302
- "- Для `image_url` в `data_items` или `image_gallery` используй URL-адреса, например, `https://picsum.photos/400/300?random=1` или `https://placehold.co/400x300?text=Photo+Title`.\n"
303
- "- Для `icon` в `features` используй классы Font Awesome 6 Free (например, `fas fa-star`, `fas fa-code`, `fas fa-check`).\n"
304
- "- Выводи ТОЛЬКО JSON-код. Не включай никакого пояснительного текста или форматирования Markdown (например, ```json) за пределами JSON."
305
- )
306
- full_prompt = f"{system_instruction}\n\nПользовательский запрос: \"{user_prompt}\""
307
- response = None
308
  try:
309
- model = genai.GenerativeModel('gemini-1.5-flash-latest')
310
- response = model.generate_content(full_prompt)
311
- generated_text = ""
312
- if hasattr(response, 'text') and response.text:
313
- generated_text = response.text
314
- elif response.parts:
315
- generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
316
- if not generated_text.strip():
317
- if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
318
- reason = response.prompt_feedback.block_reason
319
- raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другой запрос.")
320
- else:
321
- raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
322
- clean_text = generated_text.strip()
323
- if clean_text.startswith("```json"):
324
- clean_text = clean_text[7:]
325
- if clean_text.endswith("```"):
326
- clean_text = clean_text[:-3]
327
- generated_text = clean_text.strip()
328
- parsed_json = json.loads(generated_text)
329
- return parsed_json
330
- except json.JSONDecodeError as jde:
331
- logging.error(f"Вывод ИИ был невалидным JSON: {generated_text[:500]}... Ошибка: {jde}")
332
- raise ValueError("Модель сгенерировала невалидный JSON. Пожалуйста, попробуйте еще раз или измените запрос.")
333
  except Exception as e:
334
- logging.error(f"Ошибка генерации контента с GenAI: {e}", exc_info=True)
335
- error_message = str(e)
336
- if "API key not valid" in error_message or "Invalid API key" in error_message:
337
- raise ValueError("Внутренняя ошибка конфигурации API. Проверьте ключ Google API.")
338
- elif "Billing account not found" in error_message or "billing account" in error_message.lower():
339
- raise ValueError("Проблема с биллингом аккаунта Google Cloud. Возможно, аккаунт не настроен или лимит исчерпан.")
340
- elif "Could not find model" in error_message:
341
- raise ValueError(f"Модель 'gemini-1.5-flash-latest' не найдена или недоступна. Возможно, используйте 'learnlm-2.0-flash-experimental'.")
342
- elif "resource has been exhausted" in error_message.lower() or "quota" in error_message.lower():
343
- raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
344
- elif ("content has been blocked" in error_message.lower() or
345
- (response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason)):
346
- reason = "неизвестна"
347
- if response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
348
- reason = response.prompt_feedback.block_reason
349
- elif "safety settings" in error_message.lower():
350
- reason = "настройки безопасности"
351
- raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  else:
353
- raise ValueError(f"Ошибка при генерации данных сайта: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
- index_page_template = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  <!DOCTYPE html>
357
  <html lang="ru">
358
  <head>
359
  <meta charset="UTF-8">
360
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
361
  <title>EVA - Генератор Сайтов</title>
362
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
363
  <style>
364
  :root {
365
  --system-gray-100-light: #f2f2f7;
@@ -415,7 +333,7 @@ index_page_template = """
415
  --secondary-text-color: var(--system-gray-light-75-light);
416
  --tertiary-text-color: var(--system-gray-light-50-light);
417
  --border-color: var(--system-separator-light);
418
- --border-color-opaque: var(--system-separator-opaque-light);
419
  --input-bg: var(--system-gray-75-light);
420
  --primary-color: var(--system-blue-light);
421
  --primary-color-hover: var(--system-blue-light-hover);
@@ -424,11 +342,11 @@ index_page_template = """
424
  }
425
 
426
  html {
427
- height: -webkit-fill-available;
428
  }
429
 
430
  body {
431
- font-family: -apple-system, BlinkMacMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
432
  margin: 0;
433
  padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom);
434
  background-color: var(--bg-color);
@@ -448,7 +366,7 @@ index_page_template = """
448
  padding: 25px 30px 30px 30px;
449
  border-radius: 24px;
450
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
451
- max-width: 780px;
452
  width: calc(100% - 40px);
453
  box-sizing: border-box;
454
  margin-top: 30px;
@@ -500,9 +418,9 @@ index_page_template = """
500
  }
501
 
502
  textarea#prompt-input:focus {
503
- border-color: var(--primary-color);
504
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent);
505
- outline: none;
506
  }
507
 
508
  button#generate-button {
@@ -537,10 +455,10 @@ index_page_template = """
537
  }
538
 
539
  .output-header {
540
- display: flex;
541
- justify-content: space-between;
542
- align-items: center;
543
- margin-bottom: 10px;
544
  }
545
 
546
  label#output-label {
@@ -572,12 +490,12 @@ index_page_template = """
572
  }
573
 
574
  button#copy-button.copied {
575
- color: #34c759;
576
  }
577
  @media (prefers-color-scheme: dark) {
578
- button#copy-button.copied {
579
- color: #30d158;
580
- }
581
  }
582
 
583
  #output-container {
@@ -606,6 +524,7 @@ index_page_template = """
606
  text-decoration: underline;
607
  }
608
 
 
609
  #output-container.loading::before {
610
  content: "Генерация сайта...";
611
  display: block;
@@ -628,149 +547,7 @@ index_page_template = """
628
  50% { opacity: 1; }
629
  }
630
 
631
- .site-list-section {
632
- margin-top: 50px;
633
- border-top: 1px solid var(--border-color);
634
- padding-top: 30px;
635
- }
636
-
637
- .site-list-section h2 {
638
- font-size: 24px;
639
- font-weight: 600;
640
- text-align: center;
641
- margin-bottom: 25px;
642
- color: var(--text-color);
643
- }
644
-
645
- .site-cards-grid {
646
- display: grid;
647
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
648
- gap: 25px;
649
- }
650
-
651
- .site-card {
652
- background-color: var(--system-gray-75-light);
653
- border-radius: 16px;
654
- box-shadow: 0 4px 15px rgba(0,0,0,0.05);
655
- padding: 20px;
656
- display: flex;
657
- flex-direction: column;
658
- justify-content: space-between;
659
- border: 1px solid var(--border-color-opaque);
660
- transition: transform 0.2s ease, box-shadow 0.2s ease;
661
- }
662
- .site-card:hover {
663
- transform: translateY(-3px);
664
- box-shadow: 0 6px 20px rgba(0,0,0,0.1);
665
- }
666
- @media (prefers-color-scheme: dark) {
667
- .site-card {
668
- background-color: var(--system-gray-75-dark);
669
- }
670
- }
671
-
672
- .site-card h3 {
673
- font-size: 20px;
674
- font-weight: 600;
675
- color: var(--primary-color);
676
- margin-bottom: 10px;
677
- }
678
-
679
- .site-card p {
680
- font-size: 14px;
681
- color: var(--secondary-text-color);
682
- margin-bottom: 8px;
683
- word-wrap: break-word;
684
- }
685
- .site-card p strong {
686
- color: var(--text-color);
687
- }
688
-
689
- .site-card .actions {
690
- margin-top: 15px;
691
- display: flex;
692
- gap: 10px;
693
- }
694
-
695
- .site-card .actions a, .site-card .actions button {
696
- flex: 1;
697
- padding: 10px 15px;
698
- border-radius: 10px;
699
- font-size: 14px;
700
- font-weight: 500;
701
- text-align: center;
702
- text-decoration: none;
703
- cursor: pointer;
704
- transition: background-color 0.2s ease, transform 0.1s ease;
705
- display: inline-flex;
706
- align-items: center;
707
- justify-content: center;
708
- gap: 5px;
709
- }
710
-
711
- .site-card .actions a {
712
- background-color: var(--primary-color);
713
- color: white;
714
- border: none;
715
- }
716
-
717
- .site-card .actions a:hover {
718
- background-color: var(--primary-color-hover);
719
- }
720
-
721
- .site-card .actions button {
722
- background-color: var(--system-red-light);
723
- color: white;
724
- border: none;
725
- }
726
- @media (prefers-color-scheme: dark) {
727
- .site-card .actions button {
728
- background-color: var(--system-red-dark);
729
- }
730
- }
731
-
732
- .site-card .actions button:hover {
733
- background-color: color-mix(in srgb, var(--system-red-light) 80%, black);
734
- }
735
- @media (prefers-color-scheme: dark) {
736
- .site-card .actions button:hover {
737
- background-color: color-mix(in srgb, var(--system-red-dark) 80%, black);
738
- }
739
- }
740
-
741
- .site-card .actions button:active {
742
- transform: scale(0.98);
743
- }
744
-
745
- .flash-messages {
746
- margin-top: 20px;
747
- padding: 15px 20px;
748
- border-radius: 12px;
749
- font-size: 15px;
750
- font-weight: 500;
751
- text-align: center;
752
- margin-bottom: 25px;
753
- }
754
-
755
- .flash-messages.success {
756
- background-color: #d4edda;
757
- color: #155724;
758
- border: 1px solid #c3e6cb;
759
- }
760
-
761
- .flash-messages.error {
762
- background-color: #f8d7da;
763
- color: #721c24;
764
- border: 1px solid #f5c6cb;
765
- }
766
-
767
- .flash-messages.warning {
768
- background-color: #fff3cd;
769
- color: #856404;
770
- border: 1px solid #ffeeba;
771
- }
772
-
773
- @media (max-width: 768px) {
774
  body {
775
  padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom);
776
  align-items: flex-start;
@@ -784,46 +561,29 @@ index_page_template = """
784
  h1 {
785
  font-size: 28px;
786
  }
787
- p.subtitle {
788
  font-size: 16px;
789
  margin-bottom: 25px;
790
  }
791
  .form-group {
792
  margin-bottom: 22px;
793
  }
794
- textarea#prompt-input {
795
  padding: 12px 15px;
796
  min-height: 100px;
797
- }
798
  button#generate-button {
799
- padding: 15px;
800
- font-size: 16px;
801
- }
802
- #output-container {
803
- padding: 15px 18px;
804
- font-size: 14px;
805
- min-height: 50px;
806
- }
807
- .output-section {
808
- margin-top: 30px;
809
- }
810
- .site-list-section {
811
- margin-top: 35px;
812
- padding-top: 25px;
813
- }
814
- .site-list-section h2 {
815
- font-size: 20px;
816
- margin-bottom: 20px;
817
- }
818
- .site-cards-grid {
819
- grid-template-columns: 1fr;
820
- }
821
- .site-card {
822
- padding: 18px;
823
- }
824
- .site-card h3 {
825
- font-size: 18px;
826
  }
 
 
 
 
 
 
 
 
827
  }
828
  </style>
829
  </head>
@@ -832,57 +592,23 @@ index_page_template = """
832
  <h1>EVA</h1>
833
  <p class="subtitle">Генератор сайтов на базе ИИ</p>
834
 
835
- {% with messages = get_flashed_messages(with_categories=true) %}
836
- {% if messages %}
837
- {% for category, message in messages %}
838
- <div class="flash-messages {{ category }}">{{ message }}</div>
839
- {% endfor %}
840
- {% endif %}
841
- {% endwith %}
842
-
843
  <form id="generate-form">
844
  <div class="form-group">
845
  <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать</label>
846
- <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай сайт-портфолио для фотографа в минималистичном стиле, с разделами 'Обо мне', 'Портфолио' (с галереей), 'Услуги' (с ценами) и 'Контакты' с формой обратной связи. Используй синие и серые тона."></textarea>
847
  </div>
848
 
849
  <button type="submit" id="generate-button">Создать сайт</button>
850
  </form>
851
 
852
  <div class="output-section">
853
- <div class="output-header">
854
- <label id="output-label">Ссылка на сгенерированный сайт</label>
855
- <button id="copy-button">Копировать</button>
856
- </div>
857
  <div id="output-container" aria-live="polite">
858
  </div>
859
  </div>
860
-
861
- <div class="site-list-section">
862
- <h2>Ваши сгенерированные сайты</h2>
863
- {% if generated_sites %}
864
- <div class="site-cards-grid">
865
- {% for site_id, site_info in generated_sites.items() %}
866
- <div class="site-card">
867
- <h3>{{ site_info.ai_generated_data.site_title | default('Без названия') }}</h3>
868
- <p><strong>ID:</strong> {{ site_id }}</p>
869
- <p><strong>Создан:</strong> {{ site_info.timestamp }}</p>
870
- <p><strong>Основной заголовок:</strong> {{ site_info.ai_generated_data.main_heading | default('N/A') }}</p>
871
- <div class="actions">
872
- <a href="{{ url_for('serve_generated_site', site_id=site_id) }}" target="_blank"><i class="fas fa-external-link-alt"></i> Открыть</a>
873
- <form method="POST" action="{{ url_for('delete_site') }}" style="display:inline;">
874
- <input type="hidden" name="site_id" value="{{ site_id }}">
875
- <button type="submit" onclick="return confirm('Вы уверены, что хотите удалить этот сайт?');"><i class="fas fa-trash-alt"></i> Удалить</button>
876
- </form>
877
- </div>
878
- </div>
879
- {% endfor %}
880
- </div>
881
- {% else %}
882
- <p style="text-align: center; color: var(--secondary-text-color);">У вас пока нет сгенерированных сайтов. Начните с создания нового!</p>
883
- {% endif %}
884
- </div>
885
-
886
  </div>
887
 
888
  <script>
@@ -911,6 +637,7 @@ index_page_template = """
911
  copyButton.textContent = 'Копировать';
912
  copyButton.classList.remove('copied');
913
 
 
914
  try {
915
  const response = await fetch('/generate', {
916
  method: 'POST',
@@ -932,9 +659,6 @@ index_page_template = """
932
  outputContainer.appendChild(link);
933
  copyButton.style.display = 'block';
934
  copyButton.dataset.copyText = window.location.origin + result.site_url;
935
- setTimeout(() => {
936
- window.location.reload();
937
- }, 1000);
938
  } else if (result.error) {
939
  showError(result.error);
940
  } else {
@@ -960,374 +684,225 @@ index_page_template = """
960
  copyButton.textContent = 'Скопировано!';
961
  copyButton.classList.add('copied');
962
  setTimeout(() => {
963
- copyButton.textContent = 'Копировать';
964
- copyButton.classList.remove('copied');
965
  }, 1500);
966
  }).catch(err => {
967
  console.error('Ошибка копирования: ', err);
968
  copyButton.textContent = 'Ошибка';
969
- setTimeout(() => {
970
- copyButton.textContent = 'Копировать';
971
  }, 1500);
972
  });
973
  });
974
 
975
  function showError(message) {
976
- outputContainer.innerHTML = '';
977
- const errorMessageElement = document.createElement('span');
978
- errorMessageElement.textContent = message;
979
- outputContainer.appendChild(errorMessageElement);
980
- outputContainer.classList.add('error');
981
- outputContainer.classList.remove('loading');
982
- copyButton.style.display = 'none';
983
  }
 
984
  </script>
985
  </body>
986
  </html>
987
  """
988
 
989
- dynamic_site_template = """
990
- <!DOCTYPE html>
991
- <html lang="ru">
992
- <head>
993
- <meta charset="UTF-8">
994
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
995
- <title>{{ site_data.site_title | default('Сгенерированный Сайт') }}</title>
996
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Open+Sans:wght@400;600&display=swap" rel="stylesheet">
997
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
998
- <style>
999
- :root {
1000
- --primary-color: {{ site_data.primary_color | default('#007bff') }};
1001
- --secondary-color: {{ site_data.secondary_color | default('#2ecc71') }};
1002
- --heading-font: {{ site_data.font_family_heading | default("'Inter', sans-serif") }};
1003
- --body-font: {{ site_data.font_family_body | default("'Open Sans', sans-serif") }};
1004
- --text-color: #333;
1005
- --bg-color: #f4f7f6;
1006
- --card-bg: #ffffff;
1007
- --border-color: #e0e0e0;
1008
- --light-bg: #f9f9f9;
1009
- }
1010
 
1011
- body { font-family: var(--body-font); margin: 0; padding: 0; background-color: var(--bg-color); color: var(--text-color); line-height: 1.6; }
1012
- .container { max-width: 1000px; margin: 30px auto; background-color: var(--card-bg); border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); overflow: hidden; }
1013
- header { background-color: var(--primary-color); color: white; padding: 60px 20px; text-align: center; }
1014
- header h1 { margin: 0; font-size: 3.2em; font-weight: 700; font-family: var(--heading-font); }
1015
- header p { font-size: 1.4em; opacity: 0.9; margin-top: 10px; font-family: var(--heading-font); }
1016
- .section-content { padding: 40px 30px; border-bottom: 1px solid var(--border-color); }
1017
- .section-content:last-of-type { border-bottom: none; }
1018
- .section-content h2 { color: var(--primary-color); font-size: 2.5em; margin-bottom: 25px; text-align: center; font-family: var(--heading-font); }
1019
- .section-content p { font-size: 1.1em; line-height: 1.8; text-align: center; max-width: 700px; margin: 0 auto 30px;}
1020
-
1021
- /* Hero Section */
1022
- .hero-section { background-color: var(--primary-color); color: white; padding: 80px 20px; text-align: center; }
1023
- .hero-section h1 { font-size: 3.5em; margin-bottom: 15px; font-family: var(--heading-font); }
1024
- .hero-section p { font-size: 1.5em; margin-bottom: 30px; opacity: 0.9; font-family: var(--heading-font); }
1025
- .hero-section img { max-width: 100%; height: auto; border-radius: 8px; margin-top: 30px; box-shadow: 0 8px 20px rgba(0,0,0,0.2); }
1026
- .hero-section .cta-button {
1027
- display: inline-block; padding: 15px 30px; background-color: var(--secondary-color); color: white;
1028
- text-decoration: none; border-radius: 50px; font-size: 1.2em; font-weight: 600; transition: background-color 0.3s ease, transform 0.2s ease;
1029
- }
1030
- .hero-section .cta-button:hover { background-color: darken(var(--secondary-color), 10%); transform: translateY(-2px); }
1031
-
1032
- /* General Text Section */
1033
- .text-content { text-align: justify; }
1034
- .text-content p { text-align: left; }
1035
-
1036
- /* List/Gallery Grid */
1037
- .grid-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 30px; margin-top: 20px; }
1038
- .grid-item { background-color: var(--light-bg); border: 1px solid var(--border-color); border-radius: 12px; padding: 25px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.05); transition: transform 0.3s ease, box-shadow 0.3s ease; }
1039
- .grid-item:hover { transform: translateY(-5px); box-shadow: 0 4px 15px rgba(0,0,0,0.1); }
1040
- .grid-item img { max-width: 100%; height: 200px; object-fit: cover; border-radius: 8px; margin-bottom: 15px; background-color: var(--card-bg); padding: 5px; border: 1px solid var(--border-color);}
1041
- .grid-item h3 { color: var(--primary-color); margin-top: 0; font-size: 1.6em; margin-bottom: 10px; font-family: var(--heading-font); }
1042
- .grid-item p { font-size: 1em; color: #666; margin-bottom: 10px; text-align: center; }
1043
- .grid-item .price { font-size: 1.4em; font-weight: 700; color: var(--secondary-color); margin-top: 10px; }
1044
-
1045
- /* Features Section */
1046
- .feature-icon { font-size: 3.5em; color: var(--secondary-color); margin-bottom: 15px; }
1047
- .feature-item h3 { font-size: 1.4em; }
1048
-
1049
- /* Testimonials Section */
1050
- .testimonial-item { background-color: var(--light-bg); border: 1px solid var(--primary-color); border-radius: 12px; padding: 25px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
1051
- .testimonial-item img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin-bottom: 15px; border: 3px solid var(--primary-color); }
1052
- .testimonial-item p { font-style: italic; color: #555; margin-bottom: 10px; }
1053
- .testimonial-item .author { font-weight: 600; color: var(--primary-color); }
1054
- .testimonial-item .role { font-size: 0.9em; color: #888; }
1055
-
1056
- /* Pricing Section */
1057
- .pricing-plan { background-color: var(--light-bg); border: 1px solid var(--border-color); border-radius: 12px; padding: 30px; text-align: center; box-shadow: 0 4px 15px rgba(0,0,0,0.08); }
1058
- .pricing-plan.highlight { border-color: var(--primary-color); background-color: lighten(var(--primary-color), 40%); }
1059
- .pricing-plan h3 { font-size: 2em; color: var(--primary-color); margin-bottom: 15px; }
1060
- .pricing-plan .price { font-size: 3em; font-weight: 700; color: var(--secondary-color); margin-bottom: 20px; }
1061
- .pricing-plan ul { list-style: none; padding: 0; margin-bottom: 30px; }
1062
- .pricing-plan ul li { margin-bottom: 10px; font-size: 1.1em; color: #555; }
1063
- .pricing-plan .cta-button { display: inline-block; padding: 12px 25px; background-color: var(--primary-color); color: white; text-decoration: none; border-radius: 50px; font-weight: 600; transition: background-color 0.3s ease; }
1064
- .pricing-plan .cta-button:hover { background-color: darken(var(--primary-color), 10%); }
1065
-
1066
- /* Contact Section */
1067
- .contact-section { text-align: center; }
1068
- .contact-info p { font-size: 1.1em; margin-bottom: 10px; text-align: center;}
1069
- .contact-info a { color: var(--primary-color); text-decoration: none; }
1070
- .contact-info a:hover { text-decoration: underline; }
1071
- .contact-form { margin-top: 30px; max-width: 600px; margin-left: auto; margin-right: auto; text-align: left; }
1072
- .contact-form label { display: block; font-weight: 600; margin-bottom: 8px; color: var(--primary-color); }
1073
- .contact-form input[type="text"],
1074
- .contact-form input[type="email"],
1075
- .contact-form textarea {
1076
- width: 100%; padding: 12px; margin-bottom: 20px; border: 1px solid var(--border-color); border-radius: 8px;
1077
- font-size: 1em; box-sizing: border-box; transition: border-color 0.3s ease;
1078
- }
1079
- .contact-form input[type="text"]:focus,
1080
- .contact-form input[type="email"]:focus,
1081
- .contact-form textarea:focus {
1082
- border-color: var(--primary-color); outline: none; box-shadow: 0 0 0 3px rgba(var(--primary-color-rgb), 0.1);
1083
- }
1084
- .contact-form textarea { resize: vertical; min-height: 120px; }
1085
- .contact-form button {
1086
- display: inline-block; padding: 15px 30px; background-color: var(--secondary-color); color: white;
1087
- border: none; border-radius: 50px; font-size: 1.2em; font-weight: 600; cursor: pointer;
1088
- transition: background-color 0.3s ease, transform 0.2s ease;
1089
- }
1090
- .contact-form button:hover { background-color: darken(var(--secondary-color), 10%); transform: translateY(-2px); }
1091
-
1092
- /* Footer */
1093
- footer { background-color: #333; color: white; text-align: center; padding: 25px; font-size: 0.9em; margin-top: 30px; }
1094
-
1095
- @media (max-width: 768px) {
1096
- .container { margin: 15px auto; border-radius: 5px; }
1097
- header { padding: 40px 15px; }
1098
- header h1 { font-size: 2.5em; }
1099
- header p { font-size: 1.1em; }
1100
- .section-content { padding: 25px 15px; }
1101
- .section-content h2 { font-size: 2em; }
1102
- .hero-section { padding: 50px 15px; }
1103
- .hero-section h1 { font-size: 2.8em; }
1104
- .hero-section p { font-size: 1.2em; }
1105
- .hero-section .cta-button { font-size: 1em; padding: 12px 25px; }
1106
- .grid-container { grid-template-columns: 1fr; gap: 20px; }
1107
- .grid-item { padding: 20px; }
1108
- .contact-form { padding: 0 15px; }
1109
- }
1110
- </style>
1111
- </head>
1112
- <body>
1113
- <div class="container">
1114
- {% if site_data.main_heading or site_data.tagline %}
1115
- <header>
1116
- <h1>{{ site_data.main_heading | default('Добро пожаловать!') }}</h1>
1117
- <p>{{ site_data.tagline | default('Ваш сгенерированный сайт готов.') }}</p>
1118
- </header>
1119
- {% endif %}
1120
-
1121
- {% for section in site_data.sections %}
1122
- <div class="section-content">
1123
- <h2>{{ section.title }}</h2>
1124
- {% if section.description %}<p>{{ section.description | replace('\\n', '<br>') | safe }}</p>{% endif %}
1125
-
1126
- {% if section.type == 'hero' %}
1127
- <div class="hero-section" style="background-color: var(--primary-color); color: white;">
1128
- <h1>{{ section.title }}</h1>
1129
- <p>{{ section.subtitle | replace('\\n', '<br>') | safe }}</p>
1130
- {% if section.cta_text and section.cta_link %}
1131
- <a href="{{ section.cta_link }}" class="cta-button">{{ section.cta_text }}</a>
1132
- {% endif %}
1133
- {% if section.image_url %}<img src="{{ section.image_url }}" alt="{{ section.title }}">{% endif %}
1134
- </div>
1135
- {% elif section.type == 'text' %}
1136
- <div class="text-content">
1137
- <p>{{ section.content | replace('\\n', '<br>') | safe }}</p>
1138
- </div>
1139
- {% elif section.type == 'list' and site_data.data_items %}
1140
- <div class="grid-container">
1141
- {% for item in site_data.data_items %}
1142
- {% if not section.filter_category or item.category == section.filter_category %}
1143
- <div class="grid-item">
1144
- {% if item.image_url %}<img src="{{ item.image_url }}" alt="{{ item.name }}">{% endif %}
1145
- <h3>{{ item.name }}</h3>
1146
- <p>{{ item.description | default('') | replace('\\n', '<br>') | safe }}</p>
1147
- {% if item.price %}<div class="price">{{ "%.2f"|format(item.price) }}</div>{% endif %}
1148
- {% if item.category %}<p style="font-size: 0.9em; color: #999;">Категория: {{ item.category }}</p>{% endif %}
1149
- {% for key, value in item.fields.items() %}
1150
- <p style="font-size: 0.85em; color: #555;"><strong>{{ key|capitalize }}:</strong> {{ value }}</p>
1151
- {% endfor %}
1152
- </div>
1153
- {% endif %}
1154
- {% endfor %}
1155
- </div>
1156
- {% elif section.type == 'features' %}
1157
- <div class="grid-container">
1158
- {% for item in section.items if section.items else site_data.data_items %}
1159
- {% if item.icon or item.name or item.description %}
1160
- <div class="grid-item feature-item">
1161
- {% if item.icon %}<i class="{{ item.icon }} feature-icon"></i>{% endif %}
1162
- <h3>{{ item.name }}</h3>
1163
- <p>{{ item.description | default('') | replace('\\n', '<br>') | safe }}</p>
1164
- </div>
1165
- {% endif %}
1166
- {% endfor %}
1167
- </div>
1168
- {% elif section.type == 'testimonials' %}
1169
- <div class="grid-container">
1170
- {% for item in site_data.data_items %}
1171
- {% if item.author and item.description %}
1172
- <div class="grid-item testimonial-item">
1173
- {% if item.image_url %}<img src="{{ item.image_url }}" alt="{{ item.author }}">{% endif %}
1174
- <p>"{{ item.description | replace('\\n', '<br>') | safe }}"</p>
1175
- <div class="author">{{ item.author }}</div>
1176
- {% if item.role %}<div class="role">{{ item.role }}</div>{% endif %}
1177
- </div>
1178
- {% endif %}
1179
- {% endfor %}
1180
- </div>
1181
- {% elif section.type == 'pricing' %}
1182
- <div class="grid-container">
1183
- {% for item in site_data.data_items %}
1184
- {% if item.name and item.price %}
1185
- <div class="grid-item pricing-plan">
1186
- <h3>{{ item.name }}</h3>
1187
- <div class="price">{{ "%.2f"|format(item.price) }}</div>
1188
- {% if item.description %}
1189
- <ul>
1190
- {% for feature in item.description.split('\\n') %}
1191
- <li>{{ feature }}</li>
1192
- {% endfor %}
1193
- </ul>
1194
- {% endif %}
1195
- <a href="#" class="cta-button">Выбрать план</a>
1196
- </div>
1197
- {% endif %}
1198
- {% endfor %}
1199
- </div>
1200
- {% elif section.type == 'contact' %}
1201
- <div class="contact-section">
1202
- {% if section.content %}<p>{{ section.content | replace('\\n', '<br>') | safe }}</p>{% endif %}
1203
- <div class="contact-info">
1204
- {% if section.email %}<p><strong>Email:</strong> <a href="mailto:{{ section.email }}">{{ section.email }}</a></p>{% endif %}
1205
- {% if section.phone %}<p><strong>Телефон:</strong> <a href="tel:{{ section.phone }}">{{ section.phone }}</a></p>{% endif %}
1206
- {% if section.address %}<p><strong>Адрес:</strong> {{ section.address }}</p>{% endif %}
1207
- </div>
1208
- {% if section.form_fields %}
1209
- <div class="contact-form">
1210
- <form action="#" method="POST" onsubmit="alert('Эта форма только для демонстрации. Отправка данных не реализована.'); return false;">
1211
- {% for field in section.form_fields %}
1212
- <label for="field-{{ loop.index }}">{{ field.label }}:</label>
1213
- {% if field.type == 'textarea' %}
1214
- <textarea id="field-{{ loop.index }}" name="{{ field.label | lower | replace(' ', '_') }}" placeholder="{{ field.label }}" required></textarea>
1215
- {% else %}
1216
- <input type="{{ field.type }}" id="field-{{ loop.index }}" name="{{ field.label | lower | replace(' ', '_') }}" placeholder="{{ field.label }}" required>
1217
- {% endif %}
1218
- {% endfor %}
1219
- <button type="submit">Отправить сообщение</button>
1220
- </form>
1221
- </div>
1222
- {% endif %}
1223
- </div>
1224
- {% elif section.type == 'image_gallery' and section.images %}
1225
- <div class="grid-container">
1226
- {% for image_url in section.images %}
1227
- <div class="grid-item">
1228
- <img src="{{ image_url }}" alt="Галерея">
1229
- </div>
1230
- {% endfor %}
1231
- </div>
1232
- {% endif %}
1233
- </div>
1234
- {% endfor %}
1235
 
1236
- {% if site_data.footer_text %}
1237
- <footer>
1238
- <p>{{ site_data.footer_text | default(site_data.site_title + ' © ' + now.year|string + '. Все права защищены.') }}</p>
1239
- </footer>
1240
- {% endif %}
1241
- </div>
1242
- </body>
1243
- </html>
1244
- """
1245
 
1246
  @app.route('/')
1247
  def index():
1248
- generated_sites = load_site_metadata()
1249
- sorted_sites = dict(sorted(generated_sites.items(), key=lambda item: item[1].get('timestamp', ''), reverse=True))
1250
- return render_template_string(index_page_template, generated_sites=sorted_sites)
1251
 
1252
  @app.route('/generate', methods=['POST'])
1253
  def handle_generate():
1254
  if 'prompt' not in request.form:
1255
- flash("Текстовый запрос (промпт) не найден.", 'error')
1256
  return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
 
1257
  user_prompt = request.form['prompt']
 
1258
  if not user_prompt or not user_prompt.strip():
1259
- flash("Текстовый запрос не может быть пустым.", 'error')
1260
- return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
 
 
 
 
1261
  try:
1262
- site_data_json = generate_site_json_from_prompt(user_prompt)
1263
- if not site_data_json:
1264
- flash("Сгенерированные данные сайта пусты.", 'error')
1265
- return jsonify({"error": "Сгенерированные данные сайта пусты."}), 500
1266
- site_id = str(uuid.uuid4())
1267
- generated_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1268
- site_metadata = load_site_metadata()
1269
- site_metadata[site_id] = {
1270
- "timestamp": generated_at,
1271
- "ai_generated_data": site_data_json
1272
- }
1273
- save_site_metadata(site_metadata)
1274
- site_url = url_for('serve_generated_site', site_id=site_id)
1275
- flash("Сайт успешно сгенерирован!", 'success')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1276
  return jsonify({"site_url": site_url})
 
1277
  except ValueError as ve:
1278
- logging.error(f"Ошибка генерации (ValueError): {ve}")
1279
- flash(f"Ошибка генерации: {str(ve)}", 'error')
 
 
1280
  return jsonify({"error": str(ve)}), 400
1281
  except Exception as e:
1282
- logging.error(f"Неожиданная ошибка во время генерации сайта: {e}", exc_info=True)
1283
- flash(f"Внутренняя ошибка сервера при генерации сайта: {e}", 'error')
 
 
1284
  return jsonify({"error": f"Внутренняя ошибка сервера при генерации сайта: {e}"}), 500
1285
 
1286
- @app.route('/generated_site/<site_id>')
1287
- def serve_generated_site(site_id):
1288
- site_metadata = load_site_metadata()
1289
- site_info = site_metadata.get(site_id)
1290
- if not site_info:
1291
- flash(f"Сайт с ID '{site_id}' не найден.", 'error')
1292
- return redirect(url_for('index'))
1293
- site_data = site_info.get('ai_generated_data')
1294
- if not site_data:
1295
- flash(f"Данные для сайта с ID '{site_id}' повреждены.", 'error')
1296
- return redirect(url_for('index'))
1297
- return render_template_string(dynamic_site_template, site_data=site_data, now=datetime.now())
1298
-
1299
- @app.route('/delete_site', methods=['POST'])
1300
- def delete_site():
1301
- site_id_to_delete = request.form.get('site_id')
1302
- if not site_id_to_delete:
1303
- flash("ID сайта для удаления не предоставлен.", 'error')
1304
- return redirect(url_for('index'))
1305
- site_metadata = load_site_metadata()
1306
- if site_id_to_delete in site_metadata:
1307
- del site_metadata[site_id_to_delete]
1308
- save_site_metadata(site_metadata)
1309
- flash(f"Сайт с ID '{site_id_to_delete}' успешно удален.", 'success')
1310
- else:
1311
- flash(f"Сайт с ID '{site_id_to_delete}' не найден.", 'warning')
1312
- return redirect(url_for('index'))
1313
 
1314
  if __name__ == '__main__':
1315
- logging.info("Приложение запускается. Выполняется первоначальная загрузка/скачивание данных...")
1316
- download_db_from_hf()
1317
- load_site_metadata()
1318
- logging.info("Первоначальная загрузка данных завершена.")
 
 
 
 
 
 
 
 
 
 
 
1319
 
1320
- if API_KEY_INTERNAL is None:
1321
- logging.error("Переменная окружения GOOGLE_API_KEY не установлена. Генерация AI будет завершаться ошибкой.")
1322
- flash("Внимание: Google AI API ключ не настроен. Генерация сайтов будет недоступна.", "warning")
1323
 
 
1324
  if HF_TOKEN_WRITE:
1325
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1326
  backup_thread.start()
1327
- logging.info("Поток периодического резервного копирования запущен.")
1328
  else:
1329
- logging.warning("Периодическое резервное копирование НЕ будет выполняться (HF_TOKEN для записи не установлен).")
1330
 
1331
  port = int(os.environ.get('PORT', 7860))
1332
- logging.info(f"Запуск Flask-приложения на хосте 0.0.0.0 и порту {port}")
1333
- app.run(debug=False, host='0.0.0.0', port=port)
 
1
  import os
2
  import uuid
3
+ from flask import Flask, request, jsonify, Response, Blueprint, render_template_string
4
+ import google.generativeai as genai
5
  import json
6
  import logging
7
  import threading
8
  import time
9
  from datetime import datetime
10
+ from huggingface_hub import HfApi, hf_hub_download, create_repo, snapshot_download
 
 
 
11
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
12
+ from werkzeug.utils import secure_filename # Unused in final version, but kept for context if file uploads were needed
13
  from dotenv import load_dotenv
14
+ import requests
15
+ import importlib.util
16
+ import sys
17
+ import shutil # For deleting directories
18
 
19
+ # Load environment variables from .env file
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  load_dotenv()
21
 
22
  app = Flask(__name__)
 
23
 
24
+ # --- Configuration from Code 2 and New/Modified for Code 1 ---
25
  REPO_ID = "Kgshop/testsynk"
26
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Ensure this is set for write access
27
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Optional, falls back to WRITE if not set
28
 
29
+ GENERATED_SITES_DIR = 'generated_sites'
30
+ GENERATED_APPS_METADATA_FILE = os.path.join(GENERATED_SITES_DIR, 'generated_apps.json')
31
 
32
+ # Global dictionary to hold dynamically loaded blueprints
33
+ # Key: app_uuid, Value: blueprint_object
34
+ _GENERATED_BLUEPRINTS = {}
35
 
36
+ # Ensure base directory for generated sites exists
37
+ if not os.path.exists(GENERATED_SITES_DIR):
38
+ os.makedirs(GENERATED_SITES_DIR)
39
+
40
+ # --- Logging Setup from Code 2 ---
41
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
42
 
43
+ # --- Hugging Face Utility Functions (Adapted from Code 2 for directory sync) ---
 
44
 
45
+ DOWNLOAD_RETRIES = 3
46
+ DOWNLOAD_DELAY = 5
47
+
48
+ def create_hf_repo_if_not_exists():
49
+ """Ensures the Hugging Face dataset repository exists."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  if not HF_TOKEN_WRITE:
51
+ logging.warning("HF_TOKEN for writing not set. Cannot create Hugging Face repo.")
52
+ return False
53
  try:
54
+ api = HfApi(token=HF_TOKEN_WRITE)
55
+ api.create_repo(repo_id=REPO_ID, repo_type="dataset", exist_ok=True, private=True)
56
+ logging.info(f"Hugging Face dataset repo '{REPO_ID}' ensured to exist.")
57
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  except Exception as e:
59
+ logging.error(f"Failed to create/ensure Hugging Face repo '{REPO_ID}': {e}")
60
+ return False
61
 
62
+ def download_data_from_hf(target_dir="."):
63
+ """Downloads the entire repository snapshot from Hugging Face."""
64
+ if not REPO_ID:
65
+ logging.warning("REPO_ID not set. Skipping Hugging Face download.")
66
+ return False
 
 
67
 
68
+ token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
69
+ if not token_to_use:
70
+ logging.warning("No Hugging Face token available for download. Skipping.")
71
+ return False
72
+
73
+ logging.info(f"Attempting full repo snapshot download for '{REPO_ID}' to '{target_dir}'...")
74
+ success = False
75
+ for attempt in range(DOWNLOAD_RETRIES + 1):
 
 
 
 
 
 
 
76
  try:
77
+ # `snapshot_download` downloads the entire repo to the specified local_dir
78
+ logging.info(f"Downloading snapshot of repo '{REPO_ID}' (Attempt {attempt + 1}/{DOWNLOAD_RETRIES + 1})...")
79
+ # Clear existing content in target_dir if it's the `generated_sites` directory
80
+ if os.path.exists(target_dir) and target_dir == GENERATED_SITES_DIR:
81
+ logging.info(f"Clearing existing local '{target_dir}' before download to ensure full sync.")
82
+ # Keep the base directory but remove its contents
83
+ for item in os.listdir(target_dir):
84
+ item_path = os.path.join(target_dir, item)
85
+ if os.path.isfile(item_path):
86
+ os.remove(item_path)
87
+ elif os.path.isdir(item_path):
88
+ shutil.rmtree(item_path)
89
+
90
+ # Download the snapshot into the current working directory, then copy `generated_sites`
91
+ downloaded_repo_path = snapshot_download(
92
+ repo_id=REPO_ID,
93
+ repo_type="dataset",
94
+ token=token_to_use,
95
+ local_dir="hf_download_temp", # Download to a temp directory first
96
+ local_dir_use_symlinks=False,
97
+ # force_download=True # Uncomment to always redownload, useful for development
98
+ )
99
+ logging.info(f"Repo snapshot downloaded to {downloaded_repo_path}")
100
+
101
+ # Move contents of `generated_sites` from temp download to actual GENERATED_SITES_DIR
102
+ source_generated_sites = os.path.join(downloaded_repo_path, 'generated_sites')
103
+ if os.path.exists(source_generated_sites):
104
+ for item in os.listdir(source_generated_sites):
105
+ s = os.path.join(source_generated_sites, item)
106
+ d = os.path.join(GENERATED_SITES_DIR, item)
107
+ if os.path.isdir(s):
108
+ if os.path.exists(d): shutil.rmtree(d) # Remove existing sub-dir before moving
109
+ shutil.move(s, d)
110
+ else:
111
+ if os.path.exists(d): os.remove(d) # Remove existing file before moving
112
+ shutil.move(s, d)
113
+ logging.info(f"Moved generated_sites content from {source_generated_sites} to {GENERATED_SITES_DIR}")
114
+ else:
115
+ logging.warning(f"No 'generated_sites' folder found in the downloaded repo {downloaded_repo_path}.")
116
+
117
+ # Clean up the temporary download directory
118
+ if os.path.exists(downloaded_repo_path):
119
+ shutil.rmtree(downloaded_repo_path)
120
+ logging.info(f"Cleaned up temporary download directory {downloaded_repo_path}.")
121
+
122
+ success = True
123
+ break
124
+ except RepositoryNotFoundError:
125
+ logging.error(f"Repository {REPO_ID} not found on Hugging Face. Ensuring its creation...")
126
+ if create_hf_repo_if_not_exists():
127
+ logging.info(f"Repository {REPO_ID} created. Retrying download after creation.")
128
+ continue # Retry the download as repo now exists
129
+ return False # Cannot proceed without repo
130
+ except HfHubHTTPError as e:
131
+ logging.error(f"HTTP error downloading repo (Attempt {attempt + 1}): {e}. Retrying in {DOWNLOAD_DELAY}s...")
132
+ except requests.exceptions.RequestException as e:
133
+ logging.error(f"Network error downloading repo (Attempt {attempt + 1}): {e}. Retrying in {DOWNLOAD_DELAY}s...", exc_info=True)
134
  except Exception as e:
135
+ logging.error(f"Unexpected error during repo snapshot download (Attempt {attempt + 1}): {e}. Retrying in {DOWNLOAD_DELAY}s...", exc_info=True)
136
+
137
+ if attempt < DOWNLOAD_RETRIES:
138
+ time.sleep(DOWNLOAD_DELAY)
139
+
140
+ if not success:
141
+ logging.error(f"Failed to download repo '{REPO_ID}' after {DOWNLOAD_RETRIES + 1} attempts.")
142
+ return success
143
+
144
+ def upload_data_to_hf(target_dir="."):
145
+ """Uploads the specified directory to Hugging Face."""
146
+ if not HF_TOKEN_WRITE:
147
+ logging.warning("HF_TOKEN for writing not set. Skipping upload to Hugging Face.")
148
+ return
149
+
150
  try:
151
+ api = HfApi(token=HF_TOKEN_WRITE)
152
+ logging.info(f"Starting upload of directory '{target_dir}' to HF repo '{REPO_ID}'...")
153
+
154
+ # Use upload_folder to sync the entire directory
155
+ # path_in_repo='.' means upload contents of target_dir to root of repo.
156
+ # If target_dir is 'generated_sites', it will upload 'generated_sites' itself to the repo root.
157
+ # This is desired to keep the folder structure consistent.
158
+ api.upload_folder(
159
+ folder_path=target_dir,
160
+ repo_id=REPO_ID,
161
+ repo_type="dataset",
162
+ token=HF_TOKEN_WRITE,
163
+ commit_message=f"Sync {target_dir} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
164
+ )
165
+ logging.info(f"Directory '{target_dir}' successfully uploaded to Hugging Face.")
166
  except Exception as e:
167
+ logging.error(f"Error during Hugging Face folder upload for '{target_dir}': {e}", exc_info=True)
168
 
169
+ def load_generated_apps_metadata():
170
+ """Loads metadata about generated apps from generated_apps.json."""
171
+ if not os.path.exists(GENERATED_APPS_METADATA_FILE):
172
+ logging.warning(f"Metadata file {GENERATED_APPS_METADATA_FILE} not found. Creating empty structure.")
173
+ return {"apps": {}}
174
  try:
175
+ with open(GENERATED_APPS_METADATA_FILE, 'r', encoding='utf-8') as f:
176
+ data = json.load(f)
177
+ if not isinstance(data, dict) or "apps" not in data:
178
+ logging.warning(f"Invalid format in {GENERATED_APPS_METADATA_FILE}. Resetting to empty.")
179
+ return {"apps": {}}
180
+ return data
181
+ except json.JSONDecodeError as e:
182
+ logging.error(f"Error decoding JSON from {GENERATED_APPS_METADATA_FILE}: {e}. File might be corrupt. Resetting to empty.")
183
+ return {"apps": {}}
184
  except Exception as e:
185
+ logging.error(f"Unexpected error loading metadata from {GENERATED_APPS_METADATA_FILE}: {e}. Resetting to empty.", exc_info=True)
186
+ return {"apps": {}}
 
 
187
 
188
+ def save_generated_apps_metadata(metadata):
189
+ """Saves metadata about generated apps to generated_apps.json and triggers HF upload."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  try:
191
+ with open(GENERATED_APPS_METADATA_FILE, 'w', encoding='utf-8') as f:
192
+ json.dump(metadata, f, ensure_ascii=False, indent=4)
193
+ logging.info(f"Metadata saved to {GENERATED_APPS_METADATA_FILE}")
194
+ # Trigger upload of the entire generated_sites directory including the metadata file
195
+ upload_data_to_hf(GENERATED_SITES_DIR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  except Exception as e:
197
+ logging.error(f"Error saving metadata to {GENERATED_APPS_METADATA_FILE}: {e}", exc_info=True)
198
+
199
+ def load_and_register_blueprint(app_uuid, app_path):
200
+ """
201
+ Dynamically loads a Flask Blueprint from 'app.py' within a given app_path
202
+ and registers it with the main Flask app.
203
+ """
204
+ if app_uuid in _GENERATED_BLUEPRINTS:
205
+ logging.info(f"Blueprint {app_uuid} already loaded.")
206
+ return True
207
+
208
+ module_path = os.path.join(app_path, 'app.py')
209
+ if not os.path.exists(module_path):
210
+ logging.error(f"App module not found at {module_path} for UUID {app_uuid}. Skipping.")
211
+ return False
212
+
213
+ try:
214
+ # Create a module spec from the file path
215
+ # Use a unique name for the module to avoid conflicts, e.g., 'generated_app_UUID'
216
+ spec = importlib.util.spec_from_file_location(f"generated_app_{app_uuid}", module_path)
217
+ if spec is None:
218
+ raise ImportError(f"Could not create module spec for {module_path}")
219
+
220
+ # Create a new module based on the spec
221
+ module = importlib.util.module_from_spec(spec)
222
+ # Add to sys.modules for proper import behavior and to allow other modules to import it if needed
223
+ sys.modules[spec.name] = module
224
+
225
+ # Execute the module's code
226
+ spec.loader.exec_module(module)
227
+
228
+ # Look for the 'generated_blueprint' instance in the module
229
+ blueprint_found = getattr(module, 'generated_blueprint', None)
230
+
231
+ if isinstance(blueprint_found, Blueprint):
232
+ app.register_blueprint(blueprint_found, url_prefix=f'/generated_sites/{app_uuid}')
233
+ _GENERATED_BLUEPRINTS[app_uuid] = blueprint_found
234
+ logging.info(f"Blueprint '{blueprint_found.name}' for UUID {app_uuid} loaded and registered under /generated_sites/{app_uuid}")
235
+ return True
236
  else:
237
+ logging.error(f"No Flask Blueprint named 'generated_blueprint' found in {module_path} for UUID {app_uuid}.")
238
+ return False
239
+ except Exception as e:
240
+ logging.error(f"Failed to load and register blueprint from {module_path} for UUID {app_uuid}: {e}", exc_info=True)
241
+ return False
242
+
243
+ def load_all_generated_blueprints():
244
+ """Loads all generated blueprints listed in metadata on application startup."""
245
+ logging.info("Attempting to load all previously generated blueprints...")
246
+ metadata = load_generated_apps_metadata()
247
+ if not metadata.get("apps"):
248
+ logging.info("No generated apps metadata found to load.")
249
+ return
250
 
251
+ for app_uuid, app_info in metadata.get("apps", {}).items():
252
+ app_dir = os.path.join(GENERATED_SITES_DIR, app_uuid)
253
+ if not os.path.exists(app_dir):
254
+ logging.warning(f"Directory for app {app_uuid} not found locally at {app_dir}. Skipping load.")
255
+ continue
256
+ load_and_register_blueprint(app_uuid, app_dir)
257
+ logging.info("Completed loading of existing blueprints.")
258
+
259
+
260
+ def periodic_backup():
261
+ """Starts a periodic backup thread for the generated_sites directory."""
262
+ backup_interval = 1800 # 30 minutes
263
+ logging.info(f"Setting up periodic backup of generated_sites directory every {backup_interval} seconds.")
264
+ while True:
265
+ time.sleep(backup_interval)
266
+ logging.info("Starting periodic backup of generated_sites directory...")
267
+ upload_data_to_hf(GENERATED_SITES_DIR)
268
+ logging.info("Periodic backup finished.")
269
+
270
+
271
+ # --- Flask App Routes and AI Generation Logic ---
272
+
273
+ # Initial HTML template for the generator UI (from Code 1, slightly adapted for clarity)
274
+ html_template = """
275
  <!DOCTYPE html>
276
  <html lang="ru">
277
  <head>
278
  <meta charset="UTF-8">
279
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
280
  <title>EVA - Генератор Сайтов</title>
 
281
  <style>
282
  :root {
283
  --system-gray-100-light: #f2f2f7;
 
333
  --secondary-text-color: var(--system-gray-light-75-light);
334
  --tertiary-text-color: var(--system-gray-light-50-light);
335
  --border-color: var(--system-separator-light);
336
+ --border-color-opaque: var(--system-separator-opaque-light);
337
  --input-bg: var(--system-gray-75-light);
338
  --primary-color: var(--system-blue-light);
339
  --primary-color-hover: var(--system-blue-light-hover);
 
342
  }
343
 
344
  html {
345
+ height: -webkit-fill-available;
346
  }
347
 
348
  body {
349
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
350
  margin: 0;
351
  padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom);
352
  background-color: var(--bg-color);
 
366
  padding: 25px 30px 30px 30px;
367
  border-radius: 24px;
368
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
369
+ max-width: 580px;
370
  width: calc(100% - 40px);
371
  box-sizing: border-box;
372
  margin-top: 30px;
 
418
  }
419
 
420
  textarea#prompt-input:focus {
421
+ border-color: var(--primary-color);
422
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent);
423
+ outline: none;
424
  }
425
 
426
  button#generate-button {
 
455
  }
456
 
457
  .output-header {
458
+ display: flex;
459
+ justify-content: space-between;
460
+ align-items: center;
461
+ margin-bottom: 10px;
462
  }
463
 
464
  label#output-label {
 
490
  }
491
 
492
  button#copy-button.copied {
493
+ color: #34c759;
494
  }
495
  @media (prefers-color-scheme: dark) {
496
+ button#copy-button.copied {
497
+ color: #30d158;
498
+ }
499
  }
500
 
501
  #output-container {
 
524
  text-decoration: underline;
525
  }
526
 
527
+
528
  #output-container.loading::before {
529
  content: "Генерация сайта...";
530
  display: block;
 
547
  50% { opacity: 1; }
548
  }
549
 
550
+ @media (max-width: 620px) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  body {
552
  padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom);
553
  align-items: flex-start;
 
561
  h1 {
562
  font-size: 28px;
563
  }
564
+ p.subtitle {
565
  font-size: 16px;
566
  margin-bottom: 25px;
567
  }
568
  .form-group {
569
  margin-bottom: 22px;
570
  }
571
+ textarea#prompt-input {
572
  padding: 12px 15px;
573
  min-height: 100px;
574
+ }
575
  button#generate-button {
576
+ padding: 15px;
577
+ font-size: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  }
579
+ #output-container {
580
+ padding: 15px 18px;
581
+ font-size: 14px;
582
+ min-height: 50px;
583
+ }
584
+ .output-section {
585
+ margin-top: 30px;
586
+ }
587
  }
588
  </style>
589
  </head>
 
592
  <h1>EVA</h1>
593
  <p class="subtitle">Генератор сайтов на базе ИИ</p>
594
 
 
 
 
 
 
 
 
 
595
  <form id="generate-form">
596
  <div class="form-group">
597
  <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать</label>
598
+ <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай одностраничный сайт-портфолио для веб-дизайнера по имени Алия, с секциями 'Обо мне', 'Мои работы' и 'Контакты'. Используй современный минималистичный дизайн."></textarea>
599
  </div>
600
 
601
  <button type="submit" id="generate-button">Создать сайт</button>
602
  </form>
603
 
604
  <div class="output-section">
605
+ <div class="output-header">
606
+ <label id="output-label">Ссылка на сайт</label>
607
+ <button id="copy-button">Копировать</button>
608
+ </div>
609
  <div id="output-container" aria-live="polite">
610
  </div>
611
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  </div>
613
 
614
  <script>
 
637
  copyButton.textContent = 'Копировать';
638
  copyButton.classList.remove('copied');
639
 
640
+
641
  try {
642
  const response = await fetch('/generate', {
643
  method: 'POST',
 
659
  outputContainer.appendChild(link);
660
  copyButton.style.display = 'block';
661
  copyButton.dataset.copyText = window.location.origin + result.site_url;
 
 
 
662
  } else if (result.error) {
663
  showError(result.error);
664
  } else {
 
684
  copyButton.textContent = 'Скопировано!';
685
  copyButton.classList.add('copied');
686
  setTimeout(() => {
687
+ copyButton.textContent = 'Копировать';
688
+ copyButton.classList.remove('copied');
689
  }, 1500);
690
  }).catch(err => {
691
  console.error('Ошибка копирования: ', err);
692
  copyButton.textContent = 'Ошибка';
693
+ setTimeout(() => {
694
+ copyButton.textContent = 'Копировать';
695
  }, 1500);
696
  });
697
  });
698
 
699
  function showError(message) {
700
+ outputContainer.innerHTML = '';
701
+ const errorMessageElement = document.createElement('span');
702
+ errorMessageElement.textContent = message;
703
+ outputContainer.appendChild(errorMessageElement);
704
+ outputContainer.classList.add('error');
705
+ outputContainer.classList.remove('loading');
706
+ copyButton.style.display = 'none';
707
  }
708
+
709
  </script>
710
  </body>
711
  </html>
712
  """
713
 
714
+ def generate_website_code_from_prompt(user_prompt):
715
+ """
716
+ Generates Flask Blueprint Python code based on the user prompt.
717
+ """
718
+ try:
719
+ # Using GEMINI_API_KEY from .env
720
+ gemini_api_key = os.getenv("GEMINI_API_KEY")
721
+ if not gemini_api_key:
722
+ raise ValueError("GEMINI_API_KEY environment variable not set. Please add it to your .env file.")
723
+ genai.configure(api_key=gemini_api_key)
724
+ except Exception as e:
725
+ logging.error(f"Error configuring GenAI: {e}")
726
+ raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
 
 
 
 
 
 
 
 
727
 
728
+ if not user_prompt or not user_prompt.strip():
729
+ raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
730
+
731
+ # Updated system instruction for generating a Flask Blueprint
732
+ system_instruction = (
733
+ "You are an expert web developer specializing in Flask. Your task is to generate a complete, "
734
+ "self-contained Python Flask Blueprint code string based on the user's request. "
735
+ "This Blueprint will be dynamically loaded into a larger Flask application.\n"
736
+ "The generated Python string must adhere to the following rules:\n"
737
+ "1. It must start with necessary imports: `from flask import Blueprint, render_template_string, request, jsonify`.\n"
738
+ " If managing data via JSON file, also include `import json` and `import os`.\n"
739
+ "2. It must define a Flask Blueprint instance, named `generated_blueprint`. For example: `generated_blueprint = Blueprint('my_app_name', __name__)`.\n"
740
+ "3. All HTML, CSS (in `<style>` tags), and JavaScript (in `<script>` tags) should be embedded directly as multi-line Python strings within the blueprint's route functions. Do NOT use external files for these. Use f-strings for dynamic content where needed.\n"
741
+ "4. If the user requests 'database' functionality, simulate it with simple in-memory Python lists or dictionaries. If persistence is implied, provide a basic JSON file read/write mechanism within the Blueprint. The JSON file should be stored in the same directory as the `app.py` file using `os.path.join(os.path.dirname(__file__), 'data.json')`. Implement simple `load_data()` and `save_data()` functions inside the blueprint.\n"
742
+ "5. Define at least one route, typically the root (`/`), within the Blueprint, like `@generated_blueprint.route('/')`.\n"
743
+ "6. The code should be functional and visually appealing, using basic modern styling where appropriate.\n"
744
+ "7. The code must NOT include `app.run()` or any top-level Flask application instance creation (only a Blueprint).\n"
745
+ "8. For images or other assets, assume placeholder services (e.g., `https://via.placeholder.com/`) or base64 if small. Do not assume local static files that are not part of the `app.py` itself unless explicitly handled by Blueprint's `static_folder` (which you should define if needed, e.g., `static_folder='static', static_url_path='/static'`). For simplicity, prefer embedding or external links.\n"
746
+ "9. Directly output ONLY the Python code string. Do not include any explanatory text, markdown formatting (like ```python), or anything else before or after the code itself."
747
+ )
748
+
749
+ full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
750
+
751
+ response = None
752
+ try:
753
+ model = genai.GenerativeModel('gemini-1.5-flash-latest')
754
+ response = model.generate_content(full_prompt)
755
+
756
+ generated_text = ""
757
+ if hasattr(response, 'text') and response.text:
758
+ generated_text = response.text
759
+ elif response.parts:
760
+ generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
761
+
762
+ # Attempt to clean markdown if present (common issue with AI outputs)
763
+ clean_text = generated_text.strip()
764
+ if clean_text.startswith("```python"):
765
+ clean_text = clean_text[len("```python"):].strip()
766
+ if clean_text.endswith("```"):
767
+ clean_text = clean_text[:-len("```")].strip()
768
+ generated_text = clean_text
769
+
770
+ if not generated_text.strip():
771
+ if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
772
+ reason = response.prompt_feedback.block_reason
773
+ raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другой запрос.")
774
+ else:
775
+ raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
776
+
777
+ # Basic validation for expected blueprint structure
778
+ if "from flask import Blueprint" not in generated_text or "generated_blueprint = Blueprint(" not in generated_text:
779
+ logging.warning(f"Generated text does not seem to contain a Flask Blueprint definition as expected. Preview: {generated_text[:500]}")
780
+ raise ValueError("Модель не сгенерировала корректный код Flask Blueprint. Попробуйте другой запрос или переформулируйте.")
781
+
782
+ return generated_text
783
+
784
+ except Exception as e:
785
+ logging.error(f"Error generating content with GenAI: {e}")
786
+ error_message = str(e)
787
+ if "API key not valid" in error_message:
788
+ raise ValueError("Внутренняя ошибка конфигурации API. Проверьте GEMINI_API_KEY в .env.")
789
+ elif "Billing account not found" in error_message or "billing account" in error_message.lower():
790
+ raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте статус вашего биллинг-аккаунта.")
791
+ elif "Could not find model" in error_message:
792
+ raise ValueError(f"Модель 'gemini-1.5-flash-latest' не найдена или недоступна. Возможно, регион или квоты.")
793
+ elif "resource has been exhausted" in error_message.lower() or "quota" in error_message.lower():
794
+ raise ValueError("Квота запросов к AI-модели исчерпана. Попробуйте позже.")
795
+ elif ("content has been blocked" in error_message.lower() or
796
+ (response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason)):
797
+ reason = "неизвестна"
798
+ if response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
799
+ reason = response.prompt_feedback.block_reason
800
+ elif "safety settings" in error_message.lower():
801
+ reason = "настройки безопасности"
802
+
803
+ raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
804
+ else:
805
+ raise ValueError(f"Ошибка при генерации Flask Blueprint кода: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806
 
 
 
 
 
 
 
 
 
 
807
 
808
  @app.route('/')
809
  def index():
810
+ return Response(html_template, mimetype='text/html')
811
+
 
812
 
813
  @app.route('/generate', methods=['POST'])
814
  def handle_generate():
815
  if 'prompt' not in request.form:
 
816
  return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
817
+
818
  user_prompt = request.form['prompt']
819
+
820
  if not user_prompt or not user_prompt.strip():
821
+ return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
822
+
823
+ app_uuid = str(uuid.uuid4())
824
+ app_dir = os.path.join(GENERATED_SITES_DIR, app_uuid)
825
+ app_file_path = os.path.join(app_dir, 'app.py')
826
+
827
  try:
828
+ logging.info(f"Generating blueprint for prompt: '{user_prompt[:100]}...' [UUID: {app_uuid}]")
829
+ blueprint_code = generate_website_code_from_prompt(user_prompt)
830
+
831
+ if not blueprint_code or not blueprint_code.strip():
832
+ return jsonify({"error": "Сгенерированный Python код Blueprint пуст."}), 500
833
+
834
+ # Create the unique directory for this app
835
+ os.makedirs(app_dir, exist_ok=True)
836
+
837
+ # Save the generated Blueprint code
838
+ with open(app_file_path, "w", encoding="utf-8") as f:
839
+ f.write(blueprint_code)
840
+ logging.info(f"Generated blueprint saved to {app_file_path}")
841
+
842
+ # Dynamically load and register the new blueprint
843
+ if not load_and_register_blueprint(app_uuid, app_dir):
844
+ # If loading fails, ensure cleanup of the created directory
845
+ if os.path.exists(app_dir):
846
+ shutil.rmtree(app_dir)
847
+ raise Exception("Не удалось загрузить или зарегистрировать сгенерированный Blueprint. Возможно, код содержит ошибки.")
848
+
849
+ # Update metadata for persistence and future loading
850
+ metadata = load_generated_apps_metadata()
851
+ metadata["apps"][app_uuid] = {
852
+ "created_at": datetime.now().isoformat(),
853
+ "prompt": user_prompt,
854
+ "path": os.path.relpath(app_dir, GENERATED_SITES_DIR), # Store relative path
855
+ "url_prefix": f"/generated_sites/{app_uuid}"
856
+ }
857
+ save_generated_apps_metadata(metadata) # This also triggers HF upload
858
+ logging.info(f"Metadata updated and triggered Hugging Face upload for app: {app_uuid}.")
859
+
860
+ site_url = f"/generated_sites/{app_uuid}"
861
  return jsonify({"site_url": site_url})
862
+
863
  except ValueError as ve:
864
+ logging.error(f"ValueError during site generation: {ve}", exc_info=True)
865
+ # Clean up partial directory if creation failed midway
866
+ if os.path.exists(app_dir) and not os.listdir(app_dir):
867
+ os.rmdir(app_dir)
868
  return jsonify({"error": str(ve)}), 400
869
  except Exception as e:
870
+ logging.error(f"Unexpected error during Flask Blueprint generation/registration: {e}", exc_info=True)
871
+ # Attempt to clean up generated files and directory if an error occurred
872
+ if os.path.exists(app_dir):
873
+ shutil.rmtree(app_dir)
874
  return jsonify({"error": f"Внутренняя ошибка сервера при генерации сайта: {e}"}), 500
875
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
876
 
877
  if __name__ == '__main__':
878
+ logging.info("Application starting up. Performing initial setup and data synchronization...")
879
+
880
+ # 1. Ensure Hugging Face repository exists
881
+ if HF_TOKEN_WRITE:
882
+ create_hf_repo_if_not_exists()
883
+ else:
884
+ logging.warning("HF_TOKEN for writing is not set. Cannot create Hugging Face repo or upload data. Periodic backup will not run.")
885
+
886
+ # 2. Attempt to download existing generated sites data from Hugging Face
887
+ # This will pull the 'generated_sites' folder contents into the local 'generated_sites' folder.
888
+ if REPO_ID:
889
+ logging.info(f"Attempting to download latest generated sites from Hugging Face repo '{REPO_ID}'...")
890
+ download_data_from_hf(GENERATED_SITES_DIR) # Target_dir should be 'generated_sites'
891
+ else:
892
+ logging.warning("REPO_ID not set, skipping Hugging Face snapshot download.")
893
 
894
+ # 3. Load and register existing generated blueprints from local storage (after potential download)
895
+ load_all_generated_blueprints()
896
+ logging.info("Initial blueprint loading complete.")
897
 
898
+ # 4. Start periodic backup thread if HF_TOKEN_WRITE is set
899
  if HF_TOKEN_WRITE:
900
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
901
  backup_thread.start()
902
+ logging.info("Periodic backup thread started.")
903
  else:
904
+ logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
905
 
906
  port = int(os.environ.get('PORT', 7860))
907
+ logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
908
+ app.run(host='0.0.0.0', port=port, debug=False)