second commit

This commit is contained in:
Carlos Tello
2026-09-20 13:51:24 -03:00
parent bdf42e74a3
commit f99baf0d92
4 changed files with 1186 additions and 0 deletions
+316
View File
@@ -0,0 +1,316 @@
"""
Code Agent - Handles file operations, code execution, and testing
"""
import os
import json
import requests
import subprocess
import webbrowser
import time
from pathlib import Path
from datetime import datetime
from typing import Dict, Any
from dotenv import load_dotenv
load_dotenv()
# ============================================================================
# CONFIGURATION
# ============================================================================
LLM_SERVER = os.getenv("LLM_SERVER_URL", "http://localhost:8000")
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")
LLM_FAST = os.getenv("LLM_FAST_MODEL", "phi")
LLM_POWER = os.getenv("LLM_POWER_MODEL", "deepseek-coder:33b")
# ============================================================================
# CODE AGENT CLASS
# ============================================================================
class CodeAgent:
"""Agent that modifies files, executes code, and tests in browser"""
def __init__(self, work_dir: str = "./workspace"):
self.work_dir = Path(work_dir)
self.work_dir.mkdir(exist_ok=True)
print(f"📁 Workspace: {self.work_dir.absolute()}")
# ========================================================================
# FILE OPERATIONS
# ========================================================================
def read_file(self, file_path: str) -> str:
"""Read file from workspace"""
file = self.work_dir / file_path
if not file.exists():
return f"❌ File not found: {file_path}"
try:
with open(file, 'r', encoding='utf-8') as f:
content = f.read()
print(f"✅ Read: {file_path} ({len(content)} bytes)")
return content
except Exception as e:
return f"❌ Error reading {file_path}: {e}"
def write_file(self, file_path: str, content: str) -> str:
"""Write/modify file in workspace"""
file = self.work_dir / file_path
file.parent.mkdir(parents=True, exist_ok=True)
try:
with open(file, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✅ Wrote: {file_path}")
return f"File saved: {file_path}"
except Exception as e:
return f"❌ Error writing {file_path}: {e}"
def delete_file(self, file_path: str) -> str:
"""Delete file from workspace"""
file = self.work_dir / file_path
if not file.exists():
return f"❌ File not found: {file_path}"
try:
file.unlink()
return f"✅ Deleted: {file_path}"
except Exception as e:
return f"❌ Error deleting {file_path}: {e}"
def list_files(self) -> str:
"""List all files in workspace"""
files = list(self.work_dir.rglob("*"))
file_list = "\n".join([
f" {f.relative_to(self.work_dir)}"
for f in files if f.is_file()
])
return f"📁 Files in workspace:\n{file_list}" if file_list else "📁 No files"
# ========================================================================
# CODE EXECUTION
# ========================================================================
def execute_python(self, code: str, file_name: str = "exec.py") -> str:
"""Execute Python code"""
script_path = self.work_dir / file_name
try:
# Save script
with open(script_path, 'w', encoding='utf-8') as f:
f.write(code)
# Execute
result = subprocess.run(
["python3", str(script_path)],
capture_output=True,
text=True,
timeout=30,
cwd=self.work_dir
)
output = result.stdout + result.stderr
print(f"🐍 Python executed: {file_name}")
return output[:1000] # Limit output
except subprocess.TimeoutExpired:
return "❌ Timeout: Script took more than 30 seconds"
except Exception as e:
return f"❌ Error: {e}"
def execute_node(self, code: str, file_name: str = "exec.js") -> str:
"""Execute Node.js code"""
script_path = self.work_dir / file_name
try:
with open(script_path, 'w', encoding='utf-8') as f:
f.write(code)
result = subprocess.run(
["node", str(script_path)],
capture_output=True,
text=True,
timeout=30,
cwd=self.work_dir
)
output = result.stdout + result.stderr
print(f"📟 Node.js executed: {file_name}")
return output[:1000]
except FileNotFoundError:
return "❌ Node.js not installed"
except Exception as e:
return f"❌ Error: {e}"
def execute_shell(self, command: str) -> str:
"""Execute shell command (whitelist safe commands only)"""
allowed_commands = [
'ls', 'pwd', 'mkdir', 'rm', 'cp', 'mv',
'cat', 'grep', 'find', 'du', 'df',
'git', 'npm', 'pip', 'python'
]
cmd_name = command.split()[0] if command else ""
if cmd_name not in allowed_commands:
return f"❌ Command not allowed: {cmd_name}"
try:
result = subprocess.run(
command.split(),
capture_output=True,
text=True,
timeout=30,
cwd=self.work_dir
)
output = result.stdout + result.stderr
return output[:500]
except Exception as e:
return f"❌ Error: {e}"
# ========================================================================
# TESTING & BROWSER
# ========================================================================
def test_html(self, html_file: str) -> str:
"""Open HTML file in browser for testing"""
file_path = self.work_dir / html_file
if not file_path.exists():
return f"❌ File not found: {html_file}"
try:
url = f"file:///{file_path.absolute()}".replace("\\", "/")
webbrowser.open(url)
print(f"🌐 Opened in browser: {html_file}")
return f"✅ HTML opened: {html_file}"
except Exception as e:
return f"❌ Error: {e}"
def open_browser(self, url: str) -> str:
"""Open URL in browser"""
try:
webbrowser.open(url)
return f"✅ Browser opened: {url}"
except Exception as e:
return f"❌ Error: {e}"
# ========================================================================
# GIT OPERATIONS
# ========================================================================
def git_commit(self, message: str = None) -> str:
"""Auto-commit changes"""
try:
if not message:
message = f"Auto-commit from agent - {datetime.now().isoformat()}"
# Git add
result = subprocess.run(
['git', 'add', '.'],
cwd=self.work_dir,
capture_output=True,
text=True
)
if result.returncode != 0:
return f"❌ Git add failed: {result.stderr}"
# Git commit
result = subprocess.run(
['git', 'commit', '-m', message],
cwd=self.work_dir,
capture_output=True,
text=True
)
if result.returncode != 0:
return f"ℹ️ Nothing to commit"
return f"✅ Committed: {message}"
except Exception as e:
return f"❌ Error: {e}"
def git_push(self) -> str:
"""Push to remote repository"""
try:
result = subprocess.run(
['git', 'push'],
cwd=self.work_dir,
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
return "✅ Pushed to remote"
else:
return f"❌ Push failed: {result.stderr}"
except Exception as e:
return f"❌ Error: {e}"
# ========================================================================
# LLM INTERACTION
# ========================================================================
def call_llm(self, prompt: str, model: str = "phi") -> str:
"""Call LLM server"""
try:
response = requests.post(
f"{OLLAMA_URL}/api/generate",
json={
"model": model,
"prompt": prompt,
"stream": False
},
timeout=300
)
if response.status_code == 200:
return response.json()["response"]
else:
return f"❌ LLM Error: {response.status_code}"
except requests.exceptions.ConnectionError:
return f"❌ Cannot connect to {OLLAMA_URL}"
except Exception as e:
return f"❌ Error: {e}"
# ============================================================================
# GET AGENT TOOLS
# ============================================================================
def get_agent_tools(agent: CodeAgent) -> Dict[str, Any]:
"""Return dictionary of available tools"""
return {
"read_file": agent.read_file,
"write_file": agent.write_file,
"delete_file": agent.delete_file,
"execute_python": agent.execute_python,
"execute_node": agent.execute_node,
"execute_shell": agent.execute_shell,
"test_html": agent.test_html,
"open_browser": agent.open_browser,
"git_commit": agent.git_commit,
"git_push": agent.git_push,
"list_files": agent.list_files,
"call_llm": agent.call_llm,
}
# ============================================================================
# MAIN (for testing)
# ============================================================================
if __name__ == "__main__":
agent = CodeAgent()
# Test
print("\n🧪 Testing Agent...")
print(agent.list_files())
# Create test file
agent.write_file("test.txt", "Hello from agent!")
print(agent.read_file("test.txt"))
+247
View File
@@ -0,0 +1,247 @@
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
from fastapi.responses import StreamingResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.cors import CORSMiddleware
import aiofiles
import base64
import requests
import json
import os
from pathlib import Path
from dotenv import load_dotenv
import asyncio
import io
from PIL import Image
import uuid
load_dotenv()
app = FastAPI(title="Code Agent Chat")
# CORS para que el frontend acceda
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Config
LLM_SERVER = os.getenv("LLM_SERVER_URL", "http://localhost:8000")
UPLOAD_DIR = Path("./uploads")
WORKSPACE_DIR = Path("./workspace")
UPLOAD_DIR.mkdir(exist_ok=True)
WORKSPACE_DIR.mkdir(exist_ok=True)
# Importar agent
from agent import CodeAgent, get_agent_tools
agent = CodeAgent(work_dir=str(WORKSPACE_DIR))
# ============================================================================
# ENDPOINTS
# ============================================================================
@app.get("/api/health")
async def health():
"""Verificar que el servidor está vivo"""
return {"status": "ok", "workspace": str(WORKSPACE_DIR.absolute())}
@app.get("/api/files")
async def list_files():
"""Listar archivos en workspace"""
files = []
for f in WORKSPACE_DIR.rglob("*"):
if f.is_file():
files.append({
"name": str(f.relative_to(WORKSPACE_DIR)),
"size": f.stat().st_size,
"type": f.suffix
})
return {"files": files}
@app.get("/api/file/{file_path:path}")
async def read_file(file_path: str):
"""Leer contenido de archivo"""
file = WORKSPACE_DIR / file_path
if not file.exists():
raise HTTPException(status_code=404, detail="Archivo no encontrado")
try:
with open(file, 'r') as f:
content = f.read()
return {"content": content, "name": file_path}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/chat")
async def chat(
message: str = Form(...),
image: UploadFile = File(None),
model: str = Form("phi")
):
"""
Endpoint principal de chat
- message: tu prompt
- image: imagen adjunta (opcional)
- model: "phi" (rápido) o "deepseek-coder:33b" (potente)
"""
# Procesar imagen si hay
image_data = None
image_base64 = None
if image:
try:
contents = await image.read()
image_base64 = base64.b64encode(contents).decode()
# Guardar imagen
image_path = UPLOAD_DIR / f"{uuid.uuid4()}.png"
with open(image_path, 'wb') as f:
f.write(contents)
image_data = f"[Imagen adjunta: {image.filename}]"
print(f"📸 Imagen procesada: {image.filename}")
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error procesando imagen: {e}")
# Construir prompt para el LLM
full_prompt = f"""
{message}
{f"Contexto de imagen: {image_data}" if image_data else ""}
Tienes acceso a estas herramientas:
- read_file(file_path): Leer archivo
- write_file(file_path, content): Escribir archivo
- execute_python(code): Ejecutar Python
- execute_node(code): Ejecutar Node.js
- test_html(file_name): Servir y testear HTML
- open_browser(url): Abrir navegador
- list_files(): Listar archivos
Responde en JSON con tu pensamiento y la acción a ejecutar.
Ejemplo:
{{"thought": "Voy a leer el archivo", "action": "read_file", "args": {{"file_path": "app.js"}}}}
"""
# Llamar LLM
try:
response = requests.post(
f"{LLM_SERVER}/code",
json={
"message": full_prompt,
"model": model
},
timeout=300,
stream=False
)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail=response.text
)
llm_response = response.json()["response"]
# Parsear JSON y ejecutar acción
result = process_agent_response(llm_response)
return {
"message": message,
"image": image.filename if image else None,
"model": model,
"llm_response": llm_response,
"action_result": result,
"image_base64": image_base64
}
except requests.exceptions.ConnectionError:
raise HTTPException(
status_code=503,
detail=f"No se puede conectar a LLM Server: {LLM_SERVER}"
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/chat-stream")
async def chat_stream(
message: str = Form(...),
image: UploadFile = File(None),
model: str = Form("phi")
):
"""Versión streaming (respuestas en tiempo real)"""
image_data = None
if image:
contents = await image.read()
image_path = UPLOAD_DIR / f"{uuid.uuid4()}.png"
with open(image_path, 'wb') as f:
f.write(contents)
image_data = f"[Imagen: {image.filename}]"
full_prompt = f"{message}\n{image_data if image_data else ''}"
async def generate():
try:
# Streaming desde LLM
response = requests.post(
f"{LLM_SERVER}/code",
json={
"message": full_prompt,
"model": model
},
timeout=300,
stream=True
)
for line in response.iter_lines():
if line:
yield line + b'\n'
except Exception as e:
yield json.dumps({"error": str(e)}).encode() + b'\n'
return StreamingResponse(generate(), media_type="text/event-stream")
def process_agent_response(response: str) -> dict:
"""Parsear respuesta del LLM y ejecutar acción"""
tools = get_agent_tools(agent)
try:
# Extraer JSON
json_start = response.find('{')
json_end = response.rfind('}') + 1
json_str = response[json_start:json_end]
action = json.loads(json_str)
except:
return {"error": "No se pudo parsear la respuesta", "raw": response[:200]}
action_name = action.get("action") or action.get("tool")
args = action.get("args", {})
if action_name not in tools:
return {"error": f"Acción desconocida: {action_name}"}
try:
result = tools[action_name](**args)
return {"success": True, "action": action_name, "result": result}
except Exception as e:
return {"error": f"Error ejecutando {action_name}: {str(e)}"}
# ============================================================================
# SERVIR STATIC FILES (UI Frontend)
# ============================================================================
app.mount("/", StaticFiles(directory="static", html=True), name="static")
if __name__ == "__main__":
import uvicorn
print(f"🚀 Backend en http://localhost:5000")
print(f"📁 Workspace: {WORKSPACE_DIR.absolute()}")
uvicorn.run(app, host="0.0.0.0", port=5000)
+74
View File
@@ -0,0 +1,74 @@
{
"server": {
"host": "0.0.0.0",
"api_port": 8000,
"web_port": 5000,
"workers": 4,
"debug": false,
"environment": "production"
},
"models": {
"fast": {
"name": "phi",
"description": "Phi 2.7B - Rápido",
"vram_required_gb": 2,
"tokens_per_second": 50
},
"power": {
"name": "deepseek-coder:33b",
"description": "DeepSeek 33B - Potente",
"vram_required_gb": 18,
"tokens_per_second": 6
}
},
"gpu": {
"memory_fraction": 0.9,
"max_batch_size": 8,
"enable_cuda": true,
"device_id": 0
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"log_dir": "./logs",
"max_file_size_mb": 10,
"backup_count": 10
},
"database": {
"type": "sqlite",
"path": "./data/llm_history.db",
"enable_history": true
},
"security": {
"enable_rate_limit": true,
"rate_limit_requests": 100,
"rate_limit_period_seconds": 60,
"require_api_key": false,
"allowed_origins": ["*"]
},
"workspace": {
"path": "./workspace",
"max_file_size_mb": 100,
"allowed_extensions": [".py", ".js", ".html", ".css", ".json", ".txt", ".md"]
},
"api_endpoints": {
"health": "/api/health",
"models": "/api/models",
"chat": "/api/chat",
"files": "/api/files",
"metrics": "/api/metrics"
},
"ollama": {
"host": "localhost",
"port": 11434,
"timeout_seconds": 300,
"auto_download_models": true
},
"features": {
"enable_git_commit": true,
"enable_code_execution": true,
"enable_file_operations": true,
"enable_browser_testing": true,
"enable_metrics": true
}
}
+549
View File
@@ -0,0 +1,549 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code Agent Chat</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
display: flex;
}
.container {
display: flex;
width: 100%;
gap: 20px;
padding: 20px;
}
/* Chat Panel */
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
overflow: hidden;
}
.chat-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
font-size: 20px;
font-weight: bold;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.message {
display: flex;
gap: 10px;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message.user {
justify-content: flex-end;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 12px;
word-wrap: break-word;
}
.message.user .message-content {
background: #667eea;
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant .message-content {
background: #f0f0f0;
color: #333;
border-bottom-left-radius: 4px;
}
.message-image {
max-width: 200px;
border-radius: 8px;
margin-top: 8px;
}
.message-meta {
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* Input Area */
.input-area {
border-top: 1px solid #e0e0e0;
padding: 20px;
background: #fafafa;
}
.input-container {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
resize: vertical;
min-height: 60px;
max-height: 120px;
font-family: inherit;
font-size: 14px;
}
textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.button-group {
display: flex;
gap: 10px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.send-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
flex: 1;
}
.send-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.file-input-wrapper {
position: relative;
overflow: hidden;
}
.file-input-wrapper input[type="file"] {
display: none;
}
.upload-btn {
background: #4CAF50;
color: white;
padding: 10px 20px;
}
.upload-btn:hover {
background: #45a049;
}
.selected-image {
max-width: 150px;
max-height: 150px;
border-radius: 8px;
margin-top: 10px;
}
.image-indicator {
font-size: 12px;
color: #666;
margin-top: 5px;
}
/* Sidebar */
.sidebar {
width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
background: #f5f5f5;
padding: 15px;
font-weight: bold;
border-bottom: 1px solid #e0e0e0;
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.file-item {
padding: 8px 12px;
background: #f9f9f9;
border-radius: 6px;
margin-bottom: 8px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.file-item:hover {
background: #f0f0f0;
border-left-color: #667eea;
}
.model-selector {
padding: 15px;
border-top: 1px solid #e0e0e0;
}
select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 6px;
}
.loading {
display: inline-block;
width: 8px;
height: 8px;
background: #667eea;
border-radius: 50%;
animation: pulse 1.5s infinite;
margin-right: 5px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.error {
background: #ffebee;
color: #c62828;
padding: 12px;
border-radius: 8px;
margin-top: 10px;
}
.success {
background: #e8f5e9;
color: #2e7d32;
padding: 12px;
border-radius: 8px;
margin-top: 10px;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.message-content {
max-width: 90%;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Chat Panel -->
<div class="chat-panel">
<div class="chat-header">🤖 Code Agent Chat</div>
<div class="chat-messages" id="chatMessages"></div>
<div class="input-area">
<div class="input-container">
<textarea
id="messageInput"
placeholder="Escribe tu directive aquí... (ej: Crea un contador HTML)"
onkeydown="if(event.key==='Enter' && event.ctrlKey) sendMessage();"
></textarea>
</div>
<div id="selectedImageContainer" style="display:none;">
<img id="selectedImagePreview" class="selected-image" alt="Imagen seleccionada">
<div class="image-indicator" id="imageIndicator"></div>
</div>
<div class="button-group">
<div class="file-input-wrapper">
<input type="file" id="imageInput" accept="image/*">
<button class="upload-btn" onclick="document.getElementById('imageInput').click();">
📸 Adjuntar Imagen
</button>
</div>
<button class="send-btn" id="sendBtn" onclick="sendMessage();">
Enviar (Ctrl+Enter)
</button>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">📁 Workspace</div>
<div class="sidebar-content" id="fileList">
<p style="color: #999; font-size: 13px;">Cargando archivos...</p>
</div>
<div class="model-selector">
<label style="font-size: 12px; color: #666;">Modelo LLM:</label>
<select id="modelSelect" onchange="localStorage.setItem('selectedModel', this.value)">
<option value="phi">⚡ Phi (Rápido)</option>
<option value="deepseek-coder:33b">🧠 DeepSeek (Potente)</option>
</select>
</div>
</div>
</div>
<script>
const API_URL = "http://localhost:5000/api";
const chatMessagesDiv = document.getElementById("chatMessages");
const messageInput = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn");
const imageInput = document.getElementById("imageInput");
const fileList = document.getElementById("fileList");
const modelSelect = document.getElementById("modelSelect");
const selectedImageContainer = document.getElementById("selectedImageContainer");
const selectedImagePreview = document.getElementById("selectedImagePreview");
const imageIndicator = document.getElementById("imageIndicator");
let selectedImage = null;
// Load saved model
const savedModel = localStorage.getItem("selectedModel") || "phi";
modelSelect.value = savedModel;
// Manejo de imagen
imageInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (file) {
selectedImage = file;
const reader = new FileReader();
reader.onload = (event) => {
selectedImagePreview.src = event.target.result;
imageIndicator.textContent = `✅ ${file.name} seleccionada`;
selectedImageContainer.style.display = "block";
};
reader.readAsDataURL(file);
}
});
// Limpiar imagen
function clearImage() {
selectedImage = null;
imageInput.value = "";
selectedImageContainer.style.display = "none";
}
// Enviar mensaje
async function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
const model = modelSelect.value;
// Mostrar mensaje del usuario
addMessage(message, "user", selectedImage);
messageInput.value = "";
clearImage();
sendBtn.disabled = true;
try {
// Preparar form data
const formData = new FormData();
formData.append("message", message);
formData.append("model", model);
if (selectedImage) {
formData.append("image", selectedImage);
}
// Enviar
const response = await fetch(`${API_URL}/chat`, {
method: "POST",
body: formData
});
if (!response.ok) {
const error = await response.json();
addMessage(`❌ Error: ${error.detail}`, "assistant");
return;
}
const data = await response.json();
// Mostrar respuesta del LLM
addMessage(data.llm_response, "assistant", null, model);
// Mostrar resultado de la acción
if (data.action_result) {
const resultText = JSON.stringify(data.action_result, null, 2);
if (data.action_result.success) {
addMessage(`✅ ${data.action_result.result}`, "assistant");
} else {
addMessage(`⚠️ ${data.action_result.error || data.action_result.result}`, "assistant");
}
}
// Recargar archivos
loadFiles();
} catch (error) {
addMessage(`❌ Error: ${error.message}`, "assistant");
} finally {
sendBtn.disabled = false;
messageInput.focus();
}
}
// Agregar mensaje al chat
function addMessage(text, sender, image = null, model = null) {
const messageDiv = document.createElement("div");
messageDiv.className = `message ${sender}`;
const contentDiv = document.createElement("div");
contentDiv.className = "message-content";
// Formatear texto (markdown básico)
let formattedText = text
.replace(/```([^`]*?)```/g, "<pre><code>$1</code></pre>")
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
.replace(/_(.*?)_/g, "<em>$1</em>");
contentDiv.innerHTML = formattedText;
if (image instanceof File) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement("img");
img.src = e.target.result;
img.className = "message-image";
contentDiv.appendChild(img);
};
reader.readAsDataURL(image);
}
messageDiv.appendChild(contentDiv);
if (model) {
const meta = document.createElement("div");
meta.className = "message-meta";
meta.textContent = `Modelo: ${model}`;
messageDiv.appendChild(meta);
}
chatMessagesDiv.appendChild(messageDiv);
chatMessagesDiv.scrollTop = chatMessagesDiv.scrollHeight;
}
// Cargar archivos del workspace
async function loadFiles() {
try {
const response = await fetch(`${API_URL}/files`);
const data = await response.json();
fileList.innerHTML = "";
if (data.files.length === 0) {
fileList.innerHTML = '<p style="color: #999; font-size: 13px;">Sin archivos aún</p>';
return;
}
data.files.forEach(file => {
const item = document.createElement("div");
item.className = "file-item";
let icon = "📄";
if (file.type === ".html") icon = "🌐";
if (file.type === ".js") icon = "📜";
if (file.type === ".py") icon = "🐍";
if (file.type === ".css") icon = "🎨";
item.innerHTML = `${icon} <strong>${file.name}</strong><br>
<span style="font-size: 11px; color: #999;">${(file.size / 1024).toFixed(1)} KB</span>`;
item.onclick = async () => {
try {
const fileResponse = await fetch(`${API_URL}/file/${file.name}`);
const fileData = await fileResponse.json();
addMessage(`\`\`\`\n${fileData.content}\n\`\`\``, "assistant");
} catch (e) {
addMessage(`Error leyendo ${file.name}`, "assistant");
}
};
fileList.appendChild(item);
});
} catch (error) {
console.error("Error cargando archivos:", error);
}
}
// Cargar archivos al iniciar
loadFiles();
// Auto-refresh cada 10 segundos
setInterval(loadFiles, 10000);
// Enter para enviar
messageInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.ctrlKey) {
sendMessage();
}
});
// Mensaje inicial
setTimeout(() => {
addMessage("👋 Hola! Soy tu agent agentico. Puedes:\n\n- **Pedir que cree código** (HTML, Python, Node.js)\n- **Adjuntar imágenes** (capturas, diseños)\n- **Dar directivas** para modificar tus archivos\n- **Ejecutar y testear** automáticamente en el navegador\n\n¿Qué querés crear?", "assistant");
}, 500);
</script>
</body>
</html>