Kgshop commited on
Commit
9917d4a
·
verified ·
1 Parent(s): 1798d58

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +626 -373
app.py CHANGED
@@ -1,452 +1,705 @@
1
  import os
2
  import uuid
3
- import json
4
- import logging
5
- import threading
6
- import time
7
  import subprocess
8
- import sys
 
 
9
  import atexit
10
  from datetime import datetime
11
- from flask import Flask, request, jsonify, Response, redirect, url_for, flash
12
- from werkzeug.utils import secure_filename
13
- from dotenv import load_dotenv
14
  import google.generativeai as genai
15
  from huggingface_hub import HfApi, hf_hub_download
16
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
17
- import requests
18
-
19
- # --- Load Environment Variables ---
20
- load_dotenv()
21
 
22
- # --- Configuration ---
23
  app = Flask(__name__)
24
- app.secret_key = 'super-secret-key-for-generator-app-12345'
25
 
26
- # Google AI Configuration
27
- API_KEY_INTERNAL = os.getenv("GEMINI_API_KEY", "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns")
 
28
 
29
- # Hugging Face Configuration
30
- REPO_ID = "Kgshop/testsynk"
31
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
32
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
33
 
34
- # Local Directories and Files
35
- GENERATED_SITES_DIR = 'generated_sites'
36
- DATA_FILE = 'generator_data.json' # State file for the generator app
37
- SYNC_FILES = [DATA_FILE]
38
 
39
- # Runtime State
40
- running_sites = {} # { 'site_id': {'process': Popen_object, 'port': int, 'prompt': str} }
41
- sites_data = {'sites': {}} # This will be loaded from DATA_FILE
42
- next_available_port = 7861 # Main app runs on 7860
43
 
44
- # Logging Configuration
45
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
46
 
47
- # --- Hugging Face Backup & Restore Functions (Adapted from Code 2) ---
 
 
 
 
 
 
 
 
 
48
 
49
- def download_db_from_hf(specific_file=None, retries=3, delay=5):
50
- if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
51
- logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
52
- token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
53
- files_to_download = [specific_file] if specific_file else SYNC_FILES
54
- logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
55
- all_successful = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- for file_name in files_to_download:
58
- success = False
59
- for attempt in range(retries + 1):
60
- try:
61
- logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
62
- hf_hub_download(
63
- repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use,
64
- local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False
65
- )
66
- logging.info(f"Successfully downloaded {file_name}.")
67
- success = True
68
- break
69
- except RepositoryNotFoundError:
70
- logging.error(f"Repository {REPO_ID} not found. Download cancelled.")
71
- return False
72
- except HfHubHTTPError as e:
73
- if e.response.status_code == 404:
74
- logging.warning(f"File {file_name} not found in repo {REPO_ID}. Creating empty local file.")
75
- if file_name == DATA_FILE:
76
- with open(file_name, 'w', encoding='utf-8') as f:
77
- json.dump({'sites': {}}, f)
78
- success = True # We consider this a success as we handled it.
79
- break
80
- else:
81
- logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
82
- except Exception as e:
83
- logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...", exc_info=True)
84
- if attempt < retries:
85
- time.sleep(delay)
86
- if not success:
87
- logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
88
- all_successful = False
89
- return all_successful
90
-
91
- def upload_db_to_hf(specific_file=None):
92
  if not HF_TOKEN_WRITE:
93
- logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
94
- return
 
 
 
95
  try:
96
  api = HfApi()
97
- files_to_upload = [specific_file] if specific_file else SYNC_FILES
98
- logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
99
- for file_name in files_to_upload:
100
- if os.path.exists(file_name):
101
- api.upload_file(
102
- path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID,
103
- repo_type="dataset", token=HF_TOKEN_WRITE,
104
- commit_message=f"Sync generator state {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
105
- )
106
- logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
107
- else:
108
- logging.warning(f"File {file_name} not found locally, skipping upload.")
109
  except Exception as e:
110
- logging.error(f"Error during Hugging Face upload: {e}", exc_info=True)
 
111
 
112
- def periodic_backup():
 
 
113
  while True:
114
- time.sleep(1800) # 30 minutes
115
- logging.info("Starting periodic backup...")
116
- upload_db_to_hf()
117
-
118
- def load_data():
119
- global sites_data
120
- default_data = {'sites': {}}
121
- try:
122
- if not os.path.exists(DATA_FILE):
123
- raise FileNotFoundError
124
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
125
- data = json.load(file)
126
- if 'sites' not in data: data['sites'] = {}
127
- sites_data = data
128
- logging.info(f"Local state loaded successfully from {DATA_FILE}")
129
- except (FileNotFoundError, json.JSONDecodeError):
130
- logging.warning(f"Local file {DATA_FILE} not found or corrupt. Attempting download from HF.")
131
- if download_db_from_hf(specific_file=DATA_FILE):
132
  try:
133
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
134
- sites_data = json.load(file)
135
- if 'sites' not in sites_data: sites_data['sites'] = {}
136
- logging.info(f"State loaded from {DATA_FILE} after download.")
137
- except (FileNotFoundError, json.JSONDecodeError):
138
- logging.error(f"Failed to read file after download. Using default state.")
139
- sites_data = default_data
 
 
 
 
 
 
 
 
 
 
 
 
140
  else:
141
- logging.error(f"Failed to download state from HF. Using empty default state.")
142
- sites_data = default_data
 
143
 
144
- def save_data():
 
 
 
 
 
 
 
 
145
  try:
146
- with open(DATA_FILE, 'w', encoding='utf-8') as file:
147
- json.dump(sites_data, file, ensure_ascii=False, indent=4)
148
- logging.info(f"Generator state saved to {DATA_FILE}")
149
- # Upload to HF after every save for real-time backup
150
- upload_db_to_hf(specific_file=DATA_FILE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  except Exception as e:
152
- logging.error(f"Error saving generator state: {e}", exc_info=True)
 
 
 
153
 
154
- # --- Core AI Generation Logic (Modified for Flask Apps) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
- def generate_flask_app_code_from_prompt(user_prompt):
 
 
 
 
157
  try:
 
158
  genai.configure(api_key=API_KEY_INTERNAL)
159
  except Exception as e:
160
- raise ValueError(f"Failed to configure Google AI: {e}")
 
161
 
162
  if not user_prompt or not user_prompt.strip():
163
- raise ValueError("Prompt cannot be empty.")
164
 
165
  system_instruction = (
166
- "You are an expert Python Flask web developer. Your task is to generate a complete, single-file Flask application "
167
- "based on the user's request. The entire application must be in ONE SINGLE Python (.py) file.\n"
168
- "Crucial requirements for the generated file:\n"
169
- "1. **Single File:** Everything must be in one Python file. No external templates, CSS, or JS files.\n"
170
- "2. **HTML as Strings:** All HTML/CSS/JS for templates must be stored in multiline Python string variables (e.g., `HOME_PAGE_TEMPLATE = '''<!DOCTYPE html>...'''`).\n"
171
- "3. **Render with `render_template_string`:** Use `from flask import render_template_string` to render the HTML string variables.\n"
172
- "4. **Command-Line Port:** The application MUST accept the port number as a command-line argument. This is NOT optional. The main script will launch it like `python generated_app.py 8001`. "
173
- " Use this exact structure at the end of the file:\n"
 
174
  " ```python\n"
175
- " import sys\n"
176
  " if __name__ == '__main__':\n"
177
- " # Default port is 5000, but it will be overridden by the command line argument.\n"
178
- " port = int(sys.argv[1]) if len(sys.argv) > 1 else 5000\n"
179
- " app.run(host='0.0.0.0', port=port, debug=False)\n"
180
  " ```\n"
181
- "5. **Database:** For any data storage, use a simple in-memory Python list or dictionary defined at the top of the script. Do not use file-based databases like SQLite or JSON files.\n"
182
- "6. **Dependencies:** Only use Flask and standard Python libraries. Do not import libraries that are not commonly available, like pandas or numpy, unless absolutely necessary for the request.\n"
183
- "7. **Output Format:** Directly output ONLY the Python code. Do not include any explanatory text, markdown formatting (like ```python), or anything else before `import os` or after the `app.run(...)` call."
 
 
 
184
  )
185
 
186
  full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
187
 
 
188
  try:
189
- model = genai.GenerativeModel('gemini-1.5-flash-latest') # Using a more recent model
190
- response = model.generate_content(full_prompt)
 
 
 
 
 
 
 
 
191
 
192
- generated_text = response.text.strip()
193
-
194
- if not generated_text:
195
- if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
196
- reason = response.prompt_feedback.block_reason
197
- raise ValueError(f"Content generation blocked due to safety policy (reason: {reason}). Please try a different prompt.")
 
198
  else:
199
- raise ValueError("The model returned an empty result. Please try rephrasing your prompt.")
200
-
201
- # Clean up potential markdown formatting
202
- if generated_text.startswith("```python"):
203
- generated_text = generated_text[10:]
204
- if generated_text.endswith("```"):
205
- generated_text = generated_text[:-3]
206
-
207
- # Basic validation
208
- if "from flask import Flask" not in generated_text or "app.run" not in generated_text:
209
- raise ValueError("The generated code does not appear to be a valid Flask application.")
210
-
211
- return generated_text.strip()
 
 
 
 
 
 
 
212
 
213
  except Exception as e:
214
  logging.error(f"Error generating content with GenAI: {e}", exc_info=True)
215
- raise ValueError(f"An error occurred while generating the site code: {e}")
216
-
217
-
218
- # --- Process and Port Management ---
219
-
220
- def find_free_port():
221
- global next_available_port
222
- # In a real app, you'd check if the port is actually free.
223
- # For this demo, we'll just increment.
224
- port = next_available_port
225
- next_available_port += 1
226
- return port
227
-
228
- def cleanup():
229
- logging.info("Shutting down generator... terminating all running site processes.")
230
- for site_id, site_info in list(running_sites.items()):
231
- try:
232
- logging.info(f"Terminating site {site_id} (PID: {site_info['process'].pid})")
233
- site_info['process'].terminate()
234
- site_info['process'].wait(timeout=5) # Wait for graceful termination
235
- except subprocess.TimeoutExpired:
236
- logging.warning(f"Process for site {site_id} did not terminate gracefully, killing.")
237
- site_info['process'].kill()
238
- except Exception as e:
239
- logging.error(f"Error terminating process for site {site_id}: {e}")
240
- logging.info("Cleanup complete.")
241
-
242
- atexit.register(cleanup)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
- # --- Flask Routes ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
- # The main UI from Code 1, slightly adapted
247
- @app.route('/')
248
- def index():
249
- # This is the HTML for the generator's frontend
250
- html_template = """
251
- <!DOCTYPE html>
252
- <html lang="ru">
253
- <head>
254
- <meta charset="UTF-8">
255
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
256
- <title>EVA - Генератор Python/Flask Сайтов</title>
257
- <style>
258
  :root {
259
- --system-gray-100-light: #f2f2f7; --system-gray-75-light: #f8f8fa; --system-gray-50-light: #ffffff; --system-gray-dark-100-light: #000000; --system-gray-dark-75-light: #1c1c1e; --system-gray-dark-50-light: #3a3a3c; --system-gray-light-75-light: #8e8e93; --system-gray-light-50-light: #aeaeb2; --system-blue-light: #007aff; --system-blue-light-hover: #005ecf; --system-red-light: #ff3b30; --system-separator-light: rgba(60, 60, 67, 0.29); --system-separator-opaque-light: #d1d1d6;
260
- --system-gray-100-dark: #1c1c1e; --system-gray-75-dark: #2c2c2e; --system-gray-50-dark: #000000; --system-gray-dark-100-dark: #ffffff; --system-gray-dark-75-dark: #f2f2f7; --system-gray-dark-50-dark: #e5e5ea; --system-gray-light-75-dark: #8e8e93; --system-gray-light-50-dark: #636366; --system-blue-dark: #0a84ff; --system-blue-dark-hover: #3b9eff; --system-red-dark: #ff453a; --system-separator-dark: rgba(84, 84, 88, 0.65); --system-separator-opaque-dark: #38383a;
 
 
 
 
 
 
 
 
 
261
  }
262
- @media (prefers-color-scheme: dark) { :root { --bg-color: var(--system-gray-50-dark); --content-bg: var(--system-gray-100-dark); --text-color: var(--system-gray-dark-100-dark); --secondary-text-color: var(--system-gray-light-75-dark); --tertiary-text-color: var(--system-gray-light-50-dark); --border-color: var(--system-separator-dark); --border-color-opaque: var(--system-separator-opaque-dark); --input-bg: var(--system-gray-75-dark); --primary-color: var(--system-blue-dark); --primary-color-hover: var(--system-blue-dark-hover); --error-color: var(--system-red-dark); } }
263
- @media (prefers-color-scheme: light) { :root { --bg-color: var(--system-gray-100-light); --content-bg: var(--system-gray-50-light); --text-color: var(--system-gray-dark-100-light); --secondary-text-color: var(--system-gray-light-75-light); --tertiary-text-color: var(--system-gray-light-50-light); --border-color: var(--system-separator-light); --border-color-opaque: var(--system-separator-opaque-light); --input-bg: var(--system-gray-75-light); --primary-color: var(--system-blue-light); --primary-color-hover: var(--system-blue-light-hover); --error-color: var(--system-red-light); } }
264
- html { height: -webkit-fill-available; }
265
- body { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom); background-color: var(--bg-color); color: var(--text-color); display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; min-height: -webkit-fill-available; line-height: 1.45; }
266
- .container { background-color: var(--content-bg); padding: 25px 30px 30px 30px; border-radius: 24px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05); max-width: 580px; width: calc(100% - 40px); box-sizing: border-box; margin-top: 30px; }
267
- h1 { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 8px; color: var(--text-color); letter-spacing: -0.5px; }
268
- p.subtitle { font-size: 17px; color: var(--secondary-text-color); text-align: center; margin-bottom: 35px; font-weight: 400; }
269
- .form-group { margin-bottom: 28px; }
270
- label.input-label { display: block; font-weight: 500; margin-bottom: 10px; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
271
- textarea#prompt-input { width: 100%; padding: 14px 18px; border: 1px solid var(--border-color-opaque); border-radius: 12px; font-size: 16px; background-color: var(--input-bg); color: var(--text-color); box-sizing: border-box; transition: border-color 0.2s ease, box-shadow 0.2s ease; font-family: inherit; resize: vertical; min-height: 120px; }
272
- textarea#prompt-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent); outline: none; }
273
- button#generate-button { width: 100%; padding: 16px; background-color: var(--primary-color); color: white; border: none; border-radius: 12px; font-size: 17px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; margin-top: 15px; }
274
- button#generate-button:hover { background-color: var(--primary-color-hover); }
275
- button#generate-button:active { transform: scale(0.98); }
276
- button#generate-button:disabled { background-color: var(--tertiary-text-color); cursor: not-allowed; }
277
- .output-section { margin-top: 35px; } .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } label#output-label { font-weight: 500; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
278
- button#copy-button { background-color: transparent; border: none; color: var(--primary-color); font-size: 14px; font-weight: 500; cursor: pointer; padding: 5px 8px; border-radius: 6px; transition: background-color 0.2s ease, color 0.2s ease; display: none; } button#copy-button:hover { background-color: color-mix(in srgb, var(--primary-color) 15%, transparent); } button#copy-button:active { background-color: color-mix(in srgb, var(--primary-color) 25%, transparent); } button#copy-button.copied { color: #34c759; }
279
- #output-container { background-color: var(--input-bg); padding: 18px 20px; border-radius: 12px; min-height: 60px; border: 1px solid var(--border-color); word-wrap: break-word; font-size: 15px; color: var(--text-color); line-height: 1.5; transition: border-color 0.2s ease, background-color 0.2s ease; display: flex; align-items: center; justify-content: center; } #output-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; } #output-container a:hover { text-decoration: underline; }
280
- #output-container.loading::before { content: "Генерация и запуск сайта..."; display: block; text-align: center; font-style: italic; color: var(--secondary-text-color); animation: fadePulse 1.8s infinite ease-in-out; }
281
- #output-container.error { color: var(--error-color); font-weight: 500; border-color: color-mix(in srgb, var(--error-color) 50%, transparent); background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg)); justify-content: flex-start; }
282
- @keyframes fadePulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
283
- </style>
284
- </head>
285
- <body>
286
- <div class="container">
287
- <h1>EVA</h1>
288
- <p class="subtitle">Генератор Python/Flask сайтов</p>
289
- <form id="generate-form">
290
- <div class="form-group">
291
- <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать</label>
292
- <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай сайт-визитку для фотографа. На главной странице должны быть галерея работ (3-4 фото-заглушки), раздел 'Обо мне' и контактная форма."></textarea>
293
- </div>
294
- <button type="submit" id="generate-button">Создать и запустить сайт</button>
295
- </form>
296
- <div class="output-section">
297
- <div class="output-header">
298
- <label id="output-label">Ссылка на запущенный сайт</label>
299
- <button id="copy-button">Копировать</button>
300
- </div>
301
- <div id="output-container" aria-live="polite"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  </div>
 
 
 
 
 
 
 
 
 
303
  </div>
304
- <script>
305
- const form = document.getElementById('generate-form');
306
- const outputContainer = document.getElementById('output-container');
307
- const generateButton = document.getElementById('generate-button');
308
- const copyButton = document.getElementById('copy-button');
309
-
310
- form.addEventListener('submit', async (event) => {
311
- event.preventDefault();
312
- const formData = new FormData(form);
313
- if (!formData.get('prompt').trim()) {
314
- showError("Пожалуйста, опишите сайт."); return;
315
- }
316
- generateButton.disabled = true; generateButton.textContent = 'Генерация...';
317
- outputContainer.innerHTML = ''; outputContainer.classList.add('loading');
318
- outputContainer.classList.remove('error'); copyButton.style.display = 'none';
319
-
320
- try {
321
- const response = await fetch('/generate', { method: 'POST', body: formData });
322
- const result = await response.json();
323
- if (!response.ok) { throw new Error(result.error || `Ошибка сервера: ${response.status}`); }
324
-
325
- if (result.site_url) {
326
- const link = document.createElement('a');
327
- link.href = result.site_url;
328
- link.textContent = result.site_url;
329
- link.target = "_blank";
330
- outputContainer.innerHTML = '';
331
- outputContainer.appendChild(link);
332
- copyButton.style.display = 'block';
333
- copyButton.dataset.copyText = result.site_url;
334
- } else {
335
- showError(result.error || "Не удалось получить URL сайта.");
336
- }
337
- } catch (error) {
338
- console.error("Fetch Error:", error);
339
- showError(`Ошибка: ${error.message}`);
340
- } finally {
341
- generateButton.disabled = false; generateButton.textContent = 'Создать и запустить сайт';
342
- outputContainer.classList.remove('loading');
343
  }
344
- });
345
 
346
- copyButton.addEventListener('click', () => {
347
- navigator.clipboard.writeText(copyButton.dataset.copyText).then(() => {
348
- copyButton.textContent = 'Скопировано!';
349
- setTimeout(() => { copyButton.textContent = 'Копировать'; }, 1500);
350
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
- function showError(message) {
354
- outputContainer.innerHTML = `<span>${message}</span>`;
355
- outputContainer.classList.add('error');
356
- outputContainer.classList.remove('loading');
357
- copyButton.style.display = 'none';
358
- }
359
- </script>
360
- </body>
361
- </html>
362
- """
363
  return Response(html_template, mimetype='text/html')
364
 
365
  @app.route('/generate', methods=['POST'])
366
  def handle_generate():
367
- user_prompt = request.form.get('prompt')
 
 
 
368
  if not user_prompt or not user_prompt.strip():
369
- return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
370
 
371
  try:
372
- # 1. Generate the Python code for the Flask app
373
- flask_app_code = generate_flask_app_code_from_prompt(user_prompt)
374
 
375
- # 2. Save the generated code to a .py file
376
- site_id = str(uuid.uuid4())
377
- filename = f"{site_id}.py"
378
- filepath = os.path.join(GENERATED_SITES_DIR, filename)
 
 
379
 
380
- with open(filepath, "w", encoding="utf-8") as f:
381
- f.write(flask_app_code)
 
382
 
383
- # 3. Find a free port and launch the new Flask app in a separate process
384
- port = find_free_port()
385
-
386
- # Pass the HF token to the child process environment if needed
387
- env = os.environ.copy()
388
- if HF_TOKEN_WRITE:
389
- env["HF_TOKEN"] = HF_TOKEN_WRITE
390
 
391
- process = subprocess.Popen(
392
- [sys.executable, filepath, str(port)],
393
- stdout=subprocess.PIPE,
394
- stderr=subprocess.PIPE,
395
- env=env
396
- )
397
-
398
- # Give it a moment to start up or fail
399
- time.sleep(3)
400
-
401
- # Check if the process has already exited with an error
402
- if process.poll() is not None:
403
- stderr_output = process.stderr.read().decode('utf-8', errors='ignore')
404
- raise RuntimeError(f"Generated app failed to start. Error:\n{stderr_output}")
405
-
406
- # 4. Store the running process and update state
407
- running_sites[site_id] = {
408
- 'process': process,
409
- 'port': port,
410
- 'prompt': user_prompt,
411
- 'id': site_id
412
- }
413
-
414
- # 5. Persist the state
415
- sites_data['sites'][site_id] = {'port': port, 'prompt': user_prompt, 'id': site_id}
416
- save_data()
417
-
418
- # 6. Return the URL to the user
419
- host = request.host.split(':')[0]
420
- site_url = f"http://{host}:{port}"
421
-
422
- logging.info(f"Successfully generated and launched site {site_id} on port {port}")
423
- return jsonify({"site_url": site_url})
424
 
425
- except (ValueError, RuntimeError) as e:
426
- logging.error(f"Error during generation or launch: {e}", exc_info=True)
427
- return jsonify({"error": str(e)}), 400
428
- except Exception as e:
429
- logging.error(f"Unexpected server error: {e}", exc_info=True)
430
- return jsonify({"error": f"An unexpected internal error occurred: {e}"}), 500
431
 
432
- # --- Main Application Start ---
433
- if __name__ == '__main__':
434
- if not os.path.exists(GENERATED_SITES_DIR):
435
- os.makedirs(GENERATED_SITES_DIR)
 
 
436
 
437
- logging.info("Application starting up...")
 
 
 
438
 
439
- # Attempt to load state from HF on startup
440
- load_data()
441
- # Note: We are not auto-relaunching sites on startup to keep it simple,
442
- # but the state is preserved in generator_data.json. An admin panel could show them.
443
-
444
- if HF_TOKEN_WRITE:
445
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
446
- backup_thread.start()
447
- logging.info("Periodic state backup thread started.")
 
 
 
 
 
 
 
 
 
 
 
448
  else:
449
- logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
 
 
 
450
 
451
- logging.info("Starting main generator Flask app on host 0.0.0.0 and port 7860")
452
- app.run(host='0.0.0.0', port=7860, debug=False)
 
 
 
 
 
 
 
 
 
1
  import os
2
  import uuid
 
 
 
 
3
  import subprocess
4
+ import time
5
+ import socket
6
+ import logging
7
  import atexit
8
  from datetime import datetime
9
+ from flask import Flask, request, jsonify, Response, send_from_directory, 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
+ import requests # Нужен для HfHubHTTPError
 
 
 
14
 
15
+ # --- Конфигурация ---
16
  app = Flask(__name__)
 
17
 
18
+ # ВНИМАНИЕ: Жестко закодированный API ключ - это небезопасно. Используйте переменные окружения.
19
+ API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns" # Используется ключ из Кода 1
20
+ SITES_CACHE_DIR = 'generated_sites_cache' # Локальный кэш для .py файлов сайтов
21
 
22
+ # Конфигурация Hugging Face (адаптировано из Кода 2)
23
+ REPO_ID = "Kgshop/testsynk" # Указанный репозиторий
24
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Токен с правом записи
25
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE) # Токен для чтения (или тот же, что и для записи)
26
 
27
+ DOWNLOAD_RETRIES = 3
28
+ DOWNLOAD_DELAY = 5
 
 
29
 
30
+ # Управление запущенными сайтами
31
+ RUNNING_SITES = {} # {'site_filename.py': {'process': Popen_object, 'port': 5001, 'host': '127.0.0.1'}}
32
+ BASE_PORT_FOR_GENERATED_SITES = 8001
33
+ USED_PORTS = set()
34
 
35
+ # --- Настройка Логирования ---
36
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
37
 
38
+ # --- Создание директорий ---
39
+ if not os.path.exists(SITES_CACHE_DIR):
40
+ os.makedirs(SITES_CACHE_DIR)
41
+
42
+ # --- Функции для работы с Hugging Face ---
43
+ def download_file_from_hf(repo_filename, local_save_path, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
44
+ if not HF_TOKEN_READ:
45
+ logging.warning("HF_TOKEN_READ not set. Download might fail for private repos.")
46
+
47
+ token_to_use = HF_TOKEN_READ
48
 
49
+ logging.info(f"Attempting download for {repo_filename} from {REPO_ID} to {local_save_path}...")
50
+ for attempt in range(retries + 1):
51
+ try:
52
+ logging.info(f"Downloading {repo_filename} (Attempt {attempt + 1}/{retries + 1})...")
53
+ downloaded_path = hf_hub_download(
54
+ repo_id=REPO_ID,
55
+ filename=repo_filename,
56
+ repo_type="dataset", # Обычно 'dataset' для файлов, или 'model'
57
+ token=token_to_use,
58
+ local_dir=os.path.dirname(local_save_path),
59
+ local_dir_use_symlinks=False,
60
+ force_download=True, # Всегда скачивать свежую версию
61
+ resume_download=False,
62
+ # Важно указать имя файла, если local_dir указан, иначе он создаст структуру папок репозитория
63
+ # hf_hub_download вернет полный путь к файлу, который может включать структуру папок репозитория.
64
+ # Мы должны убедиться, что он сохраняется как local_save_path
65
+ )
66
+ # Переименовываем/перемещаем, если hf_hub_download скачал не точно по пути
67
+ if downloaded_path != local_save_path:
68
+ os.makedirs(os.path.dirname(local_save_path), exist_ok=True)
69
+ os.replace(downloaded_path, local_save_path) # Используем replace для атомарности
70
+ logging.info(f"File moved from {downloaded_path} to {local_save_path}")
71
+
72
+ logging.info(f"Successfully downloaded {repo_filename} to {local_save_path}.")
73
+ return True
74
+ except RepositoryNotFoundError:
75
+ logging.error(f"Repository {REPO_ID} not found. Download cancelled.")
76
+ return False
77
+ except HfHubHTTPError as e:
78
+ if e.response.status_code == 404:
79
+ logging.warning(f"File {repo_filename} not found in repo {REPO_ID} (404).")
80
+ return False # Файл не найден, нет смысла ретраить
81
+ logging.error(f"HTTP error downloading {repo_filename} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
82
+ except requests.exceptions.RequestException as e: # requests используется внутри huggingface_hub
83
+ logging.error(f"Network error downloading {repo_filename} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
84
+ except Exception as e:
85
+ logging.error(f"Unexpected error downloading {repo_filename} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
86
+
87
+ if attempt < retries:
88
+ time.sleep(delay)
89
+
90
+ logging.error(f"Failed to download {repo_filename} after {retries + 1} attempts.")
91
+ return False
92
 
93
+ def upload_file_to_hf(local_filepath, path_in_repo):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  if not HF_TOKEN_WRITE:
95
+ logging.warning("HF_TOKEN_WRITE (for writing) not set. Skipping upload to Hugging Face.")
96
+ return False
97
+ if not os.path.exists(local_filepath):
98
+ logging.error(f"Local file {local_filepath} not found. Skipping upload.")
99
+ return False
100
  try:
101
  api = HfApi()
102
+ logging.info(f"Starting upload of {local_filepath} to {path_in_repo} in HF repo {REPO_ID}...")
103
+ api.upload_file(
104
+ path_or_fileobj=local_filepath,
105
+ path_in_repo=path_in_repo,
106
+ repo_id=REPO_ID,
107
+ repo_type="dataset",
108
+ token=HF_TOKEN_WRITE,
109
+ commit_message=f"Sync {path_in_repo} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
110
+ )
111
+ logging.info(f"File {local_filepath} successfully uploaded to {path_in_repo} in Hugging Face.")
112
+ return True
 
113
  except Exception as e:
114
+ logging.error(f"Error uploading file {local_filepath} to Hugging Face: {e}", exc_info=True)
115
+ return False
116
 
117
+ # --- Функции управления дочерними сайтами ---
118
+ def find_free_port(start_port):
119
+ port = start_port
120
  while True:
121
+ if port in USED_PORTS:
122
+ port += 1
123
+ continue
124
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  try:
126
+ s.bind(('127.0.0.1', port))
127
+ USED_PORTS.add(port)
128
+ return port
129
+ except OSError:
130
+ logging.warning(f"Port {port} is already in use by another application, trying next.")
131
+ # Добавляем в USED_PORTS, чтобы не пробовать его снова в этой сессии для наших сайтов
132
+ USED_PORTS.add(port)
133
+ port += 1
134
+ if port > 65535:
135
+ raise RuntimeError("Could not find a free port.")
136
+
137
+
138
+ def start_generated_site(py_filename):
139
+ if py_filename in RUNNING_SITES:
140
+ site_info = RUNNING_SITES[py_filename]
141
+ # Проверить, жив ли процесс
142
+ if site_info['process'].poll() is None: # None значит, что процесс еще работает
143
+ logging.info(f"Site {py_filename} is already running on port {site_info['port']}.")
144
+ return site_info['host'], site_info['port']
145
  else:
146
+ logging.warning(f"Site {py_filename} was marked as running but process died. Cleaning up.")
147
+ USED_PORTS.discard(site_info['port'])
148
+ del RUNNING_SITES[py_filename]
149
 
150
+ local_py_path = os.path.join(SITES_CACHE_DIR, py_filename)
151
+
152
+ # 1. Скачать/убедиться в наличии файла
153
+ if not os.path.exists(local_py_path):
154
+ if not download_file_from_hf(f"sites/{py_filename}", local_py_path):
155
+ logging.error(f"Failed to download site {py_filename} from Hugging Face.")
156
+ return None, None
157
+
158
+ # 2. Найти свободный порт и модифицировать код сайта
159
  try:
160
+ assigned_port = find_free_port(BASE_PORT_FOR_GENERATED_SITES)
161
+ with open(local_py_path, 'r', encoding='utf-8') as f:
162
+ original_code = f.read()
163
+
164
+ # Заменить плейсхолдер порта
165
+ modified_code = original_code.replace("{{GENERATED_APP_PORT}}", str(assigned_port))
166
+
167
+ # Убедимся, что код пытается запуститься на 0.0.0.0 или 127.0.0.1
168
+ # Это очень грубая проверка, лучше если AI генерирует предсказуемый app.run
169
+ if f"app.run(host='0.0.0.0', port={assigned_port})" not in modified_code and \
170
+ f"app.run(host='127.0.0.1', port={assigned_port})" not in modified_code and \
171
+ f"app.run(port={assigned_port}, host='0.0.0.0')" not in modified_code and \
172
+ f"app.run(port={assigned_port}, host='127.0.0.1')" not in modified_code:
173
+
174
+ # Попробуем заменить более общий app.run(port=XXXX) или app.run()
175
+ import re
176
+ modified_code = re.sub(
177
+ r"app\.run\((.*)\)",
178
+ f"app.run(host='0.0.0.0', port={assigned_port})",
179
+ modified_code,
180
+ flags=re.DOTALL
181
+ )
182
+ if "app.run(" not in modified_code : # Если app.run не было вообще
183
+ modified_code += f"\n\nif __name__ == '__main__':\n app.run(host='0.0.0.0', port={assigned_port})\n"
184
+
185
+
186
+ with open(local_py_path, 'w', encoding='utf-8') as f:
187
+ f.write(modified_code)
188
+
189
  except Exception as e:
190
+ logging.error(f"Error preparing site {py_filename} for launch: {e}", exc_info=True)
191
+ if 'assigned_port' in locals() and assigned_port:
192
+ USED_PORTS.discard(assigned_port)
193
+ return None, None
194
 
195
+ # 3. Запустить скрипт
196
+ try:
197
+ # Для Windows, если python не в PATH, может понадобиться sys.executable
198
+ # python_executable = sys.executable
199
+ python_executable = "python" # Предполагаем, что python в PATH
200
+ process = subprocess.Popen(
201
+ [python_executable, local_py_path],
202
+ stdout=subprocess.PIPE, # Можно перенаправить в лог-файлы
203
+ stderr=subprocess.PIPE
204
+ )
205
+ time.sleep(2) # Дать время Flask-приложению запуститься
206
+
207
+ if process.poll() is not None: # Если процесс сразу завершился
208
+ stdout, stderr = process.communicate()
209
+ logging.error(f"Site {py_filename} failed to start. Return code: {process.returncode}")
210
+ logging.error(f"Stdout: {stdout.decode(errors='ignore')}")
211
+ logging.error(f"Stderr: {stderr.decode(errors='ignore')}")
212
+ USED_PORTS.discard(assigned_port)
213
+ return None, None
214
+
215
+ site_host = '127.0.0.1' # Сгенерированные сайты слушают на 0.0.0.0, но доступны через 127.0.0.1
216
+ RUNNING_SITES[py_filename] = {'process': process, 'port': assigned_port, 'host': site_host}
217
+ logging.info(f"Site {py_filename} started successfully on http://{site_host}:{assigned_port}")
218
+ return site_host, assigned_port
219
+ except Exception as e:
220
+ logging.error(f"Error launching site {py_filename}: {e}", exc_info=True)
221
+ if 'assigned_port' in locals() and assigned_port:
222
+ USED_PORTS.discard(assigned_port)
223
+ return None, None
224
+
225
+ def cleanup_running_sites():
226
+ logging.info("Cleaning up running generated sites...")
227
+ for filename, site_info in list(RUNNING_SITES.items()): # list() для копии, т.к. будем удалять
228
+ try:
229
+ if site_info['process'].poll() is None: # Если процесс еще жив
230
+ logging.info(f"Terminating site {filename} (PID: {site_info['process'].pid}) on port {site_info['port']}")
231
+ site_info['process'].terminate() # Сначала пробуем мягко
232
+ try:
233
+ site_info['process'].wait(timeout=5) # Ждем завершения
234
+ except subprocess.TimeoutExpired:
235
+ logging.warning(f"Site {filename} did not terminate gracefully, killing.")
236
+ site_info['process'].kill() # Если не помогло - жестко
237
+ USED_PORTS.discard(site_info['port'])
238
+ del RUNNING_SITES[filename]
239
+ except Exception as e:
240
+ logging.error(f"Error terminating site {filename}: {e}", exc_info=True)
241
+ logging.info("Cleanup complete.")
242
 
243
+ atexit.register(cleanup_running_sites)
244
+
245
+
246
+ # --- Функция генерации кода сайта с помощью Google AI ---
247
+ def generate_full_python_site_from_prompt(user_prompt):
248
  try:
249
+ # ВНИМАНИЕ: Ключ API жестко закодирован. В продакшене используйте переменные окружения.
250
  genai.configure(api_key=API_KEY_INTERNAL)
251
  except Exception as e:
252
+ logging.error(f"Error configuring GenAI: {e}", exc_info=True)
253
+ raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
254
 
255
  if not user_prompt or not user_prompt.strip():
256
+ raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
257
 
258
  system_instruction = (
259
+ "You are an expert Python web developer. Your task is to generate a complete, single-file Flask application "
260
+ "based on the user's request. The entire application must be contained in a single .py file.\n"
261
+ "Follow these requirements strictly:\n"
262
+ "1. **Flask Application:** The code must be a runnable Flask application.\n"
263
+ "2. **Single File:** All code, including HTML templates, CSS, and JavaScript, must be in this single Python file.\n"
264
+ "3. **HTML Templates:** Embed HTML as Python strings. Use `flask.render_template_string()`.\n"
265
+ "4. **CSS/JS:** Inline CSS within `<style>` tags in the HTML strings. Inline JavaScript within `<script>` tags in the HTML strings. Do not use external files for CSS/JS.\n"
266
+ "5. **Routes:** Define Flask routes as requested by the user.\n"
267
+ "6. **`app.run`:** Crucially, the script MUST end with a block like this for запуска:\n"
268
  " ```python\n"
 
269
  " if __name__ == '__main__':\n"
270
+ " # IMPORTANT: The port number MUST be the placeholder {{GENERATED_APP_PORT}}\n"
271
+ " # The host should be '0.0.0.0' to be accessible.\n"
272
+ " app.run(host='0.0.0.0', port={{GENERATED_APP_PORT}})\n"
273
  " ```\n"
274
+ "7. **No External File Dependencies:** The script should not rely on any other local files (e.g., for images, data) unless explicitly creating and managing them (e.g. a simple data.json). Images should preferably be placeholder URLs (e.g., from placehold.co) or base64 encoded if small and necessary.\n"
275
+ "8. **Self-Contained Database (Optional):** If the site requires data persistence, it should use a simple mechanism like storing data in Python dictionaries or lists within the script itself. For more complex needs, it could read/write to a single JSON file named `data.json` *in its current working directory if absolutely necessary*, and the script should handle its creation. Avoid complex databases or ORMs.\n"
276
+ "9. **Direct Python Output:** Output ONLY the Python code. Do NOT include ```python, markdown, or any explanatory text before or after the code block. The output must start with `import flask` or similar Python code and end with the `app.run` block.\n"
277
+ "10. **Error Handling & Imports:** Include necessary imports (like `from flask import Flask, render_template_string, request, ...`). Add basic error handling if appropriate for the requested site.\n"
278
+ "11. **Character Encoding:** Ensure any HTML generated uses UTF-8.\n"
279
+ "12. **Simplicity:** Favor simplicity and clarity in the generated code."
280
  )
281
 
282
  full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
283
 
284
+ response_obj = None # Для отладки prompt_feedback
285
  try:
286
+ # Рекомендуется использовать более новые модели для генерации кода, например, Gemini
287
+ model = genai.GenerativeModel('gemini-1.5-flash-latest') # Заменено с learnlm-2.0-flash-experimental
288
+ response_obj = model.generate_content(full_prompt)
289
+
290
+ generated_text = ""
291
+ # Обработка ответа согласно документации Gemini
292
+ if hasattr(response_obj, 'text') and response_obj.text:
293
+ generated_text = response_obj.text
294
+ elif hasattr(response_obj, 'parts') and response_obj.parts:
295
+ generated_text = "".join(part.text for part in response_obj.parts if hasattr(part, 'text'))
296
 
297
+ if not generated_text.strip():
298
+ # Проверка на блокировку контента
299
+ if hasattr(response_obj, 'prompt_feedback') and \
300
+ hasattr(response_obj.prompt_feedback, 'block_reason') and \
301
+ response_obj.prompt_feedback.block_reason:
302
+ reason = response_obj.prompt_feedback.block_reason
303
+ raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другой запрос.")
304
  else:
305
+ raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
306
+
307
+ # Очистка от markdown, если модель его добавила
308
+ clean_text = generated_text.strip()
309
+ if clean_text.startswith("```python"):
310
+ clean_text = clean_text[9:] # Удаляем ```python
311
+ if clean_text.endswith("```"):
312
+ clean_text = clean_text[:-3] # Удаляем ```
313
+ generated_text = clean_text.strip()
314
+ elif clean_text.startswith("```"): # Общий случай для ``` без указания языка
315
+ clean_text = clean_text[3:]
316
+ if clean_text.endswith("```"):
317
+ clean_text = clean_text[:-3]
318
+ generated_text = clean_text.strip()
319
+
320
+ if not (generated_text.startswith("import ") or generated_text.startswith("from ")):
321
+ logging.warning(f"Generated code might not be pure Python. Preview: {generated_text[:200]}")
322
+ # Дополнительная проверка не помешает, но AI должен следовать инструкциям
323
+
324
+ return generated_text
325
 
326
  except Exception as e:
327
  logging.error(f"Error generating content with GenAI: {e}", exc_info=True)
328
+ error_message = str(e)
329
+ # Более детальная обработка ошибок API
330
+ if "API key not valid" in error_message:
331
+ raise ValueError("Внутренняя ошибка конфигурации API Google AI. Проверьте ключ.")
332
+ elif "Billing account not found" in error_message or "billing account" in error_message.lower():
333
+ raise ValueError("Проблема с биллингом аккаунта Google Cloud.")
334
+ elif "Could not find model" in error_message:
335
+ raise ValueError(f"Модель не найдена или недоступна. Убедитесь, что модель 'gemini-1.5-flash-latest' активна.")
336
+ elif "resource has been exhausted" in error_message.lower() or "quota" in error_message.lower():
337
+ raise ValueError("Квота запросов к Google AI исчерпана. Попробуйте позже.")
338
+ elif "content has been blocked" in error_message.lower() or \
339
+ (response_obj and hasattr(response_obj, 'prompt_feedback') and \
340
+ hasattr(response_obj.prompt_feedback, 'block_reason') and \
341
+ response_obj.prompt_feedback.block_reason):
342
+ reason = "неизвестна"
343
+ if response_obj and hasattr(response_obj, 'prompt_feedback') and hasattr(response_obj.prompt_feedback, 'block_reason') and response_obj.prompt_feedback.block_reason:
344
+ reason = response_obj.prompt_feedback.block_reason
345
+ elif "safety settings" in error_message.lower():
346
+ reason = "настройки безопасности"
347
+ raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
348
+ # Ошибка из raise ValueError выше
349
+ elif "Генерация контента заблокирована" in error_message or "Модель вернула пустой результат" in error_message:
350
+ raise # Перебрасываем уже обработанную ошибку
351
+ else:
352
+ raise ValueError(f"Неизвестная ошибка при генерации Python-кода сайта: {e}")
353
+
354
+
355
+ # --- HTML шаблон для главной страницы генератора ---
356
+ # (Взят из Кода 1 и немного адаптирован)
357
+ html_template = """
358
+ <!DOCTYPE html>
359
+ <html lang="ru">
360
+ <head>
361
+ <meta charset="UTF-8">
362
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
363
+ <title>EVA - Генератор Python Сайтов</title>
364
+ <style>
365
+ :root {
366
+ --system-gray-100-light: #f2f2f7;
367
+ --system-gray-75-light: #f8f8fa;
368
+ --system-gray-50-light: #ffffff;
369
+ --system-gray-dark-100-light: #000000;
370
+ --system-gray-dark-75-light: #1c1c1e;
371
+ --system-gray-dark-50-light: #3a3a3c;
372
+ --system-gray-light-75-light: #8e8e93;
373
+ --system-gray-light-50-light: #aeaeb2;
374
+ --system-blue-light: #007aff;
375
+ --system-blue-light-hover: #005ecf;
376
+ --system-red-light: #ff3b30;
377
+ --system-separator-light: rgba(60, 60, 67, 0.29);
378
+ --system-separator-opaque-light: #d1d1d6;
379
+
380
+ --system-gray-100-dark: #1c1c1e;
381
+ --system-gray-75-dark: #2c2c2e;
382
+ --system-gray-50-dark: #000000;
383
+ --system-gray-dark-100-dark: #ffffff;
384
+ --system-gray-dark-75-dark: #f2f2f7;
385
+ --system-gray-dark-50-dark: #e5e5ea;
386
+ --system-gray-light-75-dark: #8e8e93;
387
+ --system-gray-light-50-dark: #636366;
388
+ --system-blue-dark: #0a84ff;
389
+ --system-blue-dark-hover: #3b9eff;
390
+ --system-red-dark: #ff453a;
391
+ --system-separator-dark: rgba(84, 84, 88, 0.65);
392
+ --system-separator-opaque-dark: #38383a;
393
+ }
394
 
395
+ @media (prefers-color-scheme: dark) {
396
+ :root {
397
+ --bg-color: var(--system-gray-50-dark);
398
+ --content-bg: var(--system-gray-100-dark);
399
+ --text-color: var(--system-gray-dark-100-dark);
400
+ --secondary-text-color: var(--system-gray-light-75-dark);
401
+ --tertiary-text-color: var(--system-gray-light-50-dark);
402
+ --border-color: var(--system-separator-dark);
403
+ --border-color-opaque: var(--system-separator-opaque-dark);
404
+ --input-bg: var(--system-gray-75-dark);
405
+ --primary-color: var(--system-blue-dark);
406
+ --primary-color-hover: var(--system-blue-dark-hover);
407
+ --error-color: var(--system-red-dark);
408
+ }
409
+ }
410
 
411
+ @media (prefers-color-scheme: light) {
 
 
 
 
 
 
 
 
 
 
 
412
  :root {
413
+ --bg-color: var(--system-gray-100-light);
414
+ --content-bg: var(--system-gray-50-light);
415
+ --text-color: var(--system-gray-dark-100-light);
416
+ --secondary-text-color: var(--system-gray-light-75-light);
417
+ --tertiary-text-color: var(--system-gray-light-50-light);
418
+ --border-color: var(--system-separator-light);
419
+ --border-color-opaque: var(--system-separator-opaque-light);
420
+ --input-bg: var(--system-gray-75-light);
421
+ --primary-color: var(--system-blue-light);
422
+ --primary-color-hover: var(--system-blue-light-hover);
423
+ --error-color: var(--system-red-light);
424
  }
425
+ }
426
+
427
+ html { height: -webkit-fill-available; }
428
+ body {
429
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
430
+ margin: 0; padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom);
431
+ background-color: var(--bg-color); color: var(--text-color);
432
+ display: flex; justify-content: center; align-items: flex-start;
433
+ min-height: 100vh; min-height: -webkit-fill-available;
434
+ line-height: 1.45; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
435
+ }
436
+ .container {
437
+ background-color: var(--content-bg); padding: 25px 30px 30px 30px;
438
+ border-radius: 24px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
439
+ max-width: 580px; width: calc(100% - 40px); box-sizing: border-box; margin-top: 30px;
440
+ }
441
+ h1 { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 8px; color: var(--text-color); letter-spacing: -0.5px; }
442
+ p.subtitle { font-size: 17px; color: var(--secondary-text-color); text-align: center; margin-bottom: 35px; font-weight: 400; }
443
+ .form-group { margin-bottom: 28px; }
444
+ label.input-label { display: block; font-weight: 500; margin-bottom: 10px; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
445
+ textarea#prompt-input {
446
+ width: 100%; padding: 14px 18px; border: 1px solid var(--border-color-opaque); border-radius: 12px;
447
+ font-size: 16px; background-color: var(--input-bg); color: var(--text-color);
448
+ box-sizing: border-box; transition: border-color 0.2s ease, box-shadow 0.2s ease;
449
+ font-family: inherit; resize: vertical; min-height: 120px;
450
+ }
451
+ textarea#prompt-input:focus {
452
+ border-color: var(--primary-color);
453
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent);
454
+ outline: none;
455
+ }
456
+ button#generate-button {
457
+ width: 100%; padding: 16px; background-color: var(--primary-color); color: white;
458
+ border: none; border-radius: 12px; font-size: 17px; font-weight: 600; cursor: pointer;
459
+ transition: background-color 0.2s ease, transform 0.1s ease; margin-top: 15px;
460
+ }
461
+ button#generate-button:hover { background-color: var(--primary-color-hover); }
462
+ button#generate-button:active { transform: scale(0.98); }
463
+ button#generate-button:disabled { background-color: var(--tertiary-text-color); cursor: not-allowed; }
464
+ .output-section { margin-top: 35px; }
465
+ .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
466
+ label#output-label { font-weight: 500; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
467
+ button#copy-button {
468
+ background-color: transparent; border: none; color: var(--primary-color);
469
+ font-size: 14px; font-weight: 500; cursor: pointer; padding: 5px 8px;
470
+ border-radius: 6px; transition: background-color 0.2s ease, color 0.2s ease; display: none;
471
+ }
472
+ button#copy-button:hover { background-color: color-mix(in srgb, var(--primary-color) 15%, transparent); }
473
+ button#copy-button:active { background-color: color-mix(in srgb, var(--primary-color) 25%, transparent); }
474
+ button#copy-button.copied { color: #34c759; }
475
+ @media (prefers-color-scheme: dark) { button#copy-button.copied { color: #30d158; } }
476
+ #output-container {
477
+ background-color: var(--input-bg); padding: 18px 20px; border-radius: 12px;
478
+ min-height: 60px; border: 1px solid var(--border-color); word-wrap: break-word;
479
+ font-size: 15px; color: var(--text-color); line-height: 1.5;
480
+ transition: border-color 0.2s ease, background-color 0.2s ease;
481
+ display: flex; align-items: center; justify-content: center;
482
+ }
483
+ #output-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; }
484
+ #output-container a:hover { text-decoration: underline; }
485
+ #output-container.loading::before {
486
+ content: "Генерация сайта..."; display: block; text-align: center;
487
+ font-style: italic; color: var(--secondary-text-color); animation: fadePulse 1.8s infinite ease-in-out;
488
+ }
489
+ #output-container.error {
490
+ color: var(--error-color); font-weight: 500;
491
+ border-color: color-mix(in srgb, var(--error-color) 50%, transparent);
492
+ background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg));
493
+ justify-content: flex-start;
494
+ }
495
+ @keyframes fadePulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
496
+ @media (max-width: 620px) {
497
+ body { padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom); align-items: flex-start; }
498
+ .container { padding: 20px 20px 25px 20px; margin-top: 15px; border-radius: 20px; width: calc(100% - 30px); }
499
+ h1 { font-size: 28px; }
500
+ p.subtitle { font-size: 16px; margin-bottom: 25px; }
501
+ .form-group { margin-bottom: 22px; }
502
+ textarea#prompt-input { padding: 12px 15px; min-height: 100px; }
503
+ button#generate-button { padding: 15px; font-size: 16px; }
504
+ #output-container { padding: 15px 18px; font-size: 14px; min-height: 50px; }
505
+ .output-section { margin-top: 30px; }
506
+ }
507
+ </style>
508
+ </head>
509
+ <body>
510
+ <div class="container">
511
+ <h1>EVA</h1>
512
+ <p class="subtitle">Генератор Python Flask сайтов</p>
513
+
514
+ <form id="generate-form">
515
+ <div class="form-group">
516
+ <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать (на Python/Flask)</label>
517
+ <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай простой одностраничный сайт-визитку на Flask. На главной странице должен быть заголовок 'Мой Сайт', текст 'Добро пожаловать!' и текущее время."></textarea>
518
  </div>
519
+ <button type="submit" id="generate-button">Создать сайт</button>
520
+ </form>
521
+
522
+ <div class="output-section">
523
+ <div class="output-header">
524
+ <label id="output-label">Ссылка для запуска сайта</label>
525
+ <button id="copy-button">Копировать</button>
526
+ </div>
527
+ <div id="output-container" aria-live="polite"></div>
528
  </div>
529
+ </div>
530
+
531
+ <script>
532
+ const form = document.getElementById('generate-form');
533
+ const promptInput = document.getElementById('prompt-input');
534
+ const outputContainer = document.getElementById('output-container');
535
+ const generateButton = document.getElementById('generate-button');
536
+ const copyButton = document.getElementById('copy-button');
537
+
538
+ form.addEventListener('submit', async (event) => {
539
+ event.preventDefault();
540
+ if (!promptInput.value.trim()) {
541
+ showError("Пожалуйста, опишите сайт, который вы хотите создать.");
542
+ return;
543
+ }
544
+ const formData = new FormData(form);
545
+ generateButton.disabled = true;
546
+ generateButton.textContent = 'Генерация...';
547
+ outputContainer.innerHTML = '';
548
+ outputContainer.classList.add('loading');
549
+ outputContainer.classList.remove('error');
550
+ copyButton.style.display = 'none';
551
+ copyButton.textContent = 'Копировать';
552
+ copyButton.classList.remove('copied');
553
+
554
+ try {
555
+ const response = await fetch('/generate', { method: 'POST', body: formData });
556
+ const result = await response.json();
557
+
558
+ if (!response.ok) {
559
+ throw new Error(result.error || `Ошибка сервера: ${response.status}`);
 
 
 
 
 
 
 
 
560
  }
 
561
 
562
+ if (result.site_run_url) { // Изменено с site_url на site_run_url
563
+ const link = document.createElement('a');
564
+ link.href = result.site_run_url;
565
+ link.textContent = "Запустить и открыть сгенерированный сайт";
566
+ link.target = "_blank"; // Откроет в новой вкладке, что приведет к редиректу
567
+ outputContainer.innerHTML = '';
568
+ outputContainer.appendChild(link);
569
+ copyButton.style.display = 'block';
570
+ copyButton.dataset.copyText = window.location.origin + result.site_run_url;
571
+ } else if (result.error) {
572
+ showError(result.error);
573
+ } else {
574
+ showError("Не удалось получить ссылку на сайт. Ответ сервера не содержит URL.");
575
+ }
576
+ } catch (error) {
577
+ console.error("Fetch Error:", error);
578
+ showError(`Ошибка: ${error.message}`);
579
+ } finally {
580
+ generateButton.disabled = false;
581
+ generateButton.textContent = 'Создать сайт';
582
+ outputContainer.classList.remove('loading');
583
+ }
584
+ });
585
+
586
+ copyButton.addEventListener('click', () => {
587
+ const textToCopy = copyButton.dataset.copyText;
588
+ if (!textToCopy) return;
589
+ navigator.clipboard.writeText(textToCopy).then(() => {
590
+ copyButton.textContent = 'Скопировано!';
591
+ copyButton.classList.add('copied');
592
+ setTimeout(() => {
593
+ copyButton.textContent = 'Копировать';
594
+ copyButton.classList.remove('copied');
595
+ }, 1500);
596
+ }).catch(err => {
597
+ console.error('Ошибка копирования: ', err);
598
+ copyButton.textContent = 'Ошибка';
599
+ setTimeout(() => { copyButton.textContent = 'Копировать'; }, 1500);
600
  });
601
+ });
602
+
603
+ function showError(message) {
604
+ outputContainer.innerHTML = '';
605
+ const errorMessageElement = document.createElement('span');
606
+ errorMessageElement.textContent = message;
607
+ outputContainer.appendChild(errorMessageElement);
608
+ outputContainer.classList.add('error');
609
+ outputContainer.classList.remove('loading');
610
+ copyButton.style.display = 'none';
611
+ }
612
+ </script>
613
+ </body>
614
+ </html>
615
+ """
616
 
617
+ # --- Маршруты Flask основного приложения ---
618
+ @app.route('/')
619
+ def index():
 
 
 
 
 
 
 
620
  return Response(html_template, mimetype='text/html')
621
 
622
  @app.route('/generate', methods=['POST'])
623
  def handle_generate():
624
+ if 'prompt' not in request.form:
625
+ return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
626
+
627
+ user_prompt = request.form['prompt']
628
  if not user_prompt or not user_prompt.strip():
629
+ return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
630
 
631
  try:
632
+ python_code = generate_full_python_site_from_prompt(user_prompt)
 
633
 
634
+ if not python_code or not python_code.strip():
635
+ return jsonify({"error": "Сгенерированный Python-код пуст."}), 500
636
+
637
+ # Генерируем уникальное имя файла для .py сайта
638
+ site_filename = f"site_{uuid.uuid4().hex[:12]}.py"
639
+ local_filepath = os.path.join(SITES_CACHE_DIR, site_filename)
640
 
641
+ with open(local_filepath, "w", encoding="utf-8") as f:
642
+ f.write(python_code)
643
+ logging.info(f"Сгенерированный сайт сохранен локально: {local_filepath}")
644
 
645
+ # Загружаем на Hugging Face
646
+ repo_filepath = f"sites/{site_filename}" # Путь в репозитории HF
647
+ if upload_file_to_hf(local_filepath, repo_filepath):
648
+ logging.info(f"Сайт {site_filename} успешно загружен на Hugging Face: {REPO_ID}/{repo_filepath}")
649
+ else:
650
+ # Не фатально, сайт все еще может работать из локального кэша, но не будет синхронизирован
651
+ logging.warning(f"Не удалось загрузить сайт {site_filename} на Hugging Face.")
652
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
 
654
+ # Ссылка для запуска сайта (которая приведет к редиректу на его порт)
655
+ site_run_url = url_for('run_generated_site', filename=site_filename, _external=False)
656
+ return jsonify({"site_run_url": site_run_url})
 
 
 
657
 
658
+ except ValueError as ve: # Ошибки валидации или от GenAI
659
+ logging.error(f"ValueError during site generation: {str(ve)}")
660
+ return jsonify({"error": str(ve)}), 400 # 400 Bad Request или 422 Unprocessable Entity
661
+ except Exception as e:
662
+ logging.error(f"Unexpected error during site generation: {e}", exc_info=True)
663
+ return jsonify({"error": f"Внутренняя ошибка сервера при генерации сайта: {e}"}), 500
664
 
665
+ @app.route('/run_site/<filename>')
666
+ def run_generated_site(filename):
667
+ if not filename.endswith(".py"):
668
+ return "Неверное имя файла сайта.", 400
669
 
670
+ logging.info(f"Запрос на запуск сайта: {filename}")
671
+ site_host, site_port = start_generated_site(filename)
672
+
673
+ if site_host and site_port:
674
+ # Формируем URL для редиректа. request.host содержит <host>:<port> основного приложения.
675
+ # Нам нужен только хост.
676
+ main_app_host = request.host.split(':')[0]
677
+ # Если основной сервер слушает на 0.0.0.0, браузер обычно обращается через localhost или конкретный IP.
678
+ # Используем main_app_host, чтобы редирект был на тот же хост, с которого пришел запрос.
679
+ # Если site_host это '127.0.0.1', а main_app_host это '0.0.0.0' или реальный IP,
680
+ # редирект на 127.0.0.1 может сработать только локально.
681
+ # Для простоты, если main_app_host '0.0.0.0', заменим на '127.0.0.1' для URL редиректа.
682
+ if main_app_host == '0.0.0.0':
683
+ redirect_host = '127.0.0.1'
684
+ else:
685
+ redirect_host = main_app_host
686
+
687
+ redirect_url = f"http://{redirect_host}:{site_port}/"
688
+ logging.info(f"Редирект на запущенный сайт: {redirect_url}")
689
+ return redirect(redirect_url, code=302)
690
  else:
691
+ logging.error(f"Не удалось запустить сайт {filename}.")
692
+ # Можно вернуть страницу с ошибкой
693
+ return f"Не удалось запустить сайт {filename}. Проверьте логи сервера.", 500
694
+
695
 
696
+ if __name__ == '__main__':
697
+ # Проверка наличия HF_TOKEN_WRITE для загрузки на HF
698
+ if not HF_TOKEN_WRITE:
699
+ logging.warning("Переменная окружения HF_TOKEN (с правом записи) не установлена. "
700
+ "Сгенерированные сайты не будут загружаться на Hugging Face.")
701
+
702
+ # Порт для основного приложения-генератора
703
+ main_app_port = 7860
704
+ logging.info(f"Основное приложение генератора запускается на порту {main_app_port}")
705
+ app.run(host='0.0.0.0', port=main_app_port, debug=False)