Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files
app.py
CHANGED
|
@@ -62,10 +62,17 @@ async def dashboard_home():
|
|
| 62 |
.stat-label { font-size: 0.75rem; color: #94a3b8; font-weight: 600; text-transform: uppercase; }
|
| 63 |
.stat-value { font-size: 1.5rem; font-weight: 800; color: #38bdf8; margin-top: 5px; }
|
| 64 |
.status-dot { height: 10px; width: 10px; background-color: #10b981; border-radius: 50%; display: inline-block; margin-right: 8px; }
|
|
|
|
|
|
|
|
|
|
| 65 |
</style>
|
| 66 |
</head>
|
| 67 |
<body>
|
| 68 |
<div class="container">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
<p style="text-align: center; font-size: 0.8rem; color: #64748b;"><span class="status-dot"></span> SISTEMA ONLINE</p>
|
| 70 |
<h1>Córtex-Nexus Live</h1>
|
| 71 |
<div class="stats">
|
|
@@ -274,3 +281,404 @@ async def chat_proxy(request: Request):
|
|
| 274 |
return resp_json
|
| 275 |
except Exception as e:
|
| 276 |
return JSONResponse(status_code=500, content={"error": str(e)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
.stat-label { font-size: 0.75rem; color: #94a3b8; font-weight: 600; text-transform: uppercase; }
|
| 63 |
.stat-value { font-size: 1.5rem; font-weight: 800; color: #38bdf8; margin-top: 5px; }
|
| 64 |
.status-dot { height: 10px; width: 10px; background-color: #10b981; border-radius: 50%; display: inline-block; margin-right: 8px; }
|
| 65 |
+
.nav { display: flex; justify-content: center; gap: 12px; margin-bottom: 16px; }
|
| 66 |
+
.nav a { color: #94a3b8; text-decoration: none; padding: 6px 14px; border-radius: 8px; font-size: 0.8rem; background: #334155; }
|
| 67 |
+
.nav a.active, .nav a:hover { background: #38bdf8; color: #fff; }
|
| 68 |
</style>
|
| 69 |
</head>
|
| 70 |
<body>
|
| 71 |
<div class="container">
|
| 72 |
+
<div class="nav">
|
| 73 |
+
<a href="/" class="active">🧠 Live</a>
|
| 74 |
+
<a href="/analytics">🔬 Analytics</a>
|
| 75 |
+
</div>
|
| 76 |
<p style="text-align: center; font-size: 0.8rem; color: #64748b;"><span class="status-dot"></span> SISTEMA ONLINE</p>
|
| 77 |
<h1>Córtex-Nexus Live</h1>
|
| 78 |
<div class="stats">
|
|
|
|
| 281 |
return resp_json
|
| 282 |
except Exception as e:
|
| 283 |
return JSONResponse(status_code=500, content={"error": str(e)})
|
| 284 |
+
|
| 285 |
+
# ============================================================
|
| 286 |
+
# ANALYTICS API
|
| 287 |
+
# ============================================================
|
| 288 |
+
|
| 289 |
+
import math
|
| 290 |
+
import csv
|
| 291 |
+
import io
|
| 292 |
+
from fastapi.responses import StreamingResponse
|
| 293 |
+
|
| 294 |
+
def _get_snapshots(limit=None, hours=None):
|
| 295 |
+
"""Helper: obtiene snapshots con filtro opcional de tiempo"""
|
| 296 |
+
conn = sqlite3.connect(DB_PATH)
|
| 297 |
+
conn.row_factory = sqlite3.Row
|
| 298 |
+
c = conn.cursor()
|
| 299 |
+
if hours:
|
| 300 |
+
since = (datetime.datetime.utcnow() - datetime.timedelta(hours=hours)).isoformat()
|
| 301 |
+
c.execute("SELECT * FROM snapshots WHERE timestamp > ? ORDER BY id ASC", (since,))
|
| 302 |
+
elif limit:
|
| 303 |
+
c.execute("SELECT * FROM snapshots ORDER BY id DESC LIMIT ?", (limit,))
|
| 304 |
+
else:
|
| 305 |
+
c.execute("SELECT * FROM snapshots ORDER BY id ASC")
|
| 306 |
+
rows = [dict(r) for r in c.fetchall()]
|
| 307 |
+
conn.close()
|
| 308 |
+
return rows if not limit or hours else rows[::-1]
|
| 309 |
+
|
| 310 |
+
def _calc_stats(values):
|
| 311 |
+
"""Calcula media, std, min, max, tendencia de una lista de floats"""
|
| 312 |
+
if not values:
|
| 313 |
+
return {"mean": 0, "std": 0, "min": 0, "max": 0, "trend": 0}
|
| 314 |
+
n = len(values)
|
| 315 |
+
mean = sum(values) / n
|
| 316 |
+
variance = sum((x - mean) ** 2 for x in values) / max(n - 1, 1)
|
| 317 |
+
std = math.sqrt(variance)
|
| 318 |
+
# Tendencia: pendiente de regresión lineal simple
|
| 319 |
+
if n > 1:
|
| 320 |
+
x_mean = (n - 1) / 2
|
| 321 |
+
num = sum((i - x_mean) * (v - mean) for i, v in enumerate(values))
|
| 322 |
+
den = sum((i - x_mean) ** 2 for i in range(n))
|
| 323 |
+
trend = num / den if den != 0 else 0
|
| 324 |
+
else:
|
| 325 |
+
trend = 0
|
| 326 |
+
return {
|
| 327 |
+
"mean": round(mean, 4),
|
| 328 |
+
"std": round(std, 4),
|
| 329 |
+
"min": round(min(values), 4),
|
| 330 |
+
"max": round(max(values), 4),
|
| 331 |
+
"trend": round(trend, 6)
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
def _detect_patterns(rows):
|
| 335 |
+
"""Detecta patrones activos en los datos"""
|
| 336 |
+
patterns = []
|
| 337 |
+
if len(rows) < 5:
|
| 338 |
+
return patterns
|
| 339 |
+
|
| 340 |
+
qualias = [r["qualia"] for r in rows]
|
| 341 |
+
frustraciones = [r["frustracion"] for r in rows]
|
| 342 |
+
fatigas = [r["fatiga"] for r in rows]
|
| 343 |
+
confianzas = [r["autoConfianza"] for r in rows]
|
| 344 |
+
|
| 345 |
+
# Meseta emocional: volatilidad < 0.01 en últimas 50 épocas
|
| 346 |
+
recent_q = qualias[-min(50, len(qualias)):]
|
| 347 |
+
if len(recent_q) > 10:
|
| 348 |
+
vol = _calc_stats(recent_q)["std"]
|
| 349 |
+
if vol < 0.01:
|
| 350 |
+
patterns.append({"type": "MESETA", "severity": "warning", "msg": f"Qualia estancado (vol={vol:.4f}). Necesita estímulos nuevos."})
|
| 351 |
+
|
| 352 |
+
# Ciclo de frustración: frustración sube >3x en ventana de 20
|
| 353 |
+
if len(frustraciones) >= 20:
|
| 354 |
+
inicio = sum(frustraciones[-20:-15]) / 5 if frustraciones[-20:-15] else 0.001
|
| 355 |
+
fin = sum(frustraciones[-5:]) / 5
|
| 356 |
+
if inicio > 0.001 and fin / max(inicio, 0.001) > 3:
|
| 357 |
+
patterns.append({"type": "FRUSTRACION_CICLO", "severity": "danger", "msg": f"Frustración se triplicó en 20 épocas ({inicio:.3f}→{fin:.3f})"})
|
| 358 |
+
|
| 359 |
+
# Wireheading: qualia sube monótonamente sin caídas
|
| 360 |
+
if len(qualias) >= 20:
|
| 361 |
+
increases = sum(1 for i in range(1, min(20, len(qualias))) if qualias[-i] >= qualias[-i-1])
|
| 362 |
+
if increases >= 18:
|
| 363 |
+
patterns.append({"type": "WIREHEADING", "severity": "danger", "msg": "Qualia sube sin parar. Anti-wireheading podría estar fallando."})
|
| 364 |
+
|
| 365 |
+
# Fatiga terminal: fatiga > 0.8 sostenida
|
| 366 |
+
if len(fatigas) >= 10:
|
| 367 |
+
high_fatigue = sum(1 for f in fatigas[-10:] if f > 0.8)
|
| 368 |
+
if high_fatigue >= 8:
|
| 369 |
+
patterns.append({"type": "FATIGA_TERMINAL", "severity": "danger", "msg": "Fatiga crítica. El sistema necesita 'dormir' (consolidación)."})
|
| 370 |
+
|
| 371 |
+
# Personalidad emergente (positivo): drift > 5% en confianza
|
| 372 |
+
if len(confianzas) >= 50:
|
| 373 |
+
inicio_c = sum(confianzas[:10]) / 10
|
| 374 |
+
fin_c = sum(confianzas[-10:]) / 10
|
| 375 |
+
drift = abs(fin_c - inicio_c) / max(inicio_c, 0.001)
|
| 376 |
+
if drift > 0.05:
|
| 377 |
+
direction = "↑" if fin_c > inicio_c else "↓"
|
| 378 |
+
patterns.append({"type": "PERSONALIDAD_EMERGENTE", "severity": "success", "msg": f"Drift de confianza {direction} ({inicio_c:.3f}→{fin_c:.3f}, {drift*100:.1f}%)"})
|
| 379 |
+
|
| 380 |
+
return patterns
|
| 381 |
+
|
| 382 |
+
@app.get("/api/analytics")
|
| 383 |
+
async def get_analytics():
|
| 384 |
+
"""Métricas derivadas con ventanas temporales"""
|
| 385 |
+
try:
|
| 386 |
+
windows = {}
|
| 387 |
+
for label, hours in [("1h", 1), ("6h", 6), ("24h", 24), ("7d", 168)]:
|
| 388 |
+
rows = _get_snapshots(hours=hours)
|
| 389 |
+
if not rows:
|
| 390 |
+
windows[label] = {"count": 0, "metrics": {}}
|
| 391 |
+
continue
|
| 392 |
+
|
| 393 |
+
metrics = {}
|
| 394 |
+
for col in ["qualia", "frustracion", "autoConfianza", "fatiga", "aburrimiento"]:
|
| 395 |
+
vals = [r.get(col, 0) for r in rows]
|
| 396 |
+
metrics[col] = _calc_stats(vals)
|
| 397 |
+
|
| 398 |
+
# Métricas derivadas
|
| 399 |
+
qualias = [r.get("qualia", 0) for r in rows]
|
| 400 |
+
fatigas = [r.get("fatiga", 0) for r in rows]
|
| 401 |
+
ratios = [q / max(f, 0.01) for q, f in zip(qualias, fatigas)]
|
| 402 |
+
metrics["ratio_qualia_fatiga"] = _calc_stats(ratios)
|
| 403 |
+
|
| 404 |
+
# Entropía emocional (Shannon)
|
| 405 |
+
entropies = []
|
| 406 |
+
for r in rows:
|
| 407 |
+
vec = [max(r.get(k, 0.001), 0.001) for k in ["qualia", "frustracion", "autoConfianza", "fatiga", "aburrimiento"]]
|
| 408 |
+
total = sum(vec)
|
| 409 |
+
probs = [v / total for v in vec]
|
| 410 |
+
entropy = -sum(p * math.log2(p) for p in probs if p > 0)
|
| 411 |
+
entropies.append(entropy)
|
| 412 |
+
metrics["entropia_emocional"] = _calc_stats(entropies)
|
| 413 |
+
|
| 414 |
+
windows[label] = {"count": len(rows), "metrics": metrics}
|
| 415 |
+
|
| 416 |
+
# Patrones activos
|
| 417 |
+
all_rows = _get_snapshots(hours=24)
|
| 418 |
+
patterns = _detect_patterns(all_rows)
|
| 419 |
+
|
| 420 |
+
# Arquetipo actual
|
| 421 |
+
arquetipo = {}
|
| 422 |
+
if vector_global and vector_global.memoria:
|
| 423 |
+
arquetipo = vector_global.memoria.cargar_meta_vector()
|
| 424 |
+
|
| 425 |
+
return {
|
| 426 |
+
"windows": windows,
|
| 427 |
+
"patterns": patterns,
|
| 428 |
+
"arquetipo": arquetipo,
|
| 429 |
+
"timestamp": datetime.datetime.utcnow().isoformat()
|
| 430 |
+
}
|
| 431 |
+
except Exception as e:
|
| 432 |
+
return {"error": str(e)}
|
| 433 |
+
|
| 434 |
+
@app.get("/api/export")
|
| 435 |
+
async def export_csv():
|
| 436 |
+
"""Exporta todos los snapshots como CSV descargable"""
|
| 437 |
+
try:
|
| 438 |
+
rows = _get_snapshots()
|
| 439 |
+
output = io.StringIO()
|
| 440 |
+
writer = csv.writer(output)
|
| 441 |
+
writer.writerow(["id", "timestamp", "qualia", "satisfaccion", "frustracion", "aburrimiento", "autoConfianza", "fatiga", "pregunta", "respuesta", "novedad", "complejidad"])
|
| 442 |
+
|
| 443 |
+
for r in rows:
|
| 444 |
+
meta = {}
|
| 445 |
+
try:
|
| 446 |
+
meta = json.loads(r.get("metadata", "{}"))
|
| 447 |
+
except:
|
| 448 |
+
pass
|
| 449 |
+
writer.writerow([
|
| 450 |
+
r.get("id", ""), r.get("timestamp", ""),
|
| 451 |
+
r.get("qualia", 0), r.get("satisfaccion", 0),
|
| 452 |
+
r.get("frustracion", 0), r.get("aburrimiento", 0),
|
| 453 |
+
r.get("autoConfianza", 0), r.get("fatiga", 0),
|
| 454 |
+
meta.get("pregunta", ""), meta.get("respuesta", ""),
|
| 455 |
+
meta.get("novedad", ""), meta.get("complejidad", "")
|
| 456 |
+
])
|
| 457 |
+
|
| 458 |
+
output.seek(0)
|
| 459 |
+
return StreamingResponse(
|
| 460 |
+
iter([output.getvalue()]),
|
| 461 |
+
media_type="text/csv",
|
| 462 |
+
headers={"Content-Disposition": f"attachment; filename=cortex_nexus_export_{datetime.datetime.utcnow().strftime('%Y%m%d_%H%M')}.csv"}
|
| 463 |
+
)
|
| 464 |
+
except Exception as e:
|
| 465 |
+
return JSONResponse(status_code=500, content={"error": str(e)})
|
| 466 |
+
|
| 467 |
+
@app.post("/api/sleep")
|
| 468 |
+
async def sleep_consolidation():
|
| 469 |
+
"""Ejecuta consolidación nocturna (Sleep Replay) del arquetipo"""
|
| 470 |
+
try:
|
| 471 |
+
if not vector_global or not vector_global.memoria:
|
| 472 |
+
return {"error": "Vector no inicializado"}
|
| 473 |
+
|
| 474 |
+
# Capturar arquetipo ANTES
|
| 475 |
+
antes = vector_global.memoria.cargar_meta_vector()
|
| 476 |
+
|
| 477 |
+
# Ejecutar consolidación EMA
|
| 478 |
+
vector_global.consolidar_memoria()
|
| 479 |
+
|
| 480 |
+
# Capturar arquetipo DESPUÉS
|
| 481 |
+
despues = vector_global.memoria.cargar_meta_vector()
|
| 482 |
+
|
| 483 |
+
drift = {
|
| 484 |
+
k: round(despues.get(k, 0) - antes.get(k, 0), 6)
|
| 485 |
+
for k in antes
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
return {
|
| 489 |
+
"status": "Consolidación completada",
|
| 490 |
+
"arquetipo_antes": antes,
|
| 491 |
+
"arquetipo_despues": despues,
|
| 492 |
+
"drift": drift,
|
| 493 |
+
"timestamp": datetime.datetime.utcnow().isoformat()
|
| 494 |
+
}
|
| 495 |
+
except Exception as e:
|
| 496 |
+
return {"error": str(e)}
|
| 497 |
+
|
| 498 |
+
# ============================================================
|
| 499 |
+
# ANALYTICS DASHBOARD
|
| 500 |
+
# ============================================================
|
| 501 |
+
|
| 502 |
+
@app.get("/analytics", response_class=HTMLResponse)
|
| 503 |
+
async def analytics_dashboard():
|
| 504 |
+
html = """
|
| 505 |
+
<!DOCTYPE html>
|
| 506 |
+
<html lang="es">
|
| 507 |
+
<head>
|
| 508 |
+
<meta charset="UTF-8">
|
| 509 |
+
<title>Cortex-Nexus | Analytics Lab</title>
|
| 510 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 511 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 512 |
+
<style>
|
| 513 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 514 |
+
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f8fafc; padding: 16px; }
|
| 515 |
+
.container { max-width: 900px; margin: 0 auto; }
|
| 516 |
+
.card { background: #1e293b; padding: 16px; border-radius: 12px; margin-bottom: 16px; border: 1px solid #334155; }
|
| 517 |
+
h1 { color: #a78bfa; text-align: center; font-size: 1.5rem; margin-bottom: 16px; }
|
| 518 |
+
h2 { color: #38bdf8; font-size: 1rem; margin-bottom: 10px; }
|
| 519 |
+
.nav { display: flex; justify-content: center; gap: 12px; margin-bottom: 16px; }
|
| 520 |
+
.nav a { color: #94a3b8; text-decoration: none; padding: 6px 14px; border-radius: 8px; font-size: 0.8rem; background: #334155; }
|
| 521 |
+
.nav a.active, .nav a:hover { background: #a78bfa; color: #fff; }
|
| 522 |
+
.window-selector { display: flex; gap: 8px; margin-bottom: 12px; }
|
| 523 |
+
.window-btn { padding: 6px 12px; border: 1px solid #475569; background: #334155; color: #94a3b8; border-radius: 8px; cursor: pointer; font-size: 0.75rem; }
|
| 524 |
+
.window-btn.active { background: #a78bfa; color: #fff; border-color: #a78bfa; }
|
| 525 |
+
.metrics-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
| 526 |
+
.metric { background: #334155; padding: 10px; border-radius: 8px; text-align: center; }
|
| 527 |
+
.metric-label { font-size: 0.65rem; color: #94a3b8; text-transform: uppercase; }
|
| 528 |
+
.metric-value { font-size: 1.1rem; font-weight: 800; color: #38bdf8; margin-top: 4px; }
|
| 529 |
+
.metric-trend { font-size: 0.6rem; margin-top: 2px; }
|
| 530 |
+
.trend-up { color: #10b981; }
|
| 531 |
+
.trend-down { color: #ef4444; }
|
| 532 |
+
.trend-flat { color: #94a3b8; }
|
| 533 |
+
.pattern { padding: 8px 12px; border-radius: 8px; margin-bottom: 8px; font-size: 0.75rem; }
|
| 534 |
+
.pattern-warning { background: rgba(245,158,11,0.15); border-left: 3px solid #f59e0b; color: #fbbf24; }
|
| 535 |
+
.pattern-danger { background: rgba(239,68,68,0.15); border-left: 3px solid #ef4444; color: #f87171; }
|
| 536 |
+
.pattern-success { background: rgba(16,185,129,0.15); border-left: 3px solid #10b981; color: #34d399; }
|
| 537 |
+
.arquetipo { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
| 538 |
+
.arq-item { text-align: center; }
|
| 539 |
+
.arq-label { font-size: 0.65rem; color: #94a3b8; }
|
| 540 |
+
.arq-value { font-size: 1.2rem; font-weight: 800; color: #a78bfa; }
|
| 541 |
+
.export-btn { display: block; width: 100%; padding: 12px; background: #a78bfa; color: #fff; border: none; border-radius: 8px; font-size: 0.9rem; cursor: pointer; font-weight: 600; }
|
| 542 |
+
.export-btn:hover { background: #8b5cf6; }
|
| 543 |
+
.sleep-btn { display: block; width: 100%; padding: 12px; background: #1e293b; color: #a78bfa; border: 2px solid #a78bfa; border-radius: 8px; font-size: 0.9rem; cursor: pointer; font-weight: 600; margin-top: 8px; }
|
| 544 |
+
.sleep-btn:hover { background: #a78bfa; color: #fff; }
|
| 545 |
+
</style>
|
| 546 |
+
</head>
|
| 547 |
+
<body>
|
| 548 |
+
<div class="container">
|
| 549 |
+
<div class="nav">
|
| 550 |
+
<a href="/">🧠 Live</a>
|
| 551 |
+
<a href="/analytics" class="active">🔬 Analytics</a>
|
| 552 |
+
</div>
|
| 553 |
+
<h1>🔬 Analytics Lab</h1>
|
| 554 |
+
|
| 555 |
+
<div class="window-selector">
|
| 556 |
+
<button class="window-btn active" onclick="setWindow('1h')">1H</button>
|
| 557 |
+
<button class="window-btn" onclick="setWindow('6h')">6H</button>
|
| 558 |
+
<button class="window-btn" onclick="setWindow('24h')">24H</button>
|
| 559 |
+
<button class="window-btn" onclick="setWindow('7d')">7D</button>
|
| 560 |
+
</div>
|
| 561 |
+
|
| 562 |
+
<div class="card">
|
| 563 |
+
<h2>📊 Métricas Derivadas <span id="window-label" style="color:#94a3b8; font-size:0.7rem;">(1h)</span></h2>
|
| 564 |
+
<div class="metrics-grid" id="metrics-grid">Cargando...</div>
|
| 565 |
+
</div>
|
| 566 |
+
|
| 567 |
+
<div class="card">
|
| 568 |
+
<h2>⚡ Volatilidad Emocional</h2>
|
| 569 |
+
<canvas id="volChart" height="180"></canvas>
|
| 570 |
+
</div>
|
| 571 |
+
|
| 572 |
+
<div class="card">
|
| 573 |
+
<h2>🚨 Patrones Detectados</h2>
|
| 574 |
+
<div id="patterns-box">Analizando...</div>
|
| 575 |
+
</div>
|
| 576 |
+
|
| 577 |
+
<div class="card">
|
| 578 |
+
<h2>🧬 Arquetipo (Personalidad Base)</h2>
|
| 579 |
+
<div class="arquetipo" id="arquetipo-box">Cargando...</div>
|
| 580 |
+
</div>
|
| 581 |
+
|
| 582 |
+
<div class="card">
|
| 583 |
+
<a href="/api/export" class="export-btn">📥 Exportar CSV Completo</a>
|
| 584 |
+
<button class="sleep-btn" onclick="triggerSleep()">😴 Ejecutar Consolidación (Sleep Replay)</button>
|
| 585 |
+
<div id="sleep-result" style="font-size:0.7rem; color:#94a3b8; margin-top:8px;"></div>
|
| 586 |
+
</div>
|
| 587 |
+
|
| 588 |
+
<p style="text-align:center; font-size:0.65rem; color:#475569; margin-top:12px;">Arquitectura Emocional v3.0 • Analytics Lab • Maxi Speranza</p>
|
| 589 |
+
</div>
|
| 590 |
+
|
| 591 |
+
<script>
|
| 592 |
+
let currentWindow = '1h';
|
| 593 |
+
|
| 594 |
+
const volCtx = document.getElementById('volChart').getContext('2d');
|
| 595 |
+
const volChart = new Chart(volCtx, {
|
| 596 |
+
type: 'bar',
|
| 597 |
+
data: {
|
| 598 |
+
labels: ['Qualia', 'Frustración', 'Confianza', 'Fatiga', 'Aburrimiento'],
|
| 599 |
+
datasets: [{
|
| 600 |
+
label: 'Volatilidad (σ)',
|
| 601 |
+
data: [0, 0, 0, 0, 0],
|
| 602 |
+
backgroundColor: ['rgba(56,189,248,0.6)', 'rgba(239,68,68,0.6)', 'rgba(16,185,129,0.6)', 'rgba(245,158,11,0.6)', 'rgba(167,139,250,0.6)'],
|
| 603 |
+
borderRadius: 6
|
| 604 |
+
}]
|
| 605 |
+
},
|
| 606 |
+
options: {
|
| 607 |
+
responsive: true,
|
| 608 |
+
scales: { y: { beginAtZero: true, grid: { color: '#334155' }, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } },
|
| 609 |
+
plugins: { legend: { display: false } }
|
| 610 |
+
}
|
| 611 |
+
});
|
| 612 |
+
|
| 613 |
+
function setWindow(w) {
|
| 614 |
+
currentWindow = w;
|
| 615 |
+
document.querySelectorAll('.window-btn').forEach(b => b.classList.remove('active'));
|
| 616 |
+
event.target.classList.add('active');
|
| 617 |
+
loadAnalytics();
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
function trendIcon(trend) {
|
| 621 |
+
if (trend > 0.001) return '<span class="trend-up">▲ +' + trend.toFixed(4) + '</span>';
|
| 622 |
+
if (trend < -0.001) return '<span class="trend-down">▼ ' + trend.toFixed(4) + '</span>';
|
| 623 |
+
return '<span class="trend-flat">— estable</span>';
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
async function loadAnalytics() {
|
| 627 |
+
try {
|
| 628 |
+
const res = await fetch('/api/analytics');
|
| 629 |
+
const data = await res.json();
|
| 630 |
+
if (data.error) { console.error(data.error); return; }
|
| 631 |
+
|
| 632 |
+
const w = data.windows[currentWindow];
|
| 633 |
+
document.getElementById('window-label').innerText = `(${currentWindow} • ${w.count} épocas)`;
|
| 634 |
+
|
| 635 |
+
const grid = document.getElementById('metrics-grid');
|
| 636 |
+
if (w.count === 0) { grid.innerHTML = '<p style="color:#94a3b8;">Sin datos para esta ventana</p>'; return; }
|
| 637 |
+
|
| 638 |
+
const m = w.metrics;
|
| 639 |
+
const names = {qualia: 'Qualia', frustracion: 'Frustración', autoConfianza: 'Confianza', fatiga: 'Fatiga', aburrimiento: 'Aburrimiento', ratio_qualia_fatiga: 'Q/Fatiga', entropia_emocional: 'Entropía'};
|
| 640 |
+
grid.innerHTML = Object.entries(names).map(([k, label]) => {
|
| 641 |
+
const s = m[k] || {};
|
| 642 |
+
return `<div class="metric"><div class="metric-label">${label}</div><div class="metric-value">${(s.mean||0).toFixed(3)}</div><div class="metric-trend">${trendIcon(s.trend||0)}</div><div style="font-size:0.55rem;color:#64748b;">σ=${(s.std||0).toFixed(3)} [${(s.min||0).toFixed(2)}-${(s.max||0).toFixed(2)}]</div></div>`;
|
| 643 |
+
}).join('');
|
| 644 |
+
|
| 645 |
+
// Volatilidad
|
| 646 |
+
volChart.data.datasets[0].data = ['qualia','frustracion','autoConfianza','fatiga','aburrimiento'].map(k => (m[k]||{}).std || 0);
|
| 647 |
+
volChart.update('none');
|
| 648 |
+
|
| 649 |
+
// Patrones
|
| 650 |
+
const pBox = document.getElementById('patterns-box');
|
| 651 |
+
if (data.patterns.length === 0) {
|
| 652 |
+
pBox.innerHTML = '<p style="color:#10b981; font-size:0.8rem;">✅ Sin anomalías detectadas</p>';
|
| 653 |
+
} else {
|
| 654 |
+
pBox.innerHTML = data.patterns.map(p => `<div class="pattern pattern-${p.severity}">${p.msg}</div>`).join('');
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
// Arquetipo
|
| 658 |
+
const aBox = document.getElementById('arquetipo-box');
|
| 659 |
+
const a = data.arquetipo;
|
| 660 |
+
aBox.innerHTML = `
|
| 661 |
+
<div class="arq-item"><div class="arq-label">Meta-Qualia</div><div class="arq-value">${(a.meta_qualia||0).toFixed(4)}</div></div>
|
| 662 |
+
<div class="arq-item"><div class="arq-label">Meta-Frustración</div><div class="arq-value">${(a.meta_frustracion||0).toFixed(4)}</div></div>
|
| 663 |
+
<div class="arq-item"><div class="arq-label">Meta-Confianza</div><div class="arq-value">${(a.meta_confianza||0).toFixed(4)}</div></div>
|
| 664 |
+
`;
|
| 665 |
+
} catch(e) { console.error("Error analytics:", e); }
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
async function triggerSleep() {
|
| 669 |
+
const res = await fetch('/api/sleep', {method: 'POST'});
|
| 670 |
+
const data = await res.json();
|
| 671 |
+
const box = document.getElementById('sleep-result');
|
| 672 |
+
if (data.error) { box.innerHTML = '❌ ' + data.error; return; }
|
| 673 |
+
box.innerHTML = `✅ Consolidación OK | Drift: Q=${data.drift.meta_qualia}, F=${data.drift.meta_frustracion}, C=${data.drift.meta_confianza}`;
|
| 674 |
+
loadAnalytics();
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
setInterval(loadAnalytics, 8000);
|
| 678 |
+
loadAnalytics();
|
| 679 |
+
</script>
|
| 680 |
+
</body>
|
| 681 |
+
</html>
|
| 682 |
+
"""
|
| 683 |
+
return HTMLResponse(content=html)
|
| 684 |
+
|