-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinteractive_editor.py
More file actions
668 lines (535 loc) · 31 KB
/
interactive_editor.py
File metadata and controls
668 lines (535 loc) · 31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
import os
import shutil
import subprocess
import sys
import json
import re
import httpx
from flask import Flask, request, jsonify, send_from_directory, send_file
from openai import OpenAI
import uuid
import diff_match_patch as dmp_module
# Correctly import ChatCompletion and handle potential ImportError
try:
from openai.types.chat import ChatCompletion # type: ignore
except ImportError:
ChatCompletion = None
app = Flask(__name__, template_folder='templates', static_folder='static')
# --- Configuration & State ---
SITE_DIR = ""
client = None
model_name = ""
# Store conversation state
conversation_state = {
"history": [],
"last_read_file": None, # Aggiunto per tenere traccia dell'ultimo file letto
"proposed_patch": None
}
def load_config_and_initialize_client():
global client, model_name
try:
with open("llm_config.json", "r") as f:
llm_config = json.load(f)
# Gestisce il caso in cui api_key sia assente o vuota per i modelli locali
api_key = llm_config.get("api_key")
if not api_key:
api_key = "no-key-required" # Fornisce una chiave fittizia per la libreria
base_url = llm_config.get("url")
model_name = llm_config.get("model", "gpt-4-turbo") # Default se non trovato
# The openai library appends /chat/completions to the base_url.
# If the provided URL already ends with that, remove it to avoid duplication.
if base_url and base_url.endswith("/chat/completions"):
base_url = base_url.removesuffix("/chat/completions")
# Crea esplicitamente un client httpx, disabilitando la gestione dei proxy
# per evitare problemi con proxy di ambiente inattesi.
http_client = httpx.Client()
client = OpenAI(
api_key=api_key,
base_url=base_url,
http_client=http_client
)
return True
except Exception as e:
print(f"Errore durante il caricamento della configurazione LLM: {e}")
return False
# --- Helper Functions ---
def get_file_tree(startpath):
tree = []
for root, dirs, files in os.walk(startpath):
level = root.replace(startpath, '').count(os.sep)
indent = ' ' * 4 * (level)
tree.append(f'{indent}{os.path.basename(root)}/')
subindent = ' ' * 4 * (level + 1)
for f in files:
tree.append(f'{subindent}{f}')
return "\n".join(tree)
def read_file_content(filepath):
try:
with open(os.path.join(SITE_DIR, filepath), 'r', encoding='utf-8') as f:
content = f.read()
# Se è un file HTML, aggiungi automaticamente i CSS collegati come contesto
if filepath.endswith('.html'):
css_context = ""
# Estrai i link CSS dal file HTML
css_links = re.findall(r'<link[^>]*href=["\'](.*?\.css)["\'][^>]*>', content)
for css_file in css_links:
# Rimuovi il prefisso "/" se presente per il path relativo
css_file_path = css_file.lstrip('/')
try:
with open(os.path.join(SITE_DIR, css_file_path), 'r', encoding='utf-8') as css_f:
css_content = css_f.read()
css_context += f"\n\n--- CONTESTO CSS da {css_file} ---\n{css_content}\n--- FINE CONTESTO CSS ---"
except Exception as e:
print(f"Impossibile leggere il CSS {css_file_path}: {e}")
if css_context:
content = f"{content}{css_context}"
return content
except Exception as e:
return f"Errore durante la lettura del file: {e}"
def write_file_content(filepath, content):
try:
# Assicurati che la directory esista
full_path = os.path.join(SITE_DIR, filepath)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, 'w', encoding='utf-8') as f:
f.write(content)
return True # Return True on success
except Exception as e:
print(f"Errore durante la scrittura del file: {e}")
return False # Return False on failure
def apply_patch(original_content, patch_text):
"""
Applica un patch a un testo usando diff_match_patch.
"""
dmp = dmp_module.diff_match_patch()
patches = dmp.patch_fromText(patch_text)
new_content, results = dmp.patch_apply(patches, original_content)
# Controlla se tutti i patch sono stati applicati con successo
if all(results):
return new_content
else:
raise Exception("Errore durante l'applicazione del patch. Alcune modifiche non sono state applicate.")
def extract_html_content(content):
"""
Estrae solo il contenuto HTML valido dal testo dell'LLM.
Rimuove commenti e testo extra dopo il tag </html>.
"""
# Cerca il pattern che inizia con <!DOCTYPE html> o <html>
doctype_match = re.search(r'<!DOCTYPE html>', content, re.IGNORECASE)
html_match = re.search(r'<html[^>]*>', content, re.IGNORECASE)
start_pos = 0
if doctype_match:
start_pos = doctype_match.start()
elif html_match:
start_pos = html_match.start()
# Cerca la chiusura del tag </html>
html_close_match = re.search(r'</html>', content, re.IGNORECASE)
if html_close_match:
end_pos = html_close_match.end()
# Estrai solo il contenuto HTML valido
html_content = content[start_pos:end_pos]
return html_content.strip()
# Se non trova </html>, restituisce tutto il contenuto (fallback)
return content.strip()
def validate_html_completeness(content, filepath):
"""
Valida che il contenuto HTML sia completo per i file .html
"""
if not filepath.endswith('.html'):
return True # Non validiamo file non HTML
content_lower = content.lower().strip()
# Controlla che abbia le parti essenziali di un documento HTML
has_doctype = content_lower.startswith('<!doctype html>')
has_html_open = '<html' in content_lower
has_html_close = '</html>' in content_lower
has_head = '<head>' in content_lower or '<head ' in content_lower
has_body = '<body>' in content_lower or '<body ' in content_lower
if not (has_doctype and has_html_open and has_html_close and has_head and has_body):
return False
return True
def create_patch(original_content, new_content):
"""
Crea un patch in formato testo usando diff_match_patch.
"""
dmp = dmp_module.diff_match_patch()
patches = dmp.patch_make(original_content, new_content)
patch_text = dmp.patch_toText(patches)
return patch_text
def get_system_prompt(file_tree, image_filename=None):
image_info = ""
if image_filename:
image_info = f"\n\n**IMPORTANTE**: L'utente ha caricato un'immagine chiamata `{image_filename}` che è ora disponibile nella directory del sito. Quando l'utente chiede di sostituire o aggiungere questa immagine, usa esattamente questo nome file: `{image_filename}`."
return f"""Sei un'intelligenza artificiale esperta nello sviluppo web. Il tuo compito è modificare un sito web in base alle richieste dell'utente. Devi rispondere sempre in italiano.{image_info}
Il tuo processo deve essere il seguente:
1. **Pianifica**: Spiega brevemente il tuo piano all'utente. Se hai bisogno di modificare un file, devi prima leggerlo.
2. **Leggi il file**: Usa lo strumento `read_file('percorso/del/file.html')` per ottenere il contenuto del file che vuoi modificare. Puoi leggere un solo file alla volta.
3. **Proponi la modifica**: Una volta che hai il contenuto del file, il tuo compito successivo è fornire il **contenuto completo e aggiornato** del file. Il nuovo contenuto deve essere racchiuso in un blocco di codice markdown (es. ```html ... ```). Il backend si occuperà di creare e applicare un patch.
**Strumenti disponibili:**
- `read_file('percorso/del/file.html')`: Legge il contenuto di un file. L'output di questo strumento ti sarà fornito per il passo successivo.
**Gestione delle immagini:**
- Quando l'utente carica un'immagine, questa viene salvata automaticamente nella directory del sito
- Per sostituire un'immagine esistente, modifica semplicemente l'attributo `src` dell'elemento `<img>` con il nuovo nome file
- Per aggiungere una nuova immagine, inserisci un nuovo elemento `<img>` con il nome file fornito
- Le immagini caricate mantengono automaticamente le loro estensioni originali
**Gestione degli stili CSS:**
- Quando leggi un file HTML, riceverai automaticamente anche il contenuto dei file CSS collegati come contesto
- Il contenuto CSS ti viene fornito solo per riferimento - NON devi modificare i file CSS
- Se devi applicare nuovi stili o modificare quelli esistenti, usa SEMPRE CSS inline nell'HTML
- Ispeziona gli stili CSS esistenti per mantenere coerenza visiva
- Per nuove immagini o elementi, aggiungi stili inline che si armonizzino con il design esistente
**Esempio di CSS inline:**
```html
<img src="nuova_immagine.jpg" style="width: 300px; height: 200px; border-radius: 10px; margin: 20px;">
```
**Esempio di sostituzione immagine:**
Se l'utente carica `nuova_foto.jpg` e dice "sostituisci la prima immagine nella home page", trova il primo tag `<img>` e cambia il `src` da quello attuale a `nuova_foto.jpg`.
**Importante**:
- Non generare mai codice Python per modificare i file.
- Non inventare il contenuto dei file. Leggi sempre prima di modificare.
- Fornisci sempre il **contenuto completo del file** nel blocco di codice, non solo la parte modificata.
- **CRITICO**: Quando modifichi un file HTML, devi fornire l'INTERO file dal <!DOCTYPE html> fino a </html>. NON fornire solo sezioni o frammenti.
- **CRITICO**: Non modificare mai i file CSS - usa sempre CSS inline per le modifiche agli stili.
- Se un file non esiste e deve essere creato, fornisci il contenuto completo che il nuovo file dovrebbe avere.
- Quando lavori con immagini caricate, usa sempre il nome file esatto fornito dal sistema.
**Struttura del file tree del sito:**
```
{file_tree}
```
**Ricorda**: Il tuo obiettivo è sempre fornire il contenuto completo e aggiornato del file HTML. Il sistema si occupa di gestire le differenze e applicare le modifiche. Per gli stili CSS, usa sempre CSS inline basandoti sul contesto fornito dai file CSS collegati."""
# --- Flask Routes ---
@app.route('/')
def chat_ui():
return send_from_directory('templates', 'chat.html')
@app.route('/site')
@app.route('/site/<path:path>')
def serve_site(path='index.html'):
return send_from_directory(SITE_DIR, path)
@app.route('/api/upload-image', methods=['POST'])
def upload_image():
try:
if 'image' not in request.files:
return jsonify({'error': 'Nessuna immagine fornita'}), 400
file = request.files['image']
if file.filename == '':
return jsonify({'error': 'Nessun file selezionato'}), 400
if file and file.content_type.startswith('image/'):
# Genera un nome file unico mantenendo l'estensione originale
file_extension = os.path.splitext(file.filename)[1].lower()
if not file_extension:
file_extension = '.jpg' # Default extension
unique_filename = f"uploaded_{uuid.uuid4().hex[:8]}{file_extension}"
# Salva l'immagine nella directory del sito
image_path = os.path.join(SITE_DIR, unique_filename)
file.save(image_path)
print(f"--- DEBUG: Immagine salvata come {unique_filename} ---")
return jsonify({
'success': True,
'filename': unique_filename,
'message': f'Immagine caricata con successo: {unique_filename}'
})
else:
return jsonify({'error': 'Tipo di file non supportato. Solo immagini.'}), 400
except Exception as e:
print(f"Errore durante l'upload dell'immagine: {e}")
return jsonify({'error': 'Errore interno del server'}), 500
@app.route('/api/chat', methods=['POST'])
def handle_chat():
global conversation_state
data = request.json
action = data.get('action', 'chat')
update_happened = False
reply = ""
proposed_code = None # Questo ora conterrà `read_file` o `create_file`
proposed_patch = None # Questo conterrà il diff
if action == 'execute':
# L'esecuzione ora applica un patch, non esegue codice Python
patch_to_apply = conversation_state.get("proposed_patch")
file_to_patch_info = conversation_state.get("last_read_file")
if not patch_to_apply or not file_to_patch_info:
return jsonify({'reply': "Nessun patch da applicare. Per favore, richiedi prima una modifica.", 'updated': False})
filepath = file_to_patch_info["path"]
original_content = file_to_patch_info["content"]
try:
# Normalizza il contenuto originale prima di applicare il patch
original_content_normalized = original_content.replace('\r\n', '\n')
# Applica il patch
new_content = apply_patch(original_content_normalized, patch_to_apply)
# Scrivi il file aggiornato
write_file_content(filepath, new_content)
reply = f"Ho applicato le modifiche al file `{filepath}`. La scheda di anteprima dovrebbe essersi aggiornata."
update_happened = True
# Resetta lo stato dopo l'applicazione
conversation_state["proposed_patch"] = None
conversation_state["last_read_file"] = None
except Exception as e:
print(f"Error during patch application: {e}")
reply = f"Ho riscontrato un errore durante l'applicazione delle modifiche: {e}"
return jsonify({'reply': reply, 'updated': update_happened})
# --- Gestione della richiesta di chat dall'utente ---
user_request = data['text']
image_filename = data.get('image_filename') # Nome del file immagine se presente
file_tree = get_file_tree(SITE_DIR)
# Aggiungi il messaggio dell'utente alla cronologia
user_message_content = user_request
if image_filename:
user_message_content += f" [Immagine allegata: {image_filename}]"
conversation_state["history"].append({"role": "user", "content": user_message_content})
# --- Interazione con LLM ---
try:
# PRIMA CHIAMATA ALL'IA: Pianificazione e richiesta di lettura file
messages = [
{"role": "system", "content": get_system_prompt(file_tree, image_filename)},
] + conversation_state["history"]
response = client.chat.completions.create(
model=model_name,
messages=messages
)
# Estrai la risposta dell'IA
ai_response_raw = ""
if ChatCompletion and isinstance(response, ChatCompletion):
if response.choices and response.choices[0].message and response.choices[0].message.content:
ai_response_raw = response.choices[0].message.content
elif isinstance(response, dict):
if response.get("choices") and response["choices"][0].get("message") and response["choices"][0]["message"].get("content"):
ai_response_raw = response["choices"][0]["message"]["content"]
if not ai_response_raw:
raise ValueError("Ricevuta una risposta vuota o non valida dall'IA.")
# Aggiungi la risposta dell'assistente alla cronologia
conversation_state["history"].append({"role": "assistant", "content": ai_response_raw})
# --- Analizza la risposta dell'IA ---
conversation_state["proposed_patch"] = None
python_match = re.search(r"```(?:python)?\n(.*)```", ai_response_raw, re.DOTALL)
# Aggiungo la ricerca di un blocco di codice generico (html, ecc.) nella prima risposta
code_block_match_first_response = re.search(r"```(?:\w+)?\n(.*)```", ai_response_raw, re.DOTALL)
# Funzione helper per gestire la logica di creazione del patch
def process_file_modification(filepath, user_request_for_context, image_filename=None):
original_content = ""
try:
original_content = read_file_content(filepath)
if "Errore durante la lettura del file" in original_content and "No such file or directory" in original_content:
original_content = ""
conversation_state["last_read_file"] = {"path": filepath, "content": original_content}
# Costruisci il messaggio per la seconda chiamata
system_message = f"Contenuto di `{filepath}` (trattare come vuoto se il file non esiste):\n```\n{original_content}\n```\nL'utente ha chiesto: '{user_request_for_context}'."
if image_filename:
system_message += f" L'utente ha caricato un'immagine chiamata `{image_filename}` che è disponibile nella directory del sito."
system_message += " Ora fornisci il contenuto completo e aggiornato del file per applicare le modifiche richieste."
system_message += "\n\n**ATTENZIONE CRITICA**: Devi fornire l'INTERO file HTML completo, dal <!DOCTYPE html> fino alla chiusura </html>. NON fornire solo una sezione o un frammento. Il file deve essere completo e funzionante."
# SECONDA CHIAMATA ALL'IA: Generazione del contenuto aggiornato
messages_for_modification = conversation_state["history"] + [{
"role": "system",
"content": system_message
}]
modification_response = client.chat.completions.create(
model=model_name,
messages=messages_for_modification
)
modification_ai_response_raw = ""
if ChatCompletion and isinstance(modification_response, ChatCompletion):
if modification_response.choices and modification_response.choices[0].message and modification_response.choices[0].message.content:
modification_ai_response_raw = modification_response.choices[0].message.content
elif isinstance(modification_response, dict):
if modification_response.get("choices") and modification_response["choices"][0].get("message") and modification_response["choices"][0]["message"].get("content"):
modification_ai_response_raw = modification_response.choices[0]["message"]["content"]
if not modification_ai_response_raw:
raise ValueError("L'IA non ha generato il contenuto del file dopo averlo letto.")
conversation_state["history"].append({"role": "assistant", "content": modification_ai_response_raw})
print("\n--- DEBUG: Risposta IA per la modifica ---")
print(modification_ai_response_raw)
print("------------------------------------------\n")
code_block_match = re.search(r"```(?:\w+)?\n(.*)```", modification_ai_response_raw, re.DOTALL)
if code_block_match:
print("--- DEBUG: Rilevato blocco di codice ---")
raw_content = code_block_match.group(1).strip()
# Per file HTML, estrai solo il contenuto valido escludendo commenti extra
if filepath.endswith('.html'):
new_file_content = extract_html_content(raw_content)
print(f"--- DEBUG: Contenuto HTML estratto e filtrato ---")
print(f"--- DEBUG: Primi 200 caratteri: {new_file_content[:200]}... ---")
# Validazione per file HTML
if not validate_html_completeness(new_file_content, filepath):
print("--- ERRORE: Il contenuto HTML non è completo ---")
return "ERRORE: L'IA ha fornito un contenuto HTML incompleto. Il file deve contenere <!DOCTYPE html>, <html>, <head>, <body> e le relative chiusure. Per favore riprova la richiesta.", None
else:
# Per file non HTML (CSS, JS, ecc.), usa il contenuto raw senza filtri
new_file_content = raw_content
print(f"--- DEBUG: File non HTML - contenuto usato senza filtri ---")
print(f"--- DEBUG: Primi 200 caratteri: {new_file_content[:200]}... ---")
# Normalizza i line endings per evitare problemi con il diff
original_content_normalized = original_content.replace('\r\n', '\n')
new_file_content_normalized = new_file_content.replace('\r\n', '\n')
# Logica semplificata: ci fidiamo che l'IA restituisca il contenuto completo del file
# come richiesto dal prompt di sistema.
proposed_patch = create_patch(original_content_normalized, new_file_content_normalized)
# Estrai il testo dopo il blocco di codice come riassunto
print("--- DEBUG: Estrazione riassunto dalla risposta dell'IA ---")
# Trova il testo dopo il blocco di codice
code_block_end = modification_ai_response_raw.find('```', modification_ai_response_raw.find('```') + 3)
if code_block_end != -1:
summary_text = modification_ai_response_raw[code_block_end + 3:].strip()
# Rimuovi eventuali backticks rimasti
summary_text = summary_text.replace('`', '')
# Se il testo è troppo lungo, prendi solo le prime righe significative
if len(summary_text) > 800:
lines = summary_text.split('\n')
# Prendi le prime righe fino a un massimo di caratteri ragionevole
summary_lines = []
char_count = 0
for line in lines:
if char_count + len(line) > 800:
break
summary_lines.append(line)
char_count += len(line)
summary_text = '\n'.join(summary_lines)
print(f"--- DEBUG: Riassunto estratto: {summary_text[:200]}... ---")
else:
summary_text = "Verrà applicata la modifica richiesta al file."
# Se il riassunto è vuoto o troppo breve, usa un fallback
if not summary_text or len(summary_text.strip()) < 20:
if image_filename:
summary_text = f"Aggiungerò l'immagine '{image_filename}' al sito web."
else:
summary_text = "Applicherò le modifiche richieste al sito web."
# La reply per il frontend con il riassunto estratto
reply_text = f"{summary_text}"
conversation_state["proposed_patch"] = proposed_patch
print(f"--- DEBUG: Patch generato dal backend ---\n{proposed_patch}\n-----------------------------")
return reply_text, proposed_patch
else:
print("--- DEBUG: Nessun blocco di codice trovato nella seconda risposta ---")
return f"{ai_response_raw.split('```')[0].strip()}\n\n{modification_ai_response_raw}", None
except Exception as e:
return f"Errore durante la lettura del file o la generazione della modifica: {e}", None
if python_match:
command_code = python_match.group(1).strip()
print(f"--- DEBUG: Codice Python rilevato: {command_code[:200]} ---")
# Rimuovi eventuali commenti all'inizio del comando
command_code = command_code.lstrip('#').strip()
if "read_file" in command_code:
print("--- DEBUG: read_file trovato nel comando ---")
read_match = re.search(r"read_file\(['\"](.*?)['\"]\)", command_code)
if read_match:
filepath = read_match.group(1)
print(f"--- DEBUG: File da leggere: {filepath} ---")
reply, proposed_patch = process_file_modification(filepath, user_request, image_filename)
if proposed_patch:
conversation_state["proposed_patch"] = proposed_patch
else:
reply = "Non sono riuscito a capire quale file leggere dal tuo comando."
else:
# Se l'LLM non ha usato read_file, proviamo a dedurre il file target e procedere
print("--- DEBUG: LLM non ha usato read_file. Tento di procedere automaticamente ---")
# Assumiamo che voglia modificare index.html se non specifica diversamente
filepath_to_use = "index.html"
# Controlliamo se nella risposta dell'AI è menzionato un file specifico
filepath_mentions = re.findall(r'\b[\w/.-]+\.(?:html|css|js|json)\b', ai_response_raw.lower())
if filepath_mentions:
filepath_to_use = filepath_mentions[0]
print(f"--- DEBUG: File menzionato nella risposta: {filepath_to_use} ---")
print(f"--- DEBUG: Procedo con la modifica del file: {filepath_to_use} ---")
reply, proposed_patch = process_file_modification(filepath_to_use, user_request, image_filename)
if proposed_patch:
conversation_state["proposed_patch"] = proposed_patch
# GESTIONE DEL CASO IN CUI L'IA PROPONE SUBITO IL CODICE
elif code_block_match_first_response:
print("--- DEBUG: Rilevato blocco di codice nella prima risposta. Tento di applicare la modifica. ---")
# Se l'IA propone subito una modifica, dobbiamo capire a quale file si riferisce.
# Per ora, assumiamo si riferisca all'ultimo file letto, o a un file standard come 'index.html'.
last_file = conversation_state.get("last_read_file")
filepath_to_use = last_file["path"] if last_file else "index.html"
print(f"--- DEBUG: Assumo che la modifica sia per il file: {filepath_to_use} ---")
# Eseguiamo comunque il flusso a due passaggi per coerenza e robustezza
reply, proposed_patch = process_file_modification(filepath_to_use, user_request, image_filename)
if proposed_patch:
conversation_state["proposed_patch"] = proposed_patch
else:
# Nessun codice trovato, solo una risposta testuale
print("--- DEBUG: Nessun codice Python o blocco di codice trovato. Risposta testuale semplice. ---")
# Controllo se nella risposta è menzionato read_file anche senza blocco di codice
if "read_file" in ai_response_raw:
print("--- DEBUG: read_file menzionato nella risposta testuale ---")
read_match = re.search(r"read_file\(['\"](.*?)['\"]\)", ai_response_raw)
if read_match:
filepath = read_match.group(1)
print(f"--- DEBUG: File da leggere dalla risposta testuale: {filepath} ---")
reply, proposed_patch = process_file_modification(filepath, user_request, image_filename)
if proposed_patch:
conversation_state["proposed_patch"] = proposed_patch
else:
reply = ai_response_raw
else:
reply = ai_response_raw
conversation_state["last_read_file"] = None
except Exception as e:
print(f"Error during AI interaction or execution: {e}")
reply = f"Ho riscontrato un errore: {e}"
conversation_state["proposed_patch"] = None
conversation_state["last_read_file"] = None
# Debug log per verificare cosa viene inviato al frontend
patch_to_send = conversation_state.get("proposed_patch")
print(f"--- DEBUG: Invio al frontend - proposed_patch presente: {patch_to_send is not None} ---")
if patch_to_send:
print(f"--- DEBUG: Lunghezza patch: {len(patch_to_send)} caratteri ---")
return jsonify({
'reply': reply,
'updated': update_happened,
'proposed_patch': conversation_state.get("proposed_patch") # Invia il patch al frontend
})
@app.route('/<path:path>')
def serve_static_from_root(path):
"""This is a catch-all for requests to the root.
It tries to serve files like /styles.css or /main.js from the SITE_DIR.
This is a common workaround for badly formed absolute paths in generated HTML.
"""
# Prevent this route from interfering with the API or the main chat UI
if path.startswith('api/') or path == 'favicon.ico':
return jsonify({"error": "Non trovato"}), 404
return send_from_directory(SITE_DIR, path)
@app.route('/api/download')
def download_zip():
zip_path = shutil.make_archive('website', 'zip', SITE_DIR)
return send_file(zip_path, as_attachment=True)
@app.route('/api/shutdown', methods=['GET'])
def shutdown():
# This is a simple way to shutdown. In a production environment, this would be more robust.
func = request.environ.get('werkzeug.server.shutdown')
if func is None:
raise RuntimeError('Not running with the Werkzeug Server')
func()
return 'Spegnimento del server in corso...'
def run_interactive_editor(site_path, port=5001):
global SITE_DIR
SITE_DIR = site_path
# Inizializza lo stato della conversazione all'avvio
global conversation_state
conversation_state = {
"history": [],
"last_read_file": None,
"proposed_patch": None
}
if not load_config_and_initialize_client():
print("Could not start interactive editor due to LLM configuration error.")
return
print(f"Starting interactive editor for {site_path} on http://127.0.0.1:{port}")
app.run(port=port, debug=False)
if __name__ == '__main__':
# This allows running the editor directly for testing
# Example: python interactive_editor.py "path/to/your/website"
if len(sys.argv) > 1:
import webbrowser
import threading
import time
project_path = sys.argv[1]
port = 5001
chat_url = f"http://127.0.0.1:{port}"
site_url = f"{chat_url}/site"
# Open the chat UI
print(f"Attempting to open browser at {chat_url}")
webbrowser.open(chat_url)
# Open the website preview in a new tab
print(f"Attempting to open site preview at {site_url}")
webbrowser.open_new_tab(site_url)
# Run the Flask server
run_interactive_editor(project_path, port)
else:
print("Usage: python interactive_editor.py <path_to_website_folder>")