Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
import os
|
| 4 |
import uuid
|
| 5 |
import json
|
|
@@ -14,61 +12,59 @@ from huggingface_hub import HfApi, hf_hub_download
|
|
| 14 |
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
|
| 15 |
from dotenv import load_dotenv
|
| 16 |
|
| 17 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
load_dotenv()
|
| 19 |
|
| 20 |
app = Flask(__name__)
|
| 21 |
-
|
| 22 |
-
app.secret_key = os.getenv("FLASK_SECRET_KEY", "your_unique_secret_key_for_eva_app")
|
| 23 |
|
| 24 |
-
|
| 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 |
-
|
| 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 |
-
|
| 72 |
repo_id=REPO_ID,
|
| 73 |
filename=file_name,
|
| 74 |
repo_type="dataset",
|
|
@@ -78,7 +74,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
|
|
| 78 |
force_download=True,
|
| 79 |
resume_download=False
|
| 80 |
)
|
| 81 |
-
logging.info(f"Файл {file_name} успешно
|
| 82 |
success = True
|
| 83 |
break
|
| 84 |
except RepositoryNotFoundError:
|
|
@@ -91,40 +87,32 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
|
|
| 91 |
try:
|
| 92 |
if file_name == DATA_FILE:
|
| 93 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 94 |
-
json.dump({}, f)
|
| 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:
|
|
@@ -146,9 +134,6 @@ def upload_db_to_hf(specific_file=None):
|
|
| 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)
|
|
@@ -156,13 +141,7 @@ def periodic_backup():
|
|
| 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:
|
|
@@ -170,13 +149,12 @@ def load_site_metadata():
|
|
| 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:
|
|
@@ -204,36 +182,25 @@ def load_site_metadata():
|
|
| 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)
|
| 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 |
|
|
@@ -243,7 +210,7 @@ def generate_site_json_from_prompt(user_prompt):
|
|
| 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"
|
|
@@ -252,75 +219,114 @@ def generate_site_json_from_prompt(user_prompt):
|
|
| 252 |
" \"site_title\": \"string\",\n"
|
| 253 |
" \"main_heading\": \"string\",\n"
|
| 254 |
" \"tagline\": \"string\",\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
" \"sections\": [\n"
|
| 256 |
" {\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
" \"title\": \"string\",\n"
|
| 258 |
-
" \"
|
| 259 |
-
" \"
|
| 260 |
" }\n"
|
| 261 |
" ],\n"
|
| 262 |
-
" \"data_items\": [ //
|
| 263 |
" {\n"
|
| 264 |
" \"id\": \"unique_string_or_number\",\n"
|
| 265 |
-
" \"name\": \"string\"
|
| 266 |
-
" \"description\": \"string\"
|
| 267 |
-
" \"price\": \"number (optional)\"
|
| 268 |
-
" \"image_url\": \"string (optional,
|
| 269 |
-
" \"category\": \"string (optional)\"
|
| 270 |
-
" \"
|
|
|
|
|
|
|
|
|
|
| 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`: Массив блоков
|
| 282 |
-
"
|
| 283 |
-
"
|
| 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. Пожалуйста, попробуйте еще раз или измените запрос.")
|
|
@@ -346,9 +352,6 @@ def generate_site_json_from_prompt(user_prompt):
|
|
| 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">
|
|
@@ -425,7 +428,7 @@ index_page_template = """
|
|
| 425 |
}
|
| 426 |
|
| 427 |
body {
|
| 428 |
-
font-family: -apple-system,
|
| 429 |
margin: 0;
|
| 430 |
padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom);
|
| 431 |
background-color: var(--bg-color);
|
|
@@ -445,7 +448,7 @@ index_page_template = """
|
|
| 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;
|
| 449 |
width: calc(100% - 40px);
|
| 450 |
box-sizing: border-box;
|
| 451 |
margin-top: 30px;
|
|
@@ -666,7 +669,6 @@ index_page_template = """
|
|
| 666 |
}
|
| 667 |
}
|
| 668 |
|
| 669 |
-
|
| 670 |
.site-card h3 {
|
| 671 |
font-size: 20px;
|
| 672 |
font-weight: 600;
|
|
@@ -678,6 +680,10 @@ index_page_template = """
|
|
| 678 |
font-size: 14px;
|
| 679 |
color: var(--secondary-text-color);
|
| 680 |
margin-bottom: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
}
|
| 682 |
|
| 683 |
.site-card .actions {
|
|
@@ -696,6 +702,10 @@ index_page_template = """
|
|
| 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 {
|
|
@@ -739,6 +749,7 @@ index_page_template = """
|
|
| 739 |
font-size: 15px;
|
| 740 |
font-weight: 500;
|
| 741 |
text-align: center;
|
|
|
|
| 742 |
}
|
| 743 |
|
| 744 |
.flash-messages.success {
|
|
@@ -759,7 +770,6 @@ index_page_template = """
|
|
| 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);
|
|
@@ -806,7 +816,7 @@ index_page_template = """
|
|
| 806 |
margin-bottom: 20px;
|
| 807 |
}
|
| 808 |
.site-cards-grid {
|
| 809 |
-
grid-template-columns: 1fr;
|
| 810 |
}
|
| 811 |
.site-card {
|
| 812 |
padding: 18px;
|
|
@@ -833,7 +843,7 @@ index_page_template = """
|
|
| 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="Например: создай
|
| 837 |
</div>
|
| 838 |
|
| 839 |
<button type="submit" id="generate-button">Создать сайт</button>
|
|
@@ -859,10 +869,10 @@ index_page_template = """
|
|
| 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"
|
| 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('Вы уверены, что хотите удалить этот сайт?');"
|
| 866 |
</form>
|
| 867 |
</div>
|
| 868 |
</div>
|
|
@@ -922,7 +932,6 @@ index_page_template = """
|
|
| 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);
|
|
@@ -972,13 +981,11 @@ index_page_template = """
|
|
| 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">
|
|
@@ -986,198 +993,328 @@ dynamic_site_template = """
|
|
| 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 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 997 |
.section-content:last-of-type { border-bottom: none; }
|
| 998 |
-
.section-content h2 { color:
|
| 999 |
-
.
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
.
|
| 1003 |
-
.
|
| 1004 |
-
.
|
| 1005 |
-
.
|
| 1006 |
-
.
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1010 |
@media (max-width: 768px) {
|
| 1011 |
-
|
| 1012 |
-
header
|
| 1013 |
-
|
| 1014 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 1034 |
{% for item in site_data.data_items %}
|
| 1035 |
-
|
|
|
|
| 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.
|
| 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 == '
|
| 1048 |
-
<div class="
|
| 1049 |
-
{%
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1053 |
</div>
|
| 1054 |
-
{% elif section.type == '
|
| 1055 |
-
<div class="
|
| 1056 |
-
{%
|
| 1057 |
-
{%
|
| 1058 |
-
<div class="
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
| 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:
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import uuid
|
| 3 |
import json
|
|
|
|
| 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",
|
|
|
|
| 74 |
force_download=True,
|
| 75 |
resume_download=False
|
| 76 |
)
|
| 77 |
+
logging.info(f"Файл {file_name} успешно скачан.")
|
| 78 |
success = True
|
| 79 |
break
|
| 80 |
except RepositoryNotFoundError:
|
|
|
|
| 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:
|
|
|
|
| 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)
|
|
|
|
| 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:
|
|
|
|
| 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:
|
|
|
|
| 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 |
|
|
|
|
| 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"
|
|
|
|
| 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. Пожалуйста, попробуйте еще раз или измените запрос.")
|
|
|
|
| 352 |
else:
|
| 353 |
raise ValueError(f"Ошибка при генерации данных сайта: {e}")
|
| 354 |
|
|
|
|
|
|
|
|
|
|
| 355 |
index_page_template = """
|
| 356 |
<!DOCTYPE html>
|
| 357 |
<html lang="ru">
|
|
|
|
| 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 |
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;
|
|
|
|
| 669 |
}
|
| 670 |
}
|
| 671 |
|
|
|
|
| 672 |
.site-card h3 {
|
| 673 |
font-size: 20px;
|
| 674 |
font-weight: 600;
|
|
|
|
| 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 {
|
|
|
|
| 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 {
|
|
|
|
| 749 |
font-size: 15px;
|
| 750 |
font-weight: 500;
|
| 751 |
text-align: center;
|
| 752 |
+
margin-bottom: 25px;
|
| 753 |
}
|
| 754 |
|
| 755 |
.flash-messages.success {
|
|
|
|
| 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);
|
|
|
|
| 816 |
margin-bottom: 20px;
|
| 817 |
}
|
| 818 |
.site-cards-grid {
|
| 819 |
+
grid-template-columns: 1fr;
|
| 820 |
}
|
| 821 |
.site-card {
|
| 822 |
padding: 18px;
|
|
|
|
| 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>
|
|
|
|
| 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>
|
|
|
|
| 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);
|
|
|
|
| 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">
|
|
|
|
| 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:
|