Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6af9046ba | ||
|
|
655a4de5e9 | ||
|
|
5c73521685 |
34
.env.example
|
|
@ -1,34 +0,0 @@
|
|||
# ── MongoDB ────────────────────────────────────────────────────────────────
|
||||
MONGO_URL=mongodb://localhost:27017
|
||||
DB_NAME=FLUJOS_DATOS
|
||||
|
||||
# ── API de visualización (FLUJOS/BACK_BACK/FLUJOS_APP.js) ──────────────────
|
||||
PORT=3000
|
||||
# Colección de aristas que alimenta el grafo: `comparaciones` (solape de
|
||||
# palabras, histórica) o `comparaciones_v2` (motor semántico).
|
||||
COMPARACIONES_COL=comparaciones
|
||||
# Token del panel /admin. Si no se define, se lee de FLUJOS_DATOS/.admin_token
|
||||
# ADMIN_TOKEN=
|
||||
|
||||
# ── Rutas (por defecto se deducen de la ubicación del código) ──────────────
|
||||
# Envíos del buzón de periodistas. Debe quedar FUERA del webroot.
|
||||
# SUBMISSIONS_DIR=
|
||||
# Imágenes de Wikipedia ya scrapeadas, servidas en /wiki-images
|
||||
# WIKI_IMAGES_DIR=
|
||||
# Raíz de FLUJOS_DATOS para los scripts de scraping y comparación
|
||||
# FLUJOS_DATOS_DIR=
|
||||
|
||||
# ── API de noticias (coconews/api/server.js) ───────────────────────────────
|
||||
COCONEWS_PORT=3100
|
||||
|
||||
# ── Ingestor RSS (coconews/workers/ingestor.py) ────────────────────────────
|
||||
INGESTOR_SLEEP=900
|
||||
INGESTOR_WORKERS=20
|
||||
INGESTOR_FEED_TIMEOUT=20
|
||||
INGESTOR_BACKFILL_IMG=1
|
||||
INGESTOR_BACKFILL_DIAS=90
|
||||
INGESTOR_BACKFILL_LOTE=300
|
||||
|
||||
# ── Análisis de imágenes (FLUJOS_DATOS/IMAGENES/image_analyzer.py) ─────────
|
||||
VISION_MODEL=Qwen/Qwen3-VL-8B-Instruct
|
||||
# HF_HOME=
|
||||
97
.gitignore
vendored
|
|
@ -1,97 +0,0 @@
|
|||
# ========================
|
||||
# SECRETOS — NUNCA SUBIR
|
||||
# ========================
|
||||
# Token del panel de administración (lo lee FLUJOS_APP.js como fallback de ADMIN_TOKEN)
|
||||
FLUJOS_DATOS/.admin_token
|
||||
# Buzón de envíos de periodistas: material filtrado, no toca este repo jamás
|
||||
FLUJOS_DATOS/SUBMISSIONS/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# ========================
|
||||
# DATOS PESADOS - NO SUBIR
|
||||
# ========================
|
||||
|
||||
# MongoDB
|
||||
FLUJOS_DATOS/MONGO/
|
||||
|
||||
# Modelo Qwen3-VL (~16GB)
|
||||
FLUJOS_DATOS/IMAGENES/model_cache/
|
||||
|
||||
# Imágenes scrapeadas y JSONs de output
|
||||
FLUJOS_DATOS/IMAGENES/output/
|
||||
|
||||
# Datos scrapeados - solo código, no datos
|
||||
FLUJOS_DATOS/NOTICIAS/archivos/
|
||||
FLUJOS_DATOS/NOTICIAS/articulos/
|
||||
FLUJOS_DATOS/NOTICIAS/tokenized/
|
||||
FLUJOS_DATOS/NOTICIAS/noticias_procesadas.txt
|
||||
FLUJOS_DATOS/NOTICIAS/processed_articles.txt
|
||||
FLUJOS_DATOS/WIKIPEDIA/articulos_wikipedia/
|
||||
FLUJOS_DATOS/WIKIPEDIA/articulos_tokenizados/
|
||||
FLUJOS_DATOS/TORRENTS/
|
||||
articulos_wikipedia/
|
||||
|
||||
# Entornos virtuales Python
|
||||
FLUJOS_DATOS/myenv/
|
||||
myenv/
|
||||
venv/
|
||||
env/
|
||||
.venv/
|
||||
|
||||
# NLTK data
|
||||
nltk_data/
|
||||
|
||||
# Bases de datos
|
||||
*.sqlite3
|
||||
*.db
|
||||
|
||||
# ========================
|
||||
# DEPENDENCIAS NODE
|
||||
# ========================
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# ========================
|
||||
# PYTHON
|
||||
# ========================
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.egg-info/
|
||||
|
||||
# ========================
|
||||
# TEMPORALES Y BACKUPS
|
||||
# ========================
|
||||
*.save
|
||||
*.bak
|
||||
# `*.bak` no casa con `fichero.js.bak-20260823`
|
||||
*.bak-*
|
||||
*_COPIA*
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# ========================
|
||||
# LOGS
|
||||
# ========================
|
||||
logs/
|
||||
*.log
|
||||
# `*.log` no casa con los rotados `pipeline_mongolo.log.1`
|
||||
*.log.*
|
||||
npm-debug.log*
|
||||
|
||||
# ========================
|
||||
# HERRAMIENTAS Y EDITORES
|
||||
# ========================
|
||||
.vscode/
|
||||
.idea/
|
||||
.claude/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Parcel cache
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
63
FLUJOS/.gitignore
vendored
|
|
@ -1,63 +0,0 @@
|
|||
# Dependencias
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# Variables de entorno y secretos
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.eggs/
|
||||
|
||||
# Entorno virtual Python
|
||||
venv/
|
||||
env/
|
||||
myenv/
|
||||
.venv/
|
||||
|
||||
# Bases de datos locales
|
||||
*.sqlite3
|
||||
*.db
|
||||
|
||||
# Archivos de backup y temporales
|
||||
*.save
|
||||
*.bak
|
||||
*_COPIA*
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Parcel bundler
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
|
||||
# Datos escrapeados y pesados (si se colaran dentro de FLUJOS)
|
||||
NOTICIAS/
|
||||
WIKIPEDIA/
|
||||
TORRENTS/
|
||||
MONGO/
|
||||
archivos/
|
||||
articulos/
|
||||
tokenized/
|
||||
articulos_wikipedia/
|
||||
articulos_tokenizados/
|
||||
|
|
@ -1,889 +0,0 @@
|
|||
// FLUJOS_APP.js ubicado en /var/www/flujos/FLUJOS/BACK_BACK/
|
||||
|
||||
require('dotenv').config(); // Cargar variables de entorno desde .env
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const bodyParser = require('body-parser');
|
||||
const helmet = require('helmet');
|
||||
const multer = require('multer');
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// Colección de comparaciones servida al grafo (configurable para conmutar al motor semántico v2)
|
||||
const COMPARACIONES_COL = process.env.COMPARACIONES_COL || 'comparaciones';
|
||||
|
||||
// --- Buzón seguro de colaboradores/periodistas ---------------------------
|
||||
// Los envíos se guardan FUERA del webroot (nunca se sirven como estáticos).
|
||||
const SUBMISSIONS_DIR = process.env.SUBMISSIONS_DIR
|
||||
|| path.join(__dirname, '../../FLUJOS_DATOS/SUBMISSIONS');
|
||||
|
||||
// Imagenes de Wikipedia ya scrapeadas; se sirven en /wiki-images
|
||||
const WIKI_IMAGES_DIR = process.env.WIKI_IMAGES_DIR
|
||||
|| path.join(__dirname, '../../FLUJOS_DATOS/IMAGENES/output/wiki_images');
|
||||
try { fs.mkdirSync(SUBMISSIONS_DIR, { recursive: true, mode: 0o700 }); } catch (e) { /* noop */ }
|
||||
|
||||
const submitStorage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, SUBMISSIONS_DIR),
|
||||
filename: (req, file, cb) => {
|
||||
const safe = (file.originalname || 'archivo')
|
||||
.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 80);
|
||||
cb(null, `${Date.now()}_${Math.random().toString(36).slice(2, 8)}_${safe}`);
|
||||
},
|
||||
});
|
||||
const submitUpload = multer({
|
||||
storage: submitStorage,
|
||||
limits: { fileSize: 50 * 1024 * 1024, files: 8 }, // 50 MB/archivo, 8 archivos
|
||||
});
|
||||
|
||||
// Rate-limit sencillo en memoria por IP para el buzón (10 envíos/hora)
|
||||
const submitHits = new Map();
|
||||
function submitRateLimit(req, res, next) {
|
||||
const ip = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
||||
|| req.socket.remoteAddress || 'desconocida';
|
||||
const now = Date.now();
|
||||
const windowMs = 60 * 60 * 1000;
|
||||
const arr = (submitHits.get(ip) || []).filter((t) => now - t < windowMs);
|
||||
if (arr.length >= 10) {
|
||||
return res.status(429).json({ ok: false, error: 'Demasiados envíos desde tu conexión. Inténtalo más tarde.' });
|
||||
}
|
||||
arr.push(now);
|
||||
submitHits.set(ip, arr);
|
||||
next();
|
||||
}
|
||||
|
||||
// Helmet completo: X-Powered-By off, XSS protection, nosniff, frameguard, HSTS, etc.
|
||||
app.use(helmet());
|
||||
|
||||
// CSP personalizado encima del helmet por defecto
|
||||
app.use(
|
||||
helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net', 'https://esm.sh', 'https://unpkg.com', 'https://cdnjs.cloudflare.com'],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
||||
imgSrc: ["'self'", 'data:', 'blob:'],
|
||||
connectSrc: ["'self'", 'https://esm.sh'],
|
||||
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
|
||||
frameAncestors: ["'none'"],
|
||||
objectSrc: ["'none'"],
|
||||
baseUri: ["'self'"],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Prevenir CSRF: solo aceptar peticiones GET a /api desde el mismo origen
|
||||
const ORIGENES_PERMITIDOS = [
|
||||
'https://upleaks.org',
|
||||
'https://www.upleaks.org',
|
||||
`http://localhost:${port}`,
|
||||
];
|
||||
app.use('/api/', (req, res, next) => {
|
||||
const origin = req.headers.origin;
|
||||
const referer = req.headers.referer;
|
||||
// En producción detrás de nginx no hay Origin en peticiones same-site normales,
|
||||
// pero sí lo habría en peticiones cross-origin maliciosas
|
||||
if (origin && !ORIGENES_PERMITIDOS.includes(origin)) {
|
||||
return res.status(403).json({ error: 'Origen no permitido' });
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Añadir cabecera anti-clickjacking y anti-MIME-sniffing explícitas
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(bodyParser.json({ limit: '10kb' }));
|
||||
|
||||
// **Definir primero las rutas de la API**
|
||||
|
||||
// --- Proxy de imagenes para las miniaturas del grafo ----------------------
|
||||
// Sirve same-origin las og:image remotas de las noticias: WebGL/TextureLoader
|
||||
// no puede texturizar imagenes cross-origin sin CORS. Endpoint PUBLICO, asi que
|
||||
// lleva proteccion anti-SSRF (solo http(s), nunca IPs privadas/loopback).
|
||||
const _dns = require('dns').promises;
|
||||
const _net = require('net');
|
||||
function _ipPrivada(ip) {
|
||||
if (_net.isIPv4(ip)) {
|
||||
const p = ip.split('.').map(Number);
|
||||
return p[0] === 10 || p[0] === 127 || p[0] === 0
|
||||
|| (p[0] === 169 && p[1] === 254)
|
||||
|| (p[0] === 172 && p[1] >= 16 && p[1] <= 31)
|
||||
|| (p[0] === 192 && p[1] === 168);
|
||||
}
|
||||
if (_net.isIPv6(ip)) {
|
||||
const l = ip.toLowerCase();
|
||||
return l === '::1' || l.startsWith('fc') || l.startsWith('fd')
|
||||
|| l.startsWith('fe80') || l.startsWith('::ffff:');
|
||||
}
|
||||
return true; // desconocido -> bloquear
|
||||
}
|
||||
app.get('/api/img', async (req, res) => {
|
||||
try {
|
||||
const raw = req.query.u;
|
||||
if (!raw || typeof raw !== 'string' || raw.length > 2048) return res.status(400).end();
|
||||
let u;
|
||||
try { u = new URL(raw); } catch (e) { return res.status(400).end(); }
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') return res.status(400).end();
|
||||
let dir;
|
||||
try { dir = await _dns.lookup(u.hostname); } catch (e) { return res.status(502).end(); }
|
||||
if (_ipPrivada(dir.address)) return res.status(403).end();
|
||||
const ctrl = new AbortController();
|
||||
const t = setTimeout(() => ctrl.abort(), 6000);
|
||||
let r;
|
||||
try {
|
||||
r = await fetch(u.href, { signal: ctrl.signal, redirect: 'follow',
|
||||
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; upleaks-img/1.0)' } });
|
||||
} finally { clearTimeout(t); }
|
||||
const ct = r.headers.get('content-type') || '';
|
||||
if (!r.ok || !ct.startsWith('image/')) return res.status(404).end();
|
||||
const buf = Buffer.from(await r.arrayBuffer());
|
||||
if (buf.length > 8 * 1024 * 1024) return res.status(413).end();
|
||||
res.set('Content-Type', ct);
|
||||
res.set('Cache-Control', 'public, max-age=86400');
|
||||
return res.end(buf);
|
||||
} catch (e) {
|
||||
return res.status(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/data', async (req, res) => {
|
||||
console.log('Solicitud recibida en /api/data');
|
||||
try {
|
||||
// Asegurarse de que la conexión a la base de datos está establecida
|
||||
if (!db) {
|
||||
res.status(500).json({ error: 'No hay conexión a la base de datos' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Acceder a las colecciones necesarias
|
||||
const wikipediaCollection = db.collection('wikipedia');
|
||||
const noticiasCollection = db.collection('noticias');
|
||||
const torrentsCollection = db.collection('torrents');
|
||||
const imagenesCollection = db.collection('imagenes'); // analizadas por Qwen
|
||||
const imagenesWikiCollection = db.collection('imagenes_wiki'); // scrapeadas (fallback)
|
||||
const comparacionesCollection = db.collection(COMPARACIONES_COL);
|
||||
|
||||
// ── Sanitización y validación de parámetros ──────────────────────────────
|
||||
// Rechaza cualquier valor que no sea string (bloquea operator injection)
|
||||
function sanitizeParam(val) {
|
||||
if (typeof val !== 'string') return undefined;
|
||||
return val.trim();
|
||||
}
|
||||
|
||||
// Lista blanca de temas válidos (evita escaneo libre de la BD)
|
||||
const TEMAS_VALIDOS = new Set([
|
||||
'guerra global', 'inteligencia y seguridad', 'cambio climático',
|
||||
'demografía y sociedad', 'economía y corporaciones', 'otros',
|
||||
'geopolítica conflictos', 'seguridad internacional espionaje',
|
||||
'libertad de prensa periodismo', 'corporaciones poder económico',
|
||||
'populismo extremismo', 'desinformación redes sociales',
|
||||
'privacidad vigilancia masiva', 'biodiversidad medioambiente',
|
||||
'inteligencia artificial tecnología',
|
||||
]);
|
||||
|
||||
const temaRaw = sanitizeParam(req.query.tema);
|
||||
const subtematica = sanitizeParam(req.query.subtematica);
|
||||
const palabraClave = sanitizeParam(req.query.palabraClave);
|
||||
const fechaInicio = sanitizeParam(req.query.fechaInicio);
|
||||
const fechaFin = sanitizeParam(req.query.fechaFin);
|
||||
const nodos = sanitizeParam(req.query.nodos);
|
||||
const complejidad = sanitizeParam(req.query.complejidad);
|
||||
|
||||
// Validar tema contra lista blanca
|
||||
const tema = temaRaw && TEMAS_VALIDOS.has(temaRaw) ? temaRaw : null;
|
||||
if (!tema) {
|
||||
res.status(400).json({ error: 'El parámetro "tema" es obligatorio o no válido' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar subtematica: solo letras, números, espacios y guiones (máx 80 chars)
|
||||
if (subtematica && !/^[\w\s\-áéíóúñüÁÉÍÓÚÑÜ]{1,80}$/.test(subtematica)) {
|
||||
res.status(400).json({ error: 'subtematica no válida' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar palabraClave: máx 100 chars, escapar metacaracteres regex
|
||||
let regexKw = null;
|
||||
if (palabraClave) {
|
||||
if (palabraClave.length > 100) {
|
||||
res.status(400).json({ error: 'palabraClave demasiado larga' });
|
||||
return;
|
||||
}
|
||||
regexKw = palabraClave.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
// Validar fechas: formato YYYY-MM-DD estricto
|
||||
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
||||
let fechaGte = null, fechaLte = null;
|
||||
if (fechaInicio && fechaFin) {
|
||||
if (!ISO_DATE.test(fechaInicio) || !ISO_DATE.test(fechaFin)) {
|
||||
res.status(400).json({ error: 'Formato de fecha inválido (YYYY-MM-DD)' });
|
||||
return;
|
||||
}
|
||||
fechaGte = new Date(fechaInicio);
|
||||
fechaLte = new Date(fechaFin);
|
||||
if (isNaN(fechaGte) || isNaN(fechaLte) || fechaGte > fechaLte) {
|
||||
res.status(400).json({ error: 'Rango de fechas inválido' });
|
||||
return;
|
||||
}
|
||||
// `new Date('2026-08-23')` es 00:00:00Z, asi que un `$lte` dejaba fuera el
|
||||
// dia final entero (~2000 noticias/semana, justo las mas recientes).
|
||||
// Guardamos el instante siguiente y filtramos con `$lt`.
|
||||
fechaLte = new Date(fechaLte.getTime() + 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
// Validar nodos: entero positivo entre 1 y 500
|
||||
const nodosRaw = parseInt(nodos, 10);
|
||||
const nodosLimit = (!isNaN(nodosRaw) && nodosRaw > 0) ? Math.min(nodosRaw, 500) : 100;
|
||||
|
||||
// Validar complejidad: float entre 0 y 100
|
||||
const complejidadRaw = parseFloat(complejidad);
|
||||
const porcentajeSimilitudMin = (!isNaN(complejidadRaw) && complejidadRaw >= 0 && complejidadRaw <= 100)
|
||||
? complejidadRaw : 0;
|
||||
|
||||
// Construir query de MongoDB solo con valores validados
|
||||
const nodesQuery = { tema };
|
||||
if (subtematica) nodesQuery.subtema = subtematica;
|
||||
if (regexKw) nodesQuery.texto = { $regex: regexKw, $options: 'i' };
|
||||
// El filtro de fecha (modo "semanal") y el orden por frescura se aplican SOLO
|
||||
// a noticias: wikipedia/torrents/imagenes no tienen campo `fecha`, y meterles
|
||||
// el rango las vaciaria del grafo (perdiendo las conexiones noticia<->leak).
|
||||
//
|
||||
// Dos modos, y tienen que verse distintos:
|
||||
// Semanal (hay rango) -> solo noticias FECHADAS dentro de la ventana,
|
||||
// ordenadas por frescura. Las de `fecha: null`
|
||||
// quedan fuera (un $gte nunca casa con null).
|
||||
// Todas (sin rango) -> muestra aleatoria de TODO el archivo, incluidas
|
||||
// las de `fecha: null`. Con sort({fecha:-1}) +
|
||||
// limit(N) las 100 mas nuevas eran siempre las de
|
||||
// esta semana, asi que los dos modos devolvian el
|
||||
// mismo grafo byte a byte.
|
||||
const modoSemanal = Boolean(fechaGte);
|
||||
const noticiasQuery = { ...nodesQuery };
|
||||
if (modoSemanal) noticiasQuery.fecha = { $gte: fechaGte, $lt: fechaLte };
|
||||
// Imágenes: proporcional a nodosLimit (1/3), mínimo 3 para siempre mostrar algo
|
||||
const imagenesLimit = Math.max(3, Math.floor(nodosLimit / 3));
|
||||
|
||||
const [wikipediaNodes, noticiasNodes, torrentsNodes, imagenesNodes, imagenesWikiNodes] = await Promise.all([
|
||||
wikipediaCollection.find(nodesQuery).limit(nodosLimit).toArray(),
|
||||
modoSemanal
|
||||
? noticiasCollection.find(noticiasQuery).sort({ fecha: -1 }).limit(nodosLimit).toArray()
|
||||
: noticiasCollection.aggregate([
|
||||
{ $match: noticiasQuery },
|
||||
{ $sample: { size: nodosLimit } },
|
||||
]).toArray(),
|
||||
torrentsCollection.find(nodesQuery).limit(nodosLimit).toArray(),
|
||||
imagenesCollection.find(nodesQuery).limit(imagenesLimit).toArray(),
|
||||
imagenesWikiCollection.find(nodesQuery).limit(imagenesLimit).toArray(),
|
||||
]);
|
||||
|
||||
// Preferir imagenes analizadas (con keywords); si no hay, usar imagenes_wiki
|
||||
const imgNodes = imagenesNodes.length > 0 ? imagenesNodes : imagenesWikiNodes;
|
||||
|
||||
const nodes = [...wikipediaNodes, ...noticiasNodes, ...torrentsNodes];
|
||||
|
||||
// Nodos de texto
|
||||
const formattedNodes = nodes.map((result) => ({
|
||||
id: result.archivo.trim(),
|
||||
group: result.subtema || 'sin subtema',
|
||||
tema: result.tema || 'sin tema',
|
||||
content: result.texto || '',
|
||||
fecha: result.fecha || '',
|
||||
fuente: result.fuente || result.autor || result.source || '',
|
||||
imagen_url: result.imagen_url || '',
|
||||
type: 'texto',
|
||||
}));
|
||||
|
||||
// Nodos de imagen — image_url construida desde image_path absoluto
|
||||
const WIKI_IMAGES_BASE = WIKI_IMAGES_DIR.endsWith(path.sep)
|
||||
? WIKI_IMAGES_DIR
|
||||
: WIKI_IMAGES_DIR + path.sep;
|
||||
const imgFormattedNodes = imgNodes.map((result) => {
|
||||
let relativePath = null;
|
||||
if (result.image_path && result.image_path.startsWith(WIKI_IMAGES_BASE)) {
|
||||
const rel = result.image_path.slice(WIKI_IMAGES_BASE.length);
|
||||
// Reject paths with traversal sequences or absolute paths
|
||||
if (rel && !rel.includes('..') && !rel.startsWith('/')) {
|
||||
relativePath = rel;
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: result.archivo.trim(),
|
||||
group: result.subtema || result.tema || 'imagen',
|
||||
tema: result.tema || 'sin tema',
|
||||
content: result.texto || result.descripcion_wiki || '',
|
||||
fecha: result.fecha || '',
|
||||
fuente: result.fuente || result.autor || result.source || '',
|
||||
type: 'imagen',
|
||||
image_url: relativePath ? `/wiki-images/${relativePath}` : null,
|
||||
label: result.subtema || result.tema || result.archivo,
|
||||
};
|
||||
}).filter(n => n.image_url);
|
||||
|
||||
const allNodes = [...formattedNodes, ...imgFormattedNodes];
|
||||
const formattedNodes_final = allNodes;
|
||||
|
||||
const nodeIds = formattedNodes_final.map(node => node.id);
|
||||
|
||||
// Links texto-texto: filtro por complejidad del usuario.
|
||||
// Links imagen-texto (source1_type='imagen'): umbral fijo de 3% porque la similitud
|
||||
// TF-IDF keyword↔texto es estructuralmente baja (~3-10%) y quedarían siempre ocultos.
|
||||
const IMG_UMBRAL = 3.0;
|
||||
const linksQuery = {
|
||||
noticia1: { $in: nodeIds },
|
||||
noticia2: { $in: nodeIds },
|
||||
$or: [
|
||||
{ porcentaje_similitud: { $gte: porcentajeSimilitudMin } },
|
||||
{ porcentaje_similitud: { $gte: IMG_UMBRAL }, source1_type: 'imagen' },
|
||||
],
|
||||
};
|
||||
|
||||
const links = await comparacionesCollection.find(linksQuery).toArray();
|
||||
|
||||
// Limitar links por nodo: cada nodo muestra solo sus top MAX conexiones más fuertes.
|
||||
// Sin esto, los 100 nodos wikipedia del mismo tema se conectan todos entre sí
|
||||
// (>8000 links) y forman una bola densa inmanejable.
|
||||
const MAX_LINKS_PER_NODE = 8;
|
||||
links.sort((a, b) => b.porcentaje_similitud - a.porcentaje_similitud);
|
||||
const nodeDegree = new Map();
|
||||
const selectedLinks = [];
|
||||
for (const link of links) {
|
||||
const n1 = link.noticia1.trim();
|
||||
const n2 = link.noticia2.trim();
|
||||
const d1 = nodeDegree.get(n1) || 0;
|
||||
const d2 = nodeDegree.get(n2) || 0;
|
||||
if (d1 < MAX_LINKS_PER_NODE && d2 < MAX_LINKS_PER_NODE) {
|
||||
selectedLinks.push(link);
|
||||
nodeDegree.set(n1, d1 + 1);
|
||||
nodeDegree.set(n2, d2 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const formattedLinks = selectedLinks.map((result) => ({
|
||||
source: result.noticia1.trim(),
|
||||
target: result.noticia2.trim(),
|
||||
value: result.porcentaje_similitud,
|
||||
}));
|
||||
|
||||
res.json({ nodes: formattedNodes_final, links: formattedLinks });
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos:', error);
|
||||
res.status(500).json({ error: 'Error al obtener datos' });
|
||||
}
|
||||
});
|
||||
|
||||
// ── STATS: cálculo pesado → MongoDB, endpoint solo lee ─────────────────────────
|
||||
const STATS_TEMAS = ['guerra global','inteligencia y seguridad','cambio climático','demografía y sociedad','economía y corporaciones'];
|
||||
const STATS_TEXT_COLS = ['wikipedia','noticias','torrents','imagenes_wiki'];
|
||||
|
||||
async function computeAndSaveStats() {
|
||||
if (!db) return;
|
||||
console.log('[stats] Calculando stats...');
|
||||
try {
|
||||
const [wikipedia, noticias, torrents, imagenes, imagenes_wiki, comparaciones,
|
||||
comp_imagen_total] = await Promise.all([
|
||||
db.collection('wikipedia').countDocuments({}),
|
||||
db.collection('noticias').countDocuments({}),
|
||||
db.collection('torrents').countDocuments({}),
|
||||
db.collection('imagenes').countDocuments({}),
|
||||
db.collection('imagenes_wiki').countDocuments({}),
|
||||
db.collection('comparaciones').estimatedDocumentCount(),
|
||||
db.collection('comparaciones').countDocuments({ source1_type: 'imagen' }),
|
||||
]);
|
||||
|
||||
const por_tema = {};
|
||||
await Promise.all(STATS_TEMAS.map(async tema => {
|
||||
const counts = await Promise.all(
|
||||
STATS_TEXT_COLS.map(col => db.collection(col).countDocuments({ tema }))
|
||||
);
|
||||
por_tema[tema] = {};
|
||||
STATS_TEXT_COLS.forEach((col, i) => { por_tema[tema][col] = counts[i]; });
|
||||
}));
|
||||
|
||||
const subtemaRaw = await db.collection('wikipedia').aggregate([
|
||||
{ $match: { tema: { $in: STATS_TEMAS } } },
|
||||
{ $group: { _id: { tema: '$tema', subtema: '$subtema' }, n: { $sum: 1 } } },
|
||||
{ $sort: { n: -1 } },
|
||||
]).toArray();
|
||||
const subtemas = {};
|
||||
for (const doc of subtemaRaw) {
|
||||
const { tema, subtema } = doc._id;
|
||||
if (!subtemas[tema]) subtemas[tema] = [];
|
||||
if (subtemas[tema].length < 10)
|
||||
subtemas[tema].push({ subtema: subtema || 'sin subtema', n: doc.n });
|
||||
}
|
||||
|
||||
const notSubRaw = await db.collection('noticias').aggregate([
|
||||
{ $match: { tema: { $in: STATS_TEMAS } } },
|
||||
{ $group: { _id: { tema: '$tema', subtema: '$subtema' }, n: { $sum: 1 } } },
|
||||
{ $sort: { n: -1 } },
|
||||
]).toArray();
|
||||
const subtemas_noticias = {};
|
||||
for (const doc of notSubRaw) {
|
||||
const { tema, subtema } = doc._id;
|
||||
if (!subtemas_noticias[tema]) subtemas_noticias[tema] = [];
|
||||
if (subtemas_noticias[tema].length < 8)
|
||||
subtemas_noticias[tema].push({ subtema: subtema || 'sin subtema', n: doc.n });
|
||||
}
|
||||
|
||||
const compImgRaw = await db.collection('comparaciones').aggregate([
|
||||
{ $match: { source1_type: 'imagen' } },
|
||||
{ $group: {
|
||||
_id: '$tema',
|
||||
count: { $sum: 1 },
|
||||
avg_sim: { $avg: '$porcentaje_similitud' },
|
||||
max_sim: { $max: '$porcentaje_similitud' },
|
||||
}},
|
||||
]).toArray();
|
||||
const comp_imagen = { total: comp_imagen_total, por_tema: {} };
|
||||
for (const d of compImgRaw) {
|
||||
comp_imagen.por_tema[d._id] = {
|
||||
count: d.count,
|
||||
avg_sim: Math.round(d.avg_sim * 10) / 10,
|
||||
max_sim: Math.round(d.max_sim * 10) / 10,
|
||||
};
|
||||
}
|
||||
|
||||
// $sample sin filtro — 99.94% de docs son texto-texto
|
||||
const simRaw = await db.collection('comparaciones').aggregate([
|
||||
{ $sample: { size: 3000 } },
|
||||
{ $group: { _id: null, avg_sim: { $avg: '$porcentaje_similitud' }, max_sim: { $max: '$porcentaje_similitud' } } },
|
||||
]).toArray();
|
||||
const comp_texto = simRaw[0]
|
||||
? { avg_sim: Math.round(simRaw[0].avg_sim * 10) / 10, max_sim: Math.round(simRaw[0].max_sim * 10) / 10 }
|
||||
: { avg_sim: 0, max_sim: 0 };
|
||||
|
||||
const snapshot = {
|
||||
_id: 'stats',
|
||||
computed_at: new Date(),
|
||||
totales: { wikipedia, noticias, torrents, imagenes, imagenes_wiki, comparaciones },
|
||||
por_tema, subtemas, subtemas_noticias, comp_imagen, comp_texto,
|
||||
};
|
||||
|
||||
await db.collection('stats_cache').replaceOne({ _id: 'stats' }, snapshot, { upsert: true });
|
||||
console.log('[stats] Stats guardadas en MongoDB');
|
||||
} catch (e) {
|
||||
console.error('[stats] Error calculando stats:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Calcular al arrancar (si no hay cache o tiene más de 6h) y luego cada 6h
|
||||
async function initStats() {
|
||||
if (!db) return;
|
||||
const cached = await db.collection('stats_cache').findOne({ _id: 'stats' });
|
||||
const sixHours = 6 * 60 * 60 * 1000;
|
||||
if (!cached || !cached.computed_at || Date.now() - new Date(cached.computed_at).getTime() > sixHours) {
|
||||
computeAndSaveStats(); // async, no bloquea el arranque
|
||||
}
|
||||
setInterval(computeAndSaveStats, sixHours);
|
||||
}
|
||||
|
||||
app.get('/api/stats', async (req, res) => {
|
||||
try {
|
||||
if (!db) return res.status(500).json({ error: 'Sin conexión a BD' });
|
||||
const cached = await db.collection('stats_cache').findOne({ _id: 'stats' });
|
||||
if (!cached) return res.status(503).json({ error: 'Stats aún calculándose, intenta en unos minutos' });
|
||||
const { _id, ...data } = cached;
|
||||
res.json(data);
|
||||
} catch (e) {
|
||||
console.error('Error /api/stats:', e);
|
||||
res.status(500).json({ error: 'Error al obtener estadísticas' });
|
||||
}
|
||||
});
|
||||
|
||||
// **Buzón seguro: recepción de material de colaboradores/periodistas**
|
||||
const TEMAS_VALIDOS = new Set([
|
||||
'guerra global', 'inteligencia y seguridad', 'cambio climático',
|
||||
'economía y corporaciones', 'demografía y sociedad', 'otros',
|
||||
]);
|
||||
|
||||
// Página HTML de resultado (para envíos desde el formulario nativo, sin JavaScript)
|
||||
function renderSubmitPage(payload) {
|
||||
const ok = payload.ok;
|
||||
const inner = ok
|
||||
? `<div class="tag ok">Envío recibido</div>
|
||||
<h1>Gracias. Tu material está a salvo.</h1>
|
||||
<p>Guarda este código de referencia. Es tu único identificador y no lo compartas por canales inseguros:</p>
|
||||
<div class="ref">${payload.ref}</div>
|
||||
<p class="small">No hemos registrado tu IP. Si dejaste un contacto, te escribiremos por ahí.</p>`
|
||||
: `<div class="tag err">No se pudo enviar</div>
|
||||
<h1>Algo ha fallado</h1>
|
||||
<p>${(payload.error || 'Inténtalo de nuevo.').replace(/[<>&]/g, '')}</p>`;
|
||||
return `<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex">
|
||||
<title>UP-LEAKS // ${ok ? 'envío recibido' : 'error'}</title>
|
||||
<style>
|
||||
@font-face{font-family:'Fira Code';src:url('/fonts/fira-code-400.woff2') format('woff2');font-weight:400}
|
||||
@font-face{font-family:'Fira Code';src:url('/fonts/fira-code-700.woff2') format('woff2');font-weight:700}
|
||||
*{box-sizing:border-box;margin:0}
|
||||
body{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px;
|
||||
font-family:'Fira Code',monospace;color:#d7f5d0;text-align:center;
|
||||
background:linear-gradient(rgba(0,0,0,.86),rgba(0,8,0,.95)),url('/images/flujos6.jpg') center/cover fixed;}
|
||||
.card{max-width:560px;background:rgba(6,10,6,.75);border:1px solid rgba(57,255,20,.35);
|
||||
backdrop-filter:blur(8px);padding:44px 38px;box-shadow:0 0 60px -20px rgba(57,255,20,.5)}
|
||||
.tag{display:inline-block;font-size:12px;letter-spacing:.18em;text-transform:uppercase;padding:4px 12px;border:1px solid;margin-bottom:18px}
|
||||
.tag.ok{color:#39ff14;border-color:#39ff14}.tag.err{color:#ff2d2d;border-color:#ff2d2d}
|
||||
h1{font-size:clamp(24px,4vw,32px);margin-bottom:14px;font-weight:700;line-height:1.15}
|
||||
p{color:#8fb98a;font-size:15px;line-height:1.6;margin-bottom:14px}.small{font-size:13px;color:#6f9a6a}
|
||||
.ref{font-size:22px;font-weight:700;color:#39ff14;letter-spacing:.08em;text-shadow:0 0 12px rgba(57,255,20,.6);
|
||||
border:1px dashed rgba(57,255,20,.5);padding:14px;margin:20px 0}
|
||||
.btn{display:inline-block;margin-top:16px;color:#39ff14;text-decoration:none;border:1px solid #39ff14;
|
||||
padding:12px 28px;text-transform:uppercase;letter-spacing:.06em;font-weight:700;font-size:13px;transition:.18s}
|
||||
.btn:hover{background:#39ff14;color:#000;box-shadow:0 0 18px -2px rgba(57,255,20,.7)}
|
||||
</style></head><body><div class="card">${inner}
|
||||
<a class="btn" href="/journalist.html">${ok ? 'volver al buzón' : 'reintentar'}</a></div></body></html>`;
|
||||
}
|
||||
|
||||
function sendSubmitResult(req, res, status, payload) {
|
||||
const wantsHtml = (req.body && req.body.render === 'html') || (req.headers.accept || '').includes('text/html');
|
||||
if (wantsHtml) return res.status(status).type('html').send(renderSubmitPage(payload));
|
||||
return res.status(status).json(payload);
|
||||
}
|
||||
|
||||
app.post('/api/submit', submitRateLimit, (req, res) => {
|
||||
submitUpload.array('adjuntos', 8)(req, res, async (err) => {
|
||||
if (err) {
|
||||
const msg = err.code === 'LIMIT_FILE_SIZE'
|
||||
? 'Algún archivo supera el límite de 50 MB.'
|
||||
: (err.code === 'LIMIT_FILE_COUNT' ? 'Máximo 8 archivos por envío.' : 'Error al subir los archivos.');
|
||||
return sendSubmitResult(req, res, 400, { ok: false, error: msg });
|
||||
}
|
||||
try {
|
||||
// Honeypot anti-bots: si el campo oculto viene relleno, descartamos en silencio.
|
||||
if (req.body && req.body.website) {
|
||||
return sendSubmitResult(req, res, 200, { ok: true, ref: 'RECIBIDO' });
|
||||
}
|
||||
const mensaje = (req.body.mensaje || '').toString().slice(0, 20000).trim();
|
||||
let tema = (req.body.tema || 'otros').toString().trim();
|
||||
if (!TEMAS_VALIDOS.has(tema)) tema = 'otros';
|
||||
const contacto = (req.body.contacto || '').toString().slice(0, 400).trim();
|
||||
const files = req.files || [];
|
||||
|
||||
if (!mensaje && files.length === 0) {
|
||||
return sendSubmitResult(req, res, 400, { ok: false, error: 'Escribe un mensaje o adjunta al menos un archivo.' });
|
||||
}
|
||||
|
||||
const ref = 'UPL-' + Date.now().toString(36).toUpperCase()
|
||||
+ '-' + Math.random().toString(36).slice(2, 6).toUpperCase();
|
||||
|
||||
const doc = {
|
||||
ref,
|
||||
tema,
|
||||
mensaje,
|
||||
contacto,
|
||||
archivos: files.map((f) => ({
|
||||
nombre_original: f.originalname,
|
||||
guardado: path.basename(f.path),
|
||||
bytes: f.size,
|
||||
mimetype: f.mimetype,
|
||||
})),
|
||||
estado: 'nuevo',
|
||||
creado: new Date(),
|
||||
};
|
||||
// No registramos IP ni contenido en logs por privacidad de la fuente.
|
||||
if (db) await db.collection('submissions').insertOne(doc);
|
||||
return sendSubmitResult(req, res, 200, { ok: true, ref });
|
||||
} catch (e) {
|
||||
console.error('Error en /api/submit:', e.message);
|
||||
return sendSubmitResult(req, res, 500, { ok: false, error: 'No se pudo procesar el envío. Inténtalo de nuevo.' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// **Panel de administración: lectura de envíos (fail-closed)**
|
||||
const crypto = require('crypto');
|
||||
const ADMIN_TOKEN_FILE = path.join(__dirname, '../../FLUJOS_DATOS/.admin_token');
|
||||
|
||||
function getAdminToken() {
|
||||
if (process.env.ADMIN_TOKEN && process.env.ADMIN_TOKEN.trim()) return process.env.ADMIN_TOKEN.trim();
|
||||
try {
|
||||
const t = fs.readFileSync(ADMIN_TOKEN_FILE, 'utf8').trim();
|
||||
return t || null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
function safeEqual(a, b) {
|
||||
const ba = Buffer.from(String(a)); const bb = Buffer.from(String(b));
|
||||
if (ba.length !== bb.length) return false;
|
||||
return crypto.timingSafeEqual(ba, bb);
|
||||
}
|
||||
function requireAdmin(req, res, next) {
|
||||
const token = getAdminToken();
|
||||
if (!token) return res.status(503).json({ ok: false, error: 'Panel no configurado: falta ADMIN_TOKEN o FLUJOS_DATOS/.admin_token' });
|
||||
const auth = req.headers.authorization || '';
|
||||
const provided = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
||||
if (!provided || !safeEqual(provided, token)) {
|
||||
return res.status(401).json({ ok: false, error: 'No autorizado' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Listado de envíos (metadatos + mensaje). Más recientes primero.
|
||||
app.get('/api/submissions', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
if (!db) return res.status(503).json({ ok: false, error: 'BD no disponible' });
|
||||
const limit = Math.min(parseInt(req.query.limit, 10) || 100, 500);
|
||||
const docs = await db.collection('submissions')
|
||||
.find({}, { projection: { _id: 0 } })
|
||||
.sort({ creado: -1 }).limit(limit).toArray();
|
||||
res.json({ ok: true, total: docs.length, submissions: docs });
|
||||
} catch (e) {
|
||||
console.error('Error /api/submissions:', e.message);
|
||||
res.status(500).json({ ok: false, error: 'Error al leer los envíos' });
|
||||
}
|
||||
});
|
||||
|
||||
// Marcar un envío como revisado / archivado
|
||||
app.post('/api/submissions/:ref/estado', requireAdmin, express.json(), async (req, res) => {
|
||||
try {
|
||||
if (!db) return res.status(503).json({ ok: false, error: 'BD no disponible' });
|
||||
const estado = ['nuevo', 'revisado', 'archivado'].includes((req.body || {}).estado) ? req.body.estado : null;
|
||||
if (!estado) return res.status(400).json({ ok: false, error: 'estado inválido' });
|
||||
const r = await db.collection('submissions').updateOne({ ref: req.params.ref }, { $set: { estado } });
|
||||
res.json({ ok: r.matchedCount > 0, estado });
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: 'Error al actualizar' });
|
||||
}
|
||||
});
|
||||
|
||||
// Descarga de un adjunto (valida basename contra traversal; sirve como adjunto)
|
||||
app.get('/api/submissions/file/:guardado', requireAdmin, (req, res) => {
|
||||
const name = path.basename(req.params.guardado || '');
|
||||
if (!name || name.includes('..') || name.startsWith('.')) return res.status(400).json({ ok: false, error: 'nombre inválido' });
|
||||
const full = path.join(SUBMISSIONS_DIR, name);
|
||||
if (!full.startsWith(SUBMISSIONS_DIR) || !fs.existsSync(full)) return res.status(404).json({ ok: false, error: 'no encontrado' });
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${name}"`);
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
fs.createReadStream(full).pipe(res);
|
||||
});
|
||||
|
||||
// **Monitor del sistema: procesos, servicios y progreso de procesamiento (admin)**
|
||||
const os = require('os');
|
||||
const { exec } = require('child_process');
|
||||
function sh(cmd, timeout = 4500) {
|
||||
return new Promise((resolve) => { exec(cmd, { timeout }, (e, out) => resolve(e ? '' : String(out))); });
|
||||
}
|
||||
const SYS_SERVICES = ['mongod', 'flujos-stats', 'coconews-api', 'coconews-ingestor',
|
||||
'coconews-translator', 'coconews-ner', 'coconews-embedder', 'flujos-pipeline'];
|
||||
const SYS_MATCHERS = [
|
||||
{ key: 'mongod', label: 'MongoDB', re: /mongod(\s|$)/ },
|
||||
{ key: 'flujos-stats', label: 'FLUJOS · API del grafo', re: /FLUJOS_APP\.js/ },
|
||||
{ key: 'coconews-api', label: 'Coconews · API', re: /coconews\/api\/server\.js/ },
|
||||
{ key: 'ingestor', label: 'Ingestor RSS', re: /ingestor\.py/ },
|
||||
{ key: 'translator', label: 'Traductor (ES)', re: /translator\.py/ },
|
||||
{ key: 'ner', label: 'NER · entidades', re: /ner\.py/ },
|
||||
{ key: 'embedder', label: 'Embedder en vivo', re: /workers\/embedder\.py/ },
|
||||
{ key: 'backfill', label: 'Backfill de embeddings', re: /backfill_embeddings\.py/ },
|
||||
{ key: 'motor', label: 'Comparación semántica (v2)', re: /motor_comparacion\.py/ },
|
||||
{ key: 'pipeline', label: 'Pipeline maestro', re: /pipeline_maestro\.py/ },
|
||||
{ key: 'mongolo', label: 'Comparación (word-overlap, viejo)', re: /pipeline_mongolo\.py/ },
|
||||
];
|
||||
|
||||
app.get('/api/system', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const [psOut, actOut, timerOut, svcShow, timerEnabled] = await Promise.all([
|
||||
sh('ps -eo pid,pcpu,pmem,rss,etimes,args --sort=-pcpu --no-headers'),
|
||||
sh('systemctl is-active ' + SYS_SERVICES.join(' ')),
|
||||
sh('systemctl show flujos-pipeline.timer -p NextElapseUSecRealtime -p LastTriggerUSec'),
|
||||
sh('systemctl show flujos-pipeline.service -p ActiveState -p SubState'),
|
||||
sh('systemctl is-enabled flujos-pipeline.timer'),
|
||||
]);
|
||||
const parseKV = (s) => { const o = {}; (s || '').split('\n').forEach((l) => { const i = l.indexOf('='); if (i > 0) o[l.slice(0, i)] = l.slice(i + 1).trim(); }); return o; };
|
||||
|
||||
const actLines = actOut.trim().split('\n');
|
||||
const services = SYS_SERVICES.map((s, i) => ({ name: s, active: (actLines[i] || '').trim() === 'active' }));
|
||||
|
||||
const procesos = [];
|
||||
const seen = new Set();
|
||||
psOut.split('\n').forEach((line) => {
|
||||
const m = line.trim().match(/^(\d+)\s+([\d.]+)\s+([\d.]+)\s+(\d+)\s+(\d+)\s+(.*)$/);
|
||||
if (!m) return;
|
||||
const args = m[6];
|
||||
for (const mt of SYS_MATCHERS) {
|
||||
if (mt.re.test(args)) {
|
||||
procesos.push({ key: mt.key, label: mt.label, pid: +m[1], cpu: +m[2], mem: +m[3], rss: (+m[4]) * 1024, etimes: +m[5] });
|
||||
seen.add(mt.key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let progress = null;
|
||||
let comparacion = null;
|
||||
if (db) {
|
||||
const [torrTotal, torrEmb, torrProc, notiTotal, notiEmb, chunks, feedsAct, feedsFallo, subsNuevos, lastNoti] = await Promise.all([
|
||||
db.collection('torrents').estimatedDocumentCount(),
|
||||
db.collection('torrents').countDocuments({ embedding: { $exists: true } }), // vectores reales
|
||||
db.collection('torrents').countDocuments({ procesado_embedding: true }), // procesados (incluye no-aptos)
|
||||
db.collection('noticias').estimatedDocumentCount(),
|
||||
db.collection('noticias').countDocuments({ embedding: { $exists: true } }),
|
||||
db.collection('embeddings_chunks').estimatedDocumentCount(),
|
||||
db.collection('coconews_feeds').countDocuments({ activo: true }),
|
||||
db.collection('coconews_feeds').countDocuments({ fallos: { $gt: 0 } }),
|
||||
db.collection('submissions').countDocuments({ estado: 'nuevo' }),
|
||||
db.collection('noticias').find({}).sort({ _id: -1 }).limit(1).project({ fecha: 1, _id: 0 }).toArray(),
|
||||
]);
|
||||
progress = { torrTotal, torrEmb, torrProc, torrNoApto: Math.max(0, torrProc - torrEmb),
|
||||
notiTotal, notiEmb, chunks, feedsAct, feedsFallo, subsNuevos,
|
||||
lastNoti: (lastNoti[0] && lastNoti[0].fecha) || null };
|
||||
|
||||
// --- Comparación: qué colección sirve el front (v1 word-overlap vs v2 semántica) ---
|
||||
const [v1Aristas, v2Aristas] = await Promise.all([
|
||||
db.collection('comparaciones').estimatedDocumentCount(),
|
||||
db.collection('comparaciones_v2').estimatedDocumentCount(),
|
||||
]);
|
||||
let v2Eventos = 0, v2Muestra = null;
|
||||
if (v2Aristas > 0) {
|
||||
[v2Eventos, v2Muestra] = await Promise.all([
|
||||
db.collection('comparaciones_v2').countDocuments({ posible_evento: true }),
|
||||
db.collection('comparaciones_v2').find({}).sort({ _id: -1 }).limit(1).project({ metodo: 1, modelo: 1, _id: 0 }).toArray().then((a) => a[0] || null),
|
||||
]);
|
||||
}
|
||||
comparacion = {
|
||||
activa: COMPARACIONES_COL,
|
||||
v2Activa: COMPARACIONES_COL === 'comparaciones_v2',
|
||||
v1Aristas, v2Aristas, v2Eventos,
|
||||
metodo: (v2Muestra && v2Muestra.metodo) || null,
|
||||
modelo: (v2Muestra && v2Muestra.modelo) || null,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Pipeline maestro: reconstruir ejecuciones desde pipeline_log ---
|
||||
const PIPE_FASES = ['scraping', 'analisis', 'comparacion'];
|
||||
const PIPE_LABEL = { scraping: 'Scraping', analisis: 'Análisis', comparacion: 'Comparación' };
|
||||
let pipelineMaestro = null;
|
||||
if (db) {
|
||||
const logs = await db.collection('pipeline_log').find({}).sort({ inicio: 1 }).toArray();
|
||||
const runs = []; let cur = null;
|
||||
logs.forEach((l) => {
|
||||
if (l.fase === 'scraping' || !cur) { if (cur) runs.push(cur); cur = { inicio: l.inicio, fin: l.fin, fases: {} }; }
|
||||
cur.fases[l.fase] = { estado: l.estado, inicio: l.inicio, fin: l.fin, stats: l.stats || {} };
|
||||
if (l.fin && (!cur.fin || new Date(l.fin) > new Date(cur.fin))) cur.fin = l.fin;
|
||||
});
|
||||
if (cur) runs.push(cur);
|
||||
const currentRun = runs[runs.length - 1] || null;
|
||||
const finished = runs.filter((r) => r.fases.comparacion && r.fases.comparacion.fin);
|
||||
const lastFinished = finished[finished.length - 1] || null;
|
||||
const nowMs = Date.now();
|
||||
const fasesArr = PIPE_FASES.map((f) => {
|
||||
const e = currentRun && currentRun.fases[f];
|
||||
const durSec = e ? (e.fin ? (new Date(e.fin) - new Date(e.inicio)) / 1000 : (nowMs - new Date(e.inicio)) / 1000) : null;
|
||||
return { fase: f, label: PIPE_LABEL[f], estado: e ? e.estado : 'pendiente', durSec, stats: (e && e.stats) || {} };
|
||||
});
|
||||
const svcv = parseKV(svcShow);
|
||||
const timerv = parseKV(timerOut);
|
||||
const nextU = parseInt(timerv.NextElapseUSecRealtime, 10);
|
||||
const lastU = parseInt(timerv.LastTriggerUSec, 10);
|
||||
let nextRun = null;
|
||||
if (nextU > 0) nextRun = Math.round(nextU / 1000);
|
||||
else if (lastU > 0) nextRun = Math.round(lastU / 1000) + 12 * 3600 * 1000; // cada 12 h
|
||||
const running = svcv.SubState === 'start' || svcv.ActiveState === 'activating' || fasesArr.some((f) => f.estado === 'en_progreso');
|
||||
pipelineMaestro = {
|
||||
fases: fasesArr,
|
||||
runInicio: currentRun ? currentRun.inicio : null,
|
||||
runElapsedSec: currentRun ? (nowMs - new Date(currentRun.inicio)) / 1000 : null,
|
||||
lastDurSec: lastFinished ? (new Date(lastFinished.fin) - new Date(lastFinished.inicio)) / 1000 : null,
|
||||
lastFin: lastFinished ? lastFinished.fin : null,
|
||||
nextRun, running, cadencia: 'cada 12 h',
|
||||
habilitado: (timerEnabled || '').trim() === 'enabled',
|
||||
estado: fasesArr.some((f) => f.estado === 'error') ? 'error' : (running ? 'en_progreso' : 'idle'),
|
||||
};
|
||||
}
|
||||
const nextPipeline = pipelineMaestro ? pipelineMaestro.nextRun : null;
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
host: { load: os.loadavg(), cores: os.cpus().length, totalmem: os.totalmem(), freemem: os.freemem(), uptime: os.uptime() },
|
||||
services, procesos, progress, nextPipeline, comparacion,
|
||||
pipelines: { maestro: pipelineMaestro },
|
||||
now: Date.now(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('/api/system:', e.message);
|
||||
res.status(500).json({ ok: false, error: 'No se pudo leer el estado del sistema' });
|
||||
}
|
||||
});
|
||||
|
||||
// **Luego, definir las rutas de las páginas principales**
|
||||
// Rutas para las páginas principales
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/index.html'));
|
||||
});
|
||||
|
||||
app.get('/stats.html', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/stats.html'));
|
||||
});
|
||||
|
||||
app.get('/climate.html', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/climate.html'));
|
||||
});
|
||||
|
||||
app.get('/glob-war.html', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/glob-war.html'));
|
||||
});
|
||||
|
||||
app.get('/popl-up.html', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/popl-up.html'));
|
||||
});
|
||||
|
||||
app.get('/int-sec.html', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/int-sec.html'));
|
||||
});
|
||||
|
||||
app.get('/eco-corp.html', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../VISUALIZACION/public/eco-corp.html'));
|
||||
});
|
||||
|
||||
// **Después, configurar el middleware de archivos estáticos**
|
||||
|
||||
// Imágenes scrapeadas de Wikipedia
|
||||
app.use('/wiki-images', express.static(WIKI_IMAGES_DIR));
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../VISUALIZACION/public')));
|
||||
|
||||
// Conexión a MongoDB usando variables de entorno
|
||||
const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost:27017';
|
||||
const dbName = process.env.DB_NAME || 'FLUJOS_DATOS';
|
||||
|
||||
let db; // Variable para almacenar la conexión a la base de datos
|
||||
|
||||
// Función para conectar a MongoDB
|
||||
async function connectToMongoDB() {
|
||||
try {
|
||||
const mongoClient = new MongoClient(mongoUrl, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
});
|
||||
await mongoClient.connect();
|
||||
db = mongoClient.db(dbName);
|
||||
console.log('Conectado a MongoDB');
|
||||
initStats();
|
||||
} catch (error) {
|
||||
console.error('Error al conectar a MongoDB:', error);
|
||||
process.exit(1); // Salir de la aplicación si no se puede conectar
|
||||
}
|
||||
}
|
||||
|
||||
// Llamar a la función para conectar a MongoDB
|
||||
connectToMongoDB();
|
||||
|
||||
// Iniciar el servidor escuchando en todas las interfaces
|
||||
app.listen(port, '127.0.0.1', () => {
|
||||
console.log(`La aplicación está escuchando en http://localhost:${port}`);
|
||||
});
|
||||
|
||||
// Manejar el cierre de la aplicación
|
||||
process.on('SIGINT', async () => {
|
||||
if (db) {
|
||||
await db.close();
|
||||
console.log('Conexión a MongoDB cerrada');
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/tensorflow', methods=['POST'])
|
||||
def tensorflow_endpoint():
|
||||
data = request.json
|
||||
# Aquí procesarías el dato con tu modelo TensorFlow
|
||||
# Por ahora solo lo devolvemos como está
|
||||
return jsonify(data)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(port=5000)
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import pandas as pd
|
||||
from pymongo import MongoClient
|
||||
from obsei.source.pandas_source import PandasSource, PandasSourceConfig
|
||||
from obsei.analyzer.classification_analyzer import ClassificationAnalyzerConfig, ZeroShotClassificationAnalyzer
|
||||
from obsei.sink.pandas_sink import PandasSink, PandasSinkConfig
|
||||
|
||||
# Configura la fuente
|
||||
source = PandasSource()
|
||||
source_config = PandasSourceConfig(
|
||||
# Proporciona tu configuración aquí
|
||||
)
|
||||
|
||||
# Configura el analizador
|
||||
analyzer = ZeroShotClassificationAnalyzer(
|
||||
# Proporciona tu configuración aquí
|
||||
)
|
||||
analyzer_config = ClassificationAnalyzerConfig(
|
||||
# Proporciona tu configuración aquí
|
||||
)
|
||||
|
||||
# Configura el sumidero
|
||||
sink = PandasSink()
|
||||
sink_config = PandasSinkConfig(
|
||||
# Proporciona tu configuración aquí
|
||||
)
|
||||
|
||||
# Ejecuta el flujo de trabajo
|
||||
source_response_list = source.lookup(source_config)
|
||||
analyzer_response_list = analyzer.analyze_input(
|
||||
source_response_list=source_response_list,
|
||||
analyzer_config=analyzer_config
|
||||
)
|
||||
sink_response_list = sink.send_data(analyzer_response_list, sink_config)
|
||||
|
||||
# Ahora `sink_response_list` es una lista de DataFrames.
|
||||
# Puedes concatenarlos en un solo DataFrame si es necesario.
|
||||
df = pd.concat(sink_response_list)
|
||||
|
||||
# Realiza cualquier transformación o limpieza de datos necesaria aquí...
|
||||
|
||||
# Finalmente, guarda el DataFrame en MongoDB.
|
||||
client = MongoClient('mongodb://localhost:27017/')
|
||||
db = client['nombre_de_tu_base_de_datos']
|
||||
collection = db['nombre_de_tu_coleccion']
|
||||
collection.insert_many(df.to_dict('records'))
|
||||
12122
FLUJOS/BACK_BACK/package-lock.json
generated
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"name": "flujos",
|
||||
"version": "1.0.0",
|
||||
"description": "Aplicación FLUJOS",
|
||||
"main": "FLUJOS_APP.js",
|
||||
"scripts": {
|
||||
"start": "node FLUJOS_APP.js",
|
||||
"dev": "nodemon FLUJOS_APP.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.9.0",
|
||||
"body-parser": "^1.20.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.0",
|
||||
"express-graphql": "^0.12.0",
|
||||
"force-graph": "^1.43.2",
|
||||
"graphql": "^15.8.0",
|
||||
"helmet": "^8.0.0",
|
||||
"mongod": "^2.0.0",
|
||||
"mongodb": "^6.9.0",
|
||||
"mongoose": "^7.3.4",
|
||||
"multer": "^2.2.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"tree": "^0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.7",
|
||||
"parcel-bundler": "^1.8.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
pip install obsei[all]
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
const User = require('../models/User');
|
||||
const Post = require('../models/Post');
|
||||
const Node = require('../models/Node');
|
||||
const Flow = require('../models/Flow');
|
||||
const Project = require('../models/Project');
|
||||
|
||||
module.exports = {
|
||||
users: async () => {
|
||||
//... tu código existente para obtener usuarios
|
||||
},
|
||||
|
||||
posts: async () => {
|
||||
//... tu código existente para obtener posts
|
||||
},
|
||||
|
||||
nodes: async () => {
|
||||
try {
|
||||
const nodes = await Node.find();
|
||||
return nodes.map(node => {
|
||||
return {
|
||||
...node._doc,
|
||||
_id: node._id.toString(),
|
||||
flow: getFlow.bind(this, node._doc.flow)
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
flows: async () => {
|
||||
try {
|
||||
const flows = await Flow.find();
|
||||
return flows.map(flow => {
|
||||
return {
|
||||
...flow._doc,
|
||||
_id: flow._id.toString(),
|
||||
nodes: getNodes.bind(this, flow._doc.nodes),
|
||||
project: getProject.bind(this, flow._doc.project)
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
projects: async () => {
|
||||
try {
|
||||
const projects = await Project.find();
|
||||
return projects.map(project => {
|
||||
return {
|
||||
...project._doc,
|
||||
_id: project._id.toString(),
|
||||
flows: getFlows.bind(this, project._doc.flows)
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Aquí necesitarías implementar las funciones auxiliares getPosts, getUser, getFlow, getNodes, getFlows y getProject.
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
const { buildSchema } = require('graphql');
|
||||
|
||||
module.exports = buildSchema(`
|
||||
type User {
|
||||
_id: ID!
|
||||
name: String!
|
||||
email: String!
|
||||
posts: [Post!]
|
||||
}
|
||||
|
||||
type Post {
|
||||
_id: ID!
|
||||
title: String!
|
||||
content: String!
|
||||
creator: User!
|
||||
}
|
||||
|
||||
type Node {
|
||||
_id: ID!
|
||||
name: String!
|
||||
flow: Flow!
|
||||
}
|
||||
|
||||
type Flow {
|
||||
_id: ID!
|
||||
nodes: [Node!]!
|
||||
project: Project!
|
||||
}
|
||||
|
||||
type Project {
|
||||
_id: ID!
|
||||
flows: [Flow!]!
|
||||
}
|
||||
|
||||
type RootQuery {
|
||||
users: [User!]!
|
||||
posts: [Post!]!
|
||||
nodes: [Node!]!
|
||||
flows: [Flow!]!
|
||||
projects: [Project!]!
|
||||
}
|
||||
|
||||
schema {
|
||||
query: RootQuery
|
||||
}
|
||||
`);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// export const newsData = [
|
||||
// {
|
||||
// "id": "1",
|
||||
// "title": "BlackRock adquiere empresa de tecnología financiera",
|
||||
// "shares": 3500
|
||||
// },
|
||||
// {
|
||||
// "id": "2",
|
||||
// "title": "BlackRock lanza nuevo fondo de inversión en energías renovables",
|
||||
// "shares": 2900
|
||||
// },
|
||||
// {
|
||||
// "id": "3",
|
||||
// "title": "BlackRock reporta ganancias record en el último trimestre",
|
||||
// "shares": 4000
|
||||
// },
|
||||
// {
|
||||
// "id": "4",
|
||||
// "title": "BlackRock lidera ronda de financiamiento en startup de inteligencia artificial",
|
||||
// "shares": 2100
|
||||
// },
|
||||
// {
|
||||
// "id": "5",
|
||||
// "title": "CEO de BlackRock promueve inversiones sostenibles en cumbre global",
|
||||
// "shares": 2600
|
||||
// },
|
||||
// {
|
||||
// "id": "6",
|
||||
// "title": "BlackRock apunta a mercados emergentes con nuevo producto de inversión",
|
||||
// "shares": 2800
|
||||
// },
|
||||
// {
|
||||
// "id": "7",
|
||||
// "title": "BlackRock se compromete a reducir su huella de carbono en un 50% para 2030",
|
||||
// "shares": 3500
|
||||
// },
|
||||
// {
|
||||
// "id": "8",
|
||||
// "title": "BlackRock lidera el mercado de ETFs con fuerte crecimiento en el último año",
|
||||
// "shares": 3900
|
||||
// },
|
||||
// {
|
||||
// "id": "9",
|
||||
// "title": "BlackRock anuncia inversión millonaria en infraestructura de blockchain",
|
||||
// "shares": 3300
|
||||
// },
|
||||
// {
|
||||
// "id": "10",
|
||||
// "title": "BlackRock apoya iniciativa de transparencia financiera en G20",
|
||||
// "shares": 3000
|
||||
// }
|
||||
// ];
|
||||
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const FlowSchema = new Schema({
|
||||
sourceNode: { type: Schema.Types.ObjectId, ref: 'Node' },
|
||||
targetNode: { type: Schema.Types.ObjectId, ref: 'Node' },
|
||||
intensity: Number,
|
||||
// Aquí puedes incluir cualquier metadato del flujo que necesites.
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Flow', FlowSchema);
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const NodeSchema = new Schema({
|
||||
coordinates: { x: Number, y: Number, z: Number },
|
||||
type: String,
|
||||
// Aquí puedes incluir cualquier metadato del nodo que necesites.
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Node', NodeSchema);
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const ProjectSchema = new Schema({
|
||||
user: { type: Schema.Types.ObjectId, ref: 'User' },
|
||||
nodes: [{ type: Schema.Types.ObjectId, ref: 'Node' }],
|
||||
flows: [{ type: Schema.Types.ObjectId, ref: 'Flow' }],
|
||||
// Aquí puedes incluir cualquier metadato del proyecto que necesites.
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Project', ProjectSchema);
|
||||
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const UserSchema = new Schema({
|
||||
username: String,
|
||||
password: String,
|
||||
// Aquí puedes incluir cualquier metadato del usuario que necesites.
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('User', UserSchema);
|
||||
10766
FLUJOS/VISUALIZACION/package-lock.json
generated
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.9.0",
|
||||
"3d-force-graph": "^1.73.3",
|
||||
"express": "^4.18.2",
|
||||
"express-graphql": "^0.12.0",
|
||||
"force-graph": "^1.43.2",
|
||||
"graphql": "^15.8.0",
|
||||
"mongod": "^2.0.0",
|
||||
"mongoose": "^7.3.4",
|
||||
"three": "^0.168.0",
|
||||
"tree": "^0.1.3"
|
||||
},
|
||||
"name": "flujos",
|
||||
"version": "1.0.0",
|
||||
"main": "FLUJOS_APP.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"parcel-bundler": "^1.8.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
:root {
|
||||
--cube-size: 80px;
|
||||
--line-color: #333;
|
||||
--hover-color: #2ecc71;
|
||||
--background-color: #000;
|
||||
--sidebar-width: 250px; /* Ajustamos el ancho del sidebar */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: flex-start; /* Asegura que el contenido comience desde la izquierda */
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
position: relative;
|
||||
width: calc(100vw - var(--sidebar-width)); /* Restamos el ancho del sidebar */
|
||||
height: 100vh;
|
||||
margin-left: var(--sidebar-width); /* Añadimos margen para que no se solape */
|
||||
}
|
||||
|
||||
.cube-container {
|
||||
position: absolute;
|
||||
width: var(--cube-size);
|
||||
height: var(--cube-size);
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 1s ease;
|
||||
}
|
||||
|
||||
.cube-container:hover {
|
||||
transform: rotateX(360deg) rotateY(360deg); /* Efecto al pasar el mouse */
|
||||
}
|
||||
|
||||
.cube {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.face {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--hover-color);
|
||||
border: 1px solid #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.65rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.front { transform: translateZ(calc(var(--cube-size) / 2)); }
|
||||
.back { transform: rotateY(180deg) translateZ(calc(var(--cube-size) / 2)); }
|
||||
.left { transform: rotateY(-90deg) translateZ(calc(var(--cube-size) / 2)); }
|
||||
.right { transform: rotateY(90deg) translateZ(calc(var(--cube-size) / 2)); }
|
||||
.top { transform: rotateX(90deg) translateZ(calc(var(--cube-size) / 2)); }
|
||||
.bottom { transform: rotateX(-90deg) translateZ(calc(var(--cube-size) / 2)); }
|
||||
|
||||
.lines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.line-path {
|
||||
stroke: var(--line-color);
|
||||
stroke-width: 2;
|
||||
transition: stroke 0.3s;
|
||||
}
|
||||
|
||||
.line-path:hover {
|
||||
stroke: #e74c3c;
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Visualización 2D de Noticias Políticas</title>
|
||||
<link rel="stylesheet" href="3dscript_eco-corp.css">
|
||||
<style>
|
||||
.popup {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
color: black;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
z-index: 2;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.popup:focus,
|
||||
.cube-container:focus + .popup {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.popup button {
|
||||
display: block;
|
||||
margin: 10px auto;
|
||||
padding: 10px;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popup button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="graph-container" id="graph-container">
|
||||
<svg class="lines" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" id="svg-lines">
|
||||
<!-- Líneas generadas dinámicamente -->
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const numCubes = 9; // Número total de cubos
|
||||
const spacing = 150; // Espacio mínimo entre los cubos
|
||||
const graphContainer = document.getElementById('graph-container');
|
||||
const svgLines = document.getElementById('svg-lines');
|
||||
let cubePositions = [];
|
||||
|
||||
// Función para detectar colisiones y asegurarse de que los cubos no se solapen
|
||||
function detectCollision(newX, newY, positions) {
|
||||
for (let pos of positions) {
|
||||
const distance = Math.sqrt((newX - pos.x) ** 2 + (newY - pos.y) ** 2);
|
||||
if (distance < spacing) {
|
||||
return true; // Hay colisión
|
||||
}
|
||||
}
|
||||
return false; // No hay colisión
|
||||
}
|
||||
|
||||
// Función para generar una posición aleatoria sin que los cubos se solapen
|
||||
function getRandomPosition(maxWidth, maxHeight) {
|
||||
let x, y;
|
||||
do {
|
||||
x = Math.random() * (maxWidth - spacing);
|
||||
y = Math.random() * (maxHeight - spacing);
|
||||
} while (detectCollision(x, y, cubePositions));
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
// Función para generar cubos con posiciones aleatorias
|
||||
function generateRandomCubes(cubeCount) {
|
||||
for (let i = 0; i < cubeCount; i++) {
|
||||
const position = getRandomPosition(window.innerWidth, window.innerHeight);
|
||||
const cubeContainer = document.createElement('div');
|
||||
cubeContainer.classList.add('cube-container');
|
||||
cubeContainer.setAttribute('tabindex', '0');
|
||||
cubeContainer.style.top = position.y + 'px';
|
||||
cubeContainer.style.left = position.x + 'px';
|
||||
|
||||
const cube = document.createElement('div');
|
||||
cube.className = 'cube';
|
||||
[['front', `País ${i}`], ['back', `Detalle ${i}`],
|
||||
['left', `Año ${2000 + i}`], ['right', `Muertos ${(i + 1) * 1000}`],
|
||||
['top', 'ONU'], ['bottom', 'Fuente']].forEach(([cls, txt]) => {
|
||||
const face = document.createElement('div');
|
||||
face.className = `face ${cls}`;
|
||||
face.textContent = txt;
|
||||
cube.appendChild(face);
|
||||
});
|
||||
cubeContainer.appendChild(cube);
|
||||
|
||||
// Crear un popup para cada cubo
|
||||
const popup = document.createElement('div');
|
||||
popup.classList.add('popup');
|
||||
const p = document.createElement('p');
|
||||
p.textContent = `Noticia relacionada con País ${i}`;
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = 'Close';
|
||||
popup.appendChild(p);
|
||||
popup.appendChild(btn);
|
||||
|
||||
popup.querySelector('button').addEventListener('click', () => {
|
||||
popup.style.display = 'none';
|
||||
cubeContainer.focus(); // Regresar el foco al cubo
|
||||
});
|
||||
|
||||
cubeContainer.addEventListener('focus', () => {
|
||||
popup.style.display = 'block';
|
||||
});
|
||||
|
||||
cubePositions.push({ x: position.x + 35, y: position.y + 35 }); // Guardar la posición
|
||||
graphContainer.appendChild(cubeContainer);
|
||||
graphContainer.appendChild(popup); // Añadir el popup después del cubo
|
||||
}
|
||||
}
|
||||
|
||||
// Función para generar las líneas entre cubos relacionados
|
||||
function generateLines() {
|
||||
for (let i = 0; i < cubePositions.length - 1; i++) {
|
||||
const x1 = cubePositions[i].x;
|
||||
const y1 = cubePositions[i].y;
|
||||
const x2 = cubePositions[i + 1].x;
|
||||
const y2 = cubePositions[i + 1].y;
|
||||
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', x1);
|
||||
line.setAttribute('y1', y1);
|
||||
line.setAttribute('x2', x2);
|
||||
line.setAttribute('y2', y2);
|
||||
line.setAttribute('stroke', '#333');
|
||||
line.setAttribute('stroke-width', '2');
|
||||
|
||||
svgLines.appendChild(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Generar cubos aleatorios y las líneas de conexión
|
||||
generateRandomCubes(numCubes);
|
||||
generateLines();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,463 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>UP-LEAKS // panel</title>
|
||||
<style>
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }
|
||||
:root {
|
||||
--neon: #39ff14; --neon-soft: #7dff5e; --neon-deep: #0e3a08; --bg: #000; --panel: #080d08; --panel-2: #0b120b;
|
||||
--line: #1c4a14; --line-soft: #12300d; --ink: #d7f5d0; --muted: #6f9a6a; --faint: #46603f;
|
||||
--red: #ff2d2d; --amber: #ffb800;
|
||||
--mono: 'Fira Code', ui-monospace, Menlo, Consolas, monospace; --glow: 0 0 8px rgba(57,255,20,.55);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--ink); font-family: var(--mono); font-size: 14px; line-height: 1.6;
|
||||
background-image: repeating-linear-gradient(0deg, rgba(57,255,20,.015) 0 1px, transparent 1px 3px);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
a { color: var(--neon); }
|
||||
.wrap { max-width: 1000px; margin: 0 auto; padding: 0 20px; }
|
||||
::selection { background: var(--neon); color: #000; }
|
||||
|
||||
header.top { border-bottom: 1px solid var(--line); }
|
||||
.top .wrap { display: flex; align-items: center; justify-content: space-between; height: 60px; }
|
||||
.brand { font-weight: 700; letter-spacing: .2em; }
|
||||
.brand b { color: var(--neon); text-shadow: var(--glow); }
|
||||
.brand .sub { color: var(--muted); font-weight: 400; font-size: 12px; letter-spacing: .1em; }
|
||||
|
||||
button {
|
||||
background: transparent; color: var(--neon); border: 1px solid var(--neon); font-family: var(--mono);
|
||||
font-weight: 700; padding: 10px 20px; cursor: pointer; text-transform: uppercase; letter-spacing: .06em; font-size: 12.5px;
|
||||
transition: background .16s, color .16s, box-shadow .16s;
|
||||
}
|
||||
button:hover:not(:disabled) { background: var(--neon); color: #000; box-shadow: 0 0 16px -2px rgba(57,255,20,.7); }
|
||||
button:disabled { opacity: .5; cursor: wait; }
|
||||
button.ghost { border-color: var(--line); color: var(--muted); }
|
||||
button.ghost:hover { border-color: var(--neon); color: var(--neon); background: transparent; box-shadow: none; }
|
||||
|
||||
/* gate */
|
||||
.gate { max-width: 460px; margin: 90px auto; border: 1px solid var(--line); background: var(--panel); padding: 30px; }
|
||||
.gate h1 { font-size: 17px; margin: 0 0 6px; font-weight: 500; }
|
||||
.gate h1::before { content: "// "; color: var(--neon); }
|
||||
.gate p { color: var(--muted); font-size: 12.5px; margin: 0 0 18px; }
|
||||
input[type="password"], input[type="text"] {
|
||||
width: 100%; background: var(--bg); border: 1px solid var(--line); color: var(--ink); padding: 12px; font-family: var(--mono); font-size: 14px;
|
||||
}
|
||||
input:focus { outline: none; border-color: var(--neon); box-shadow: 0 0 0 1px var(--neon), 0 0 14px -2px rgba(57,255,20,.5); }
|
||||
.gate .row { display: flex; gap: 10px; margin-top: 16px; }
|
||||
.gate-err { color: var(--red); font-size: 12.5px; margin-top: 12px; min-height: 1.2em; }
|
||||
|
||||
/* tabs + toolbar */
|
||||
.toolbar { display: flex; align-items: center; gap: 12px; padding: 20px 0 12px; flex-wrap: wrap; }
|
||||
.tabs { display: flex; gap: 6px; }
|
||||
.tabs button { padding: 8px 16px; font-size: 12px; }
|
||||
.tabs button.active { background: var(--neon-deep); color: var(--neon); border-color: var(--neon); }
|
||||
.spacer { flex: 1; }
|
||||
.updated { color: var(--faint); font-size: 11.5px; letter-spacing: .04em; }
|
||||
|
||||
/* ------- submissions ------- */
|
||||
.filters { display: flex; gap: 6px; margin: 4px 0 14px; }
|
||||
.filters button { padding: 6px 12px; font-size: 11px; }
|
||||
.filters button.active { background: var(--neon-deep); color: var(--neon); border-color: var(--neon); }
|
||||
.list { display: flex; flex-direction: column; gap: 12px; padding-bottom: 60px; }
|
||||
.sub { border: 1px solid var(--line); background: var(--panel); padding: 16px 18px; }
|
||||
.sub .head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.sub .ref { color: var(--neon); font-weight: 700; }
|
||||
.sub .date { color: var(--muted); font-size: 12px; }
|
||||
.sub .tema { color: var(--ink); font-size: 12px; border: 1px solid var(--line); padding: 1px 8px; }
|
||||
.badge { font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em; padding: 2px 8px; border: 1px solid; }
|
||||
.badge.nuevo { color: var(--neon); border-color: var(--neon); }
|
||||
.badge.revisado { color: var(--amber); border-color: var(--amber); }
|
||||
.badge.archivado { color: var(--faint); border-color: var(--line); }
|
||||
.sub .msg { white-space: pre-wrap; word-break: break-word; color: var(--ink); font-size: 13.5px; margin: 8px 0; }
|
||||
.sub .contacto { color: var(--muted); font-size: 12.5px; }
|
||||
.sub .contacto b { color: var(--ink); }
|
||||
.files { display: flex; flex-direction: column; gap: 5px; margin: 10px 0 4px; }
|
||||
.files a { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; cursor: pointer; }
|
||||
.files a::before { content: "⬇ "; color: var(--neon); }
|
||||
.actions { display: flex; gap: 8px; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); }
|
||||
.actions button { padding: 5px 12px; font-size: 10.5px; }
|
||||
.empty { color: var(--muted); text-align: center; padding: 50px; }
|
||||
|
||||
/* ------- system (htop) ------- */
|
||||
.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
|
||||
@media (max-width: 720px) { .tiles { grid-template-columns: repeat(2, 1fr); } }
|
||||
.tile { border: 1px solid var(--line); background: var(--panel); padding: 14px 16px; }
|
||||
.tile .k { font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase; color: var(--faint); }
|
||||
.tile .v { font-size: 24px; font-weight: 700; margin: 6px 0 8px; color: var(--ink); font-variant-numeric: tabular-nums; }
|
||||
.tile .v small { font-size: 13px; color: var(--muted); font-weight: 400; }
|
||||
|
||||
.sec-title { font-size: 11.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); margin: 22px 0 12px; }
|
||||
.sec-title::before { content: "// "; color: var(--neon); }
|
||||
|
||||
.meter { height: 9px; background: rgba(57,255,20,.08); border: 1px solid var(--line-soft); position: relative; overflow: hidden; }
|
||||
.meter > span { position: absolute; left: 0; top: 0; bottom: 0; background: linear-gradient(90deg, var(--neon), var(--neon-soft)); transition: width .5s ease; }
|
||||
.meter.warn > span { background: linear-gradient(90deg, #b8860b, var(--amber)); }
|
||||
.meter.crit > span { background: linear-gradient(90deg, #a01d1d, var(--red)); }
|
||||
|
||||
.svcs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
|
||||
.svc { font-size: 11px; padding: 4px 10px; border: 1px solid; letter-spacing: .04em; }
|
||||
.svc.up { color: var(--neon); border-color: var(--neon); }
|
||||
.svc.down { color: var(--red); border-color: var(--red); }
|
||||
.svc::before { content: "● "; }
|
||||
|
||||
.procs { border: 1px solid var(--line); background: var(--panel); }
|
||||
.prow { display: grid; grid-template-columns: 1.6fr 60px 1.3fr 70px 66px; gap: 10px; align-items: center;
|
||||
padding: 10px 14px; border-bottom: 1px solid var(--line-soft); font-size: 12.5px; }
|
||||
.prow:last-child { border-bottom: none; }
|
||||
.prow.hdr { color: var(--faint); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.prow .name { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.prow .name.viejo { color: var(--red); }
|
||||
.prow .cpu { text-align: right; font-variant-numeric: tabular-nums; color: var(--neon); }
|
||||
.prow .mem, .prow .time, .prow .pid { color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.prow .time { text-align: right; } .prow .pid { text-align: right; color: var(--faint); }
|
||||
|
||||
.prog { display: flex; flex-direction: column; gap: 14px; margin-bottom: 40px; }
|
||||
.prog .item .top { display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 12.5px; }
|
||||
.prog .item .lbl { color: var(--ink); } .prog .item .val { color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.prog .item .val b { color: var(--neon); }
|
||||
.kv { display: flex; gap: 24px; flex-wrap: wrap; margin-top: 4px; font-size: 12.5px; color: var(--muted); }
|
||||
.kv b { color: var(--neon); }
|
||||
|
||||
/* ------- pipelines ------- */
|
||||
#pipesView { padding-bottom: 50px; }
|
||||
.pipe { border: 1px solid var(--line); background: var(--panel); padding: 20px 22px; margin-bottom: 14px; }
|
||||
.pipe .phead { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.pipe .ptitle { font-size: 15.5px; font-weight: 700; color: var(--ink); }
|
||||
.pipe .pcad { color: var(--faint); font-weight: 400; font-size: 12px; }
|
||||
.pstate { font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em; padding: 3px 10px; border: 1px solid; white-space: nowrap; }
|
||||
.pstate.e-run { color: var(--neon); border-color: var(--neon); }
|
||||
.pstate.e-idle { color: var(--faint); border-color: var(--line); }
|
||||
.pstate.e-err { color: var(--red); border-color: var(--red); }
|
||||
.steps-bar, .flow { display: flex; gap: 5px; align-items: stretch; }
|
||||
.flow { align-items: center; gap: 2px; flex-wrap: wrap; }
|
||||
.seg { flex: 1; min-width: 92px; border: 1px solid var(--line-soft); padding: 12px 8px; display: flex; flex-direction: column; gap: 4px; align-items: center; justify-content: center; min-height: 54px; text-align: center; }
|
||||
.seg .sl { font-size: 12px; color: var(--ink); }
|
||||
.seg .sd { font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.seg.done { background: var(--neon-deep); border-color: var(--neon); }
|
||||
.seg.done .sl { color: var(--neon); }
|
||||
.seg.run { border-color: var(--neon); background: rgba(57,255,20,.1); animation: pulse 1.4s ease-in-out infinite; }
|
||||
.seg.run .sl { color: var(--neon); text-shadow: var(--glow); }
|
||||
.seg.err { border-color: var(--red); } .seg.err .sl, .seg.err .sd { color: var(--red); }
|
||||
.seg.pend { opacity: .5; } .seg.pend .sl { color: var(--faint); }
|
||||
.seg.live { border-color: var(--neon); } .seg.live .sl { color: var(--neon); }
|
||||
.flow .arrow { color: var(--faint); font-size: 15px; padding: 0 2px; }
|
||||
@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(57,255,20,0); } 50% { box-shadow: 0 0 16px -2px rgba(57,255,20,.6); } }
|
||||
@media (prefers-reduced-motion: reduce) { .seg.run { animation: none; } }
|
||||
.bigbar { height: 26px; background: rgba(57,255,20,.08); border: 1px solid var(--line); position: relative; overflow: hidden; margin-bottom: 12px; }
|
||||
.bigbar > span { position: absolute; left: 0; top: 0; bottom: 0; background: linear-gradient(90deg, var(--neon), var(--neon-soft)); transition: width .6s ease; }
|
||||
.bigbar > em { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-style: normal; font-size: 12px; font-weight: 700; color: #fff; mix-blend-mode: difference; }
|
||||
.pmeta { display: flex; gap: 22px; flex-wrap: wrap; font-size: 12.5px; color: var(--muted); }
|
||||
.pmeta b { color: var(--neon); }
|
||||
.pstats { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line-soft); font-size: 12px; color: var(--muted); }
|
||||
.pstats b { color: var(--ink); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="top">
|
||||
<div class="wrap">
|
||||
<span class="brand"><b>UP</b>-LEAKS <span class="sub">// panel de control</span></span>
|
||||
<button class="ghost" id="logoutBtn" style="display:none">salir</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="wrap" id="gateWrap">
|
||||
<div class="gate">
|
||||
<h1>acceso restringido</h1>
|
||||
<p>Introduce el token de administración.</p>
|
||||
<input type="password" id="tokenInput" placeholder="ADMIN_TOKEN" autocomplete="off">
|
||||
<div class="row"><button id="loginBtn">entrar</button></div>
|
||||
<div class="gate-err" id="gateErr"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap" id="panel" style="display:none">
|
||||
<div class="toolbar">
|
||||
<div class="tabs">
|
||||
<button data-tab="envios" class="active">envíos</button>
|
||||
<button data-tab="pipelines">pipelines</button>
|
||||
<button data-tab="sistema">sistema</button>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
<span class="updated" id="updated"></span>
|
||||
<button class="ghost" id="refreshBtn">refrescar</button>
|
||||
</div>
|
||||
|
||||
<!-- ENVÍOS -->
|
||||
<div id="enviosView">
|
||||
<div class="filters" id="filters">
|
||||
<button data-f="todos" class="active">todos</button>
|
||||
<button data-f="nuevo">nuevos</button>
|
||||
<button data-f="revisado">revisados</button>
|
||||
<button data-f="archivado">archivados</button>
|
||||
</div>
|
||||
<div class="list" id="list"></div>
|
||||
</div>
|
||||
|
||||
<!-- PIPELINES -->
|
||||
<div id="pipesView" style="display:none">
|
||||
<div id="pipesView-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- SISTEMA -->
|
||||
<div id="systemView" style="display:none">
|
||||
<div class="tiles" id="tiles"></div>
|
||||
<div class="sec-title">servicios</div>
|
||||
<div class="svcs" id="svcs"></div>
|
||||
<div class="sec-title">procesos en marcha</div>
|
||||
<div class="procs" id="procs"></div>
|
||||
<div class="sec-title">procesamiento</div>
|
||||
<div class="prog" id="prog"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var TOKEN = sessionStorage.getItem('upl_admin_token') || '';
|
||||
var subsData = [], filter = 'todos', tab = 'envios', sysTimer = null;
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
function authHeaders() { return { 'Authorization': 'Bearer ' + TOKEN }; }
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }
|
||||
function fmtDate(d) { try { return new Date(d).toLocaleString('es-ES'); } catch (e) { return d; } }
|
||||
function human(b) { if (b < 1024) return b + ' B'; if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'; if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB'; return (b / 1073741824).toFixed(1) + ' GB'; }
|
||||
function dur(s) { s = Math.max(0, Math.round(s)); var d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60); if (d) return d + 'd ' + h + 'h'; if (h) return h + 'h ' + m + 'm'; if (m) return m + 'm ' + (s % 60) + 's'; return s + 's'; }
|
||||
function rel(ts) { var s = (Date.now() - new Date(ts).getTime()) / 1000; if (s < 60) return 'hace ' + Math.round(s) + 's'; if (s < 3600) return 'hace ' + Math.round(s / 60) + ' min'; if (s < 86400) return 'hace ' + Math.round(s / 3600) + ' h'; return 'hace ' + Math.round(s / 86400) + ' d'; }
|
||||
function meter(pct, cls) { pct = Math.max(0, Math.min(100, pct)); return '<div class="meter ' + (cls || '') + '"><span style="width:' + pct + '%"></span></div>'; }
|
||||
|
||||
function showPanel(show) { $('gateWrap').style.display = show ? 'none' : ''; $('panel').style.display = show ? '' : 'none'; $('logoutBtn').style.display = show ? '' : 'none'; }
|
||||
|
||||
/* ---------- ENVÍOS ---------- */
|
||||
function loadSubs() {
|
||||
return fetch('/api/submissions?limit=500', { headers: authHeaders() })
|
||||
.then(function (r) {
|
||||
if (r.status === 401) throw new Error('Token incorrecto.');
|
||||
if (r.status === 503) throw new Error('Panel no configurado en el servidor (falta ADMIN_TOKEN).');
|
||||
return r.json();
|
||||
})
|
||||
.then(function (d) {
|
||||
if (!d.ok) throw new Error(d.error || 'Error');
|
||||
subsData = d.submissions || []; sessionStorage.setItem('upl_admin_token', TOKEN);
|
||||
showPanel(true); renderSubs(); markUpdated();
|
||||
});
|
||||
}
|
||||
function renderSubs() {
|
||||
var list = $('list');
|
||||
var items = subsData.filter(function (s) { return filter === 'todos' || s.estado === filter; });
|
||||
if (!items.length) { list.innerHTML = '<div class="empty">— sin envíos —</div>'; return; }
|
||||
list.innerHTML = '';
|
||||
items.forEach(function (s) {
|
||||
var el = document.createElement('div'); el.className = 'sub';
|
||||
var files = (s.archivos || []).map(function (f) { return '<a data-file="' + esc(f.guardado) + '" data-name="' + esc(f.nombre_original) + '">' + esc(f.nombre_original) + ' · ' + human(f.bytes || 0) + '</a>'; }).join('');
|
||||
el.innerHTML =
|
||||
'<div class="head"><span class="ref">' + esc(s.ref) + '</span><span class="date">' + esc(fmtDate(s.creado)) + '</span>' +
|
||||
'<span class="tema">' + esc(s.tema) + '</span><span class="badge ' + esc(s.estado) + '">' + esc(s.estado) + '</span></div>' +
|
||||
(s.mensaje ? '<div class="msg">' + esc(s.mensaje) + '</div>' : '<div class="msg" style="color:var(--faint)">(sin mensaje)</div>') +
|
||||
(s.contacto ? '<div class="contacto">contacto: <b>' + esc(s.contacto) + '</b></div>' : '') +
|
||||
(files ? '<div class="files">' + files + '</div>' : '') +
|
||||
'<div class="actions"><button data-act="revisado" data-ref="' + esc(s.ref) + '">marcar revisado</button>' +
|
||||
'<button data-act="archivado" data-ref="' + esc(s.ref) + '">archivar</button>' +
|
||||
'<button data-act="nuevo" data-ref="' + esc(s.ref) + '" class="ghost">reabrir</button></div>';
|
||||
list.appendChild(el);
|
||||
});
|
||||
}
|
||||
function downloadFile(g, name) {
|
||||
fetch('/api/submissions/file/' + encodeURIComponent(g), { headers: authHeaders() })
|
||||
.then(function (r) { if (!r.ok) throw new Error(); return r.blob(); })
|
||||
.then(function (blob) { var u = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = u; a.download = name || g; document.body.appendChild(a); a.click(); a.remove(); setTimeout(function () { URL.revokeObjectURL(u); }, 4000); })
|
||||
.catch(function () { alert('No se pudo descargar el archivo.'); });
|
||||
}
|
||||
function setEstado(ref, estado) {
|
||||
fetch('/api/submissions/' + encodeURIComponent(ref) + '/estado', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()), body: JSON.stringify({ estado: estado }) })
|
||||
.then(function (r) { return r.json(); }).then(function (d) { if (d.ok) { var it = subsData.find(function (s) { return s.ref === ref; }); if (it) it.estado = estado; renderSubs(); } });
|
||||
}
|
||||
|
||||
/* ---------- SISTEMA ---------- */
|
||||
function loadSystem() {
|
||||
return fetch('/api/system', { headers: authHeaders() })
|
||||
.then(function (r) { if (r.status === 401) throw new Error('Token incorrecto.'); return r.json(); })
|
||||
.then(function (d) { if (!d.ok) throw new Error(d.error || 'Error'); renderSystem(d); renderPipes(d); markUpdated(); });
|
||||
}
|
||||
function statsLine(fases) {
|
||||
var f = (fases || []).find(function (x) { return x.stats && Object.keys(x.stats).length; });
|
||||
if (!f) return '';
|
||||
var parts = Object.keys(f.stats).slice(0, 5).map(function (k) { return esc(k.replace(/_/g, ' ')) + ': <b>' + esc(f.stats[k]) + '</b>'; });
|
||||
return '<div class="pstats">' + esc(f.label) + ' → ' + parts.join(' · ') + '</div>';
|
||||
}
|
||||
function renderPipes(d) {
|
||||
var out = '';
|
||||
var pr = d.progress;
|
||||
var procBy = function (k) { return (d.procesos || []).find(function (x) { return x.key === k; }); };
|
||||
|
||||
/* ---- 1 · Comparación semántica v2 (el motor nuevo, foco actual) ---- */
|
||||
var c = d.comparacion, motor = procBy('motor');
|
||||
if (c) {
|
||||
var badgeCls, badgeTxt;
|
||||
if (motor) { badgeCls = 'e-run'; badgeTxt = 'regenerando'; }
|
||||
else if (c.v2Activa) { badgeCls = 'e-run'; badgeTxt = 'activa en el grafo'; }
|
||||
else if (c.v2Aristas > 0) { badgeCls = 'e-idle'; badgeTxt = 'lista · standby'; }
|
||||
else { badgeCls = 'e-idle'; badgeTxt = 'sin generar'; }
|
||||
var srvV1 = !c.v2Activa, srvV2 = c.v2Activa;
|
||||
out += '<div class="pipe"><div class="phead"><span class="ptitle">Comparación semántica · v2 <span class="pcad">· noticias ↔ leaks por embeddings</span></span>' +
|
||||
'<span class="pstate ' + badgeCls + '">' + badgeTxt + '</span></div>' +
|
||||
'<div class="steps-bar">' +
|
||||
'<div class="seg ' + (srvV2 ? 'done' : 'pend') + '"><span class="sl">v2 semántica</span><span class="sd">' + c.v2Aristas.toLocaleString('es-ES') + ' aristas</span></div>' +
|
||||
'<div class="seg ' + (srvV1 ? 'live' : 'pend') + '"><span class="sl">v1 word-overlap</span><span class="sd">' + (c.v1Aristas >= 1e6 ? (c.v1Aristas / 1e6).toFixed(0) + 'M' : c.v1Aristas.toLocaleString('es-ES')) + ' aristas</span></div>' +
|
||||
'</div>' +
|
||||
'<div class="pmeta" style="margin-top:14px">' +
|
||||
'<span>sirve al grafo: <b>' + esc(c.activa) + '</b></span>' +
|
||||
'<span>posible_evento: <b>' + (c.v2Eventos || 0).toLocaleString('es-ES') + '</b></span>' +
|
||||
(c.modelo ? '<span>modelo: <b>' + esc(c.modelo) + '</b></span>' : '') +
|
||||
(motor ? '<span>regen: <b>' + dur(motor.etimes) + '</b> · CPU <b>' + motor.cpu.toFixed(0) + '%</b></span>' : '') +
|
||||
'</div>' +
|
||||
(c.v2Aristas > 0 && !c.v2Activa && !motor ? '<div class="pstats">v2 generada y en espera → falta el switch del front (<b>COMPARACIONES_COL=comparaciones_v2</b> + restart).</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/* ---- 2 · Backfill de embeddings (leaks → vectores) ---- */
|
||||
var bf = procBy('backfill');
|
||||
if (pr) {
|
||||
var noApto = pr.torrNoApto || 0;
|
||||
var bpct = pr.torrTotal ? pr.torrEmb / pr.torrTotal * 100 : 0;
|
||||
var procDone = (pr.torrEmb + noApto) >= pr.torrTotal;
|
||||
var bBadge = bf ? 'e-run' : (procDone ? 'e-idle' : 'e-idle');
|
||||
var bTxt = bf ? 'ejecutando' : (procDone ? 'completado' : 'detenido');
|
||||
out += '<div class="pipe"><div class="phead"><span class="ptitle">Backfill de embeddings <span class="pcad">· leaks → vectores</span></span>' +
|
||||
'<span class="pstate ' + bBadge + '">' + bTxt + '</span></div>' +
|
||||
'<div class="bigbar"><span style="width:' + Math.min(100, bpct).toFixed(1) + '%"></span><em>' + pr.torrEmb.toLocaleString('es-ES') + ' embebidos / ' + pr.torrTotal.toLocaleString('es-ES') + ' · ' + bpct.toFixed(0) + '%</em></div>' +
|
||||
'<div class="pmeta">' + (bf ? '<span>en marcha: <b>' + dur(bf.etimes) + '</b></span><span>CPU: <b>' + bf.cpu.toFixed(0) + '%</b></span>' : '<span>proceso no activo</span>') +
|
||||
'<span>no aptos: <b>' + noApto.toLocaleString('es-ES') + '</b></span>' +
|
||||
'<span>pendientes: <b>' + Math.max(0, pr.torrTotal - pr.torrEmb - noApto).toLocaleString('es-ES') + '</b></span>' +
|
||||
'<span>chunks: <b>' + pr.chunks.toLocaleString('es-ES') + '</b></span></div></div>';
|
||||
}
|
||||
|
||||
/* ---- 3 · Coconews flujo continuo (noticias) ---- */
|
||||
var stages = [['ingestor', 'Ingesta RSS'], ['translator', 'Traducción'], ['ner', 'Entidades'], ['embedder', 'Embedding']];
|
||||
var svcMap = {}; (d.services || []).forEach(function (s) { svcMap[s.name] = s.active; });
|
||||
var flow = stages.map(function (k) {
|
||||
var a = svcMap['coconews-' + k[0]];
|
||||
return '<div class="seg ' + (a ? 'live' : 'pend') + '"><span class="sl">' + k[1] + '</span><span class="sd">' + (a ? 'en vivo' : 'off') + '</span></div>';
|
||||
}).join('<span class="arrow">→</span>');
|
||||
out += '<div class="pipe"><div class="phead"><span class="ptitle">Coconews · flujo continuo <span class="pcad">· noticias en tiempo real</span></span>' +
|
||||
'<span class="pstate e-run">continuo</span></div><div class="flow">' + flow + '</div>' +
|
||||
(pr ? '<div class="pmeta" style="margin-top:14px"><span>noticias embebidas: <b>' + pr.notiEmb.toLocaleString('es-ES') + '</b> / ' + pr.notiTotal.toLocaleString('es-ES') + '</span>' + (pr.lastNoti ? '<span>última noticia: <b>' + rel(pr.lastNoti) + '</b></span>' : '') + '</div>' : '') + '</div>';
|
||||
|
||||
/* ---- 4 · Pipeline maestro word-overlap (legacy · retirado) ---- */
|
||||
var p = d.pipelines && d.pipelines.maestro;
|
||||
if (p) {
|
||||
var retirado = !p.habilitado && !p.running;
|
||||
var segs = p.fases.map(function (f) {
|
||||
var cls = retirado ? 'pend' : (f.estado === 'completado' ? 'done' : (f.estado === 'en_progreso' ? 'run' : (f.estado === 'error' ? 'err' : 'pend')));
|
||||
return '<div class="seg ' + cls + '"><span class="sl">' + esc(f.label) + '</span><span class="sd">' + (f.durSec != null ? dur(f.durSec) : '—') + '</span></div>';
|
||||
}).join('');
|
||||
var stCls, stTxt;
|
||||
if (retirado) { stCls = 'e-idle'; stTxt = 'retirado'; }
|
||||
else if (p.running) { stCls = 'e-run'; stTxt = 'ejecutando'; }
|
||||
else if (p.estado === 'error') { stCls = 'e-err'; stTxt = 'con errores'; }
|
||||
else { stCls = 'e-idle'; stTxt = 'en espera'; }
|
||||
var next = retirado ? 'desactivado' : (p.nextRun ? (p.running ? 'en ejecución' : 'en ' + dur((p.nextRun - d.now) / 1000)) : '—');
|
||||
out += '<div class="pipe" style="opacity:' + (retirado ? '.72' : '1') + '"><div class="phead"><span class="ptitle">Comparación word-overlap <span class="pcad">· motor viejo' + (retirado ? ', sustituido por v2' : ' · ' + esc(p.cadencia)) + '</span></span>' +
|
||||
'<span class="pstate ' + stCls + '">' + stTxt + '</span></div>' +
|
||||
'<div class="steps-bar">' + segs + '</div>' +
|
||||
'<div class="pmeta" style="margin-top:14px">' +
|
||||
'<span>última ejecución: <b>' + (p.lastDurSec != null ? dur(p.lastDurSec) : '—') + '</b>' + (p.lastFin ? ' (' + rel(p.lastFin) + ')' : '') + '</span>' +
|
||||
'<span>próxima: <b>' + next + '</b></span></div>' +
|
||||
(retirado ? '<div class="pstats">Timer <b>flujos-pipeline</b> deshabilitado. La colección <b>comparaciones</b> (word-overlap) se conserva intacta como respaldo.</div>' : statsLine(p.fases)) +
|
||||
'</div>';
|
||||
}
|
||||
$('pipesView-body').innerHTML = out || '<div class="empty">— sin datos de pipeline —</div>';
|
||||
}
|
||||
function renderSystem(d) {
|
||||
var h = d.host, load1 = h.load[0], loadPct = (load1 / h.cores) * 100;
|
||||
var loadCls = loadPct > 100 ? 'crit' : (loadPct > 70 ? 'warn' : '');
|
||||
var memUsed = h.totalmem - h.freemem, memPct = memUsed / h.totalmem * 100;
|
||||
var memCls = memPct > 90 ? 'crit' : (memPct > 75 ? 'warn' : '');
|
||||
$('tiles').innerHTML =
|
||||
tile('Carga (1m)', load1.toFixed(2) + ' <small>/ ' + h.cores + ' cores</small>', loadPct, loadCls) +
|
||||
tile('Memoria', human(memUsed) + ' <small>/ ' + human(h.totalmem) + '</small>', memPct, memCls) +
|
||||
tile('Núcleos', h.cores + ' <small>CPU</small>', null) +
|
||||
tile('Uptime', dur(h.uptime), null);
|
||||
|
||||
$('svcs').innerHTML = (d.services || []).map(function (s) {
|
||||
return '<span class="svc ' + (s.active ? 'up' : 'down') + '">' + esc(s.name) + '</span>';
|
||||
}).join('');
|
||||
|
||||
var procs = (d.procesos || []).slice().sort(function (a, b) { return b.cpu - a.cpu; });
|
||||
var rows = '<div class="prow hdr"><span>proceso</span><span style="text-align:right">cpu%</span><span>cpu</span><span style="text-align:right">tiempo</span><span style="text-align:right">pid</span></div>';
|
||||
rows += procs.map(function (p) {
|
||||
var barCls = p.cpu > 100 ? 'warn' : '';
|
||||
return '<div class="prow"><span class="name ' + (p.key === 'mongolo' ? 'viejo' : '') + '">' + esc(p.label) + '</span>' +
|
||||
'<span class="cpu">' + p.cpu.toFixed(0) + '%</span>' +
|
||||
'<span>' + meter(Math.min(100, p.cpu), barCls) + '</span>' +
|
||||
'<span class="time">' + dur(p.etimes) + '</span>' +
|
||||
'<span class="pid">' + p.pid + '</span></div>';
|
||||
}).join('');
|
||||
if (!procs.length) rows += '<div class="prow"><span class="name" style="color:var(--faint)">— sin procesos activos —</span></div>';
|
||||
$('procs').innerHTML = rows;
|
||||
|
||||
var pr = d.progress, out = '';
|
||||
if (pr) {
|
||||
out += progItem('Leaks embebidos (embeddings)', pr.torrEmb, pr.torrTotal);
|
||||
out += progItem('Noticias embebidas', pr.notiEmb, pr.notiTotal);
|
||||
out += progItem('Feeds activos', pr.feedsAct, pr.feedsAct, pr.feedsFallo + ' con fallos');
|
||||
var next = d.nextPipeline ? (d.nextPipeline > d.now ? 'en ' + dur((d.nextPipeline - d.now) / 1000) : 'inminente') : 'sin programar';
|
||||
out += '<div class="kv"><span>chunks de embeddings: <b>' + pr.chunks.toLocaleString('es-ES') + '</b></span>' +
|
||||
'<span>envíos nuevos: <b>' + pr.subsNuevos + '</b></span>' +
|
||||
'<span>próximo pipeline: <b>' + next + '</b></span>' +
|
||||
(pr.lastNoti ? '<span>última noticia: <b>' + rel(pr.lastNoti) + '</b></span>' : '') + '</div>';
|
||||
}
|
||||
$('prog').innerHTML = out;
|
||||
}
|
||||
function tile(k, v, pct, cls) {
|
||||
return '<div class="tile"><div class="k">' + k + '</div><div class="v">' + v + '</div>' + (pct == null ? '' : meter(pct, cls)) + '</div>';
|
||||
}
|
||||
function progItem(lbl, val, total, extra) {
|
||||
var pct = total ? (val / total * 100) : 0;
|
||||
return '<div class="item"><div class="top"><span class="lbl">' + esc(lbl) + '</span>' +
|
||||
'<span class="val"><b>' + val.toLocaleString('es-ES') + '</b> / ' + total.toLocaleString('es-ES') +
|
||||
' · ' + pct.toFixed(0) + '%' + (extra ? ' · ' + esc(extra) : '') + '</span></div>' + meter(pct) + '</div>';
|
||||
}
|
||||
|
||||
function markUpdated() { $('updated').textContent = 'actualizado ' + new Date().toLocaleTimeString('es-ES'); }
|
||||
|
||||
function switchTab(t) {
|
||||
tab = t;
|
||||
[].forEach.call(document.querySelectorAll('.tabs button'), function (b) { b.classList.toggle('active', b.dataset.tab === t); });
|
||||
$('enviosView').style.display = t === 'envios' ? '' : 'none';
|
||||
$('pipesView').style.display = t === 'pipelines' ? '' : 'none';
|
||||
$('systemView').style.display = t === 'sistema' ? '' : 'none';
|
||||
if (sysTimer) { clearInterval(sysTimer); sysTimer = null; }
|
||||
if (t === 'sistema' || t === 'pipelines') {
|
||||
loadSystem().catch(function (e) { alert(e.message); });
|
||||
sysTimer = setInterval(function () { loadSystem().catch(function () {}); }, 4000);
|
||||
} else { loadSubs().catch(function () {}); }
|
||||
}
|
||||
function refresh() { (tab === 'sistema' ? loadSystem() : loadSubs()).catch(function (e) { alert(e.message); }); }
|
||||
|
||||
/* ---------- eventos ---------- */
|
||||
$('loginBtn').addEventListener('click', function () {
|
||||
TOKEN = $('tokenInput').value.trim(); $('gateErr').textContent = '';
|
||||
if (!TOKEN) { $('gateErr').textContent = 'Introduce el token.'; return; }
|
||||
loadSubs().catch(function (e) { $('gateErr').textContent = e.message; showPanel(false); });
|
||||
});
|
||||
$('tokenInput').addEventListener('keydown', function (e) { if (e.key === 'Enter') $('loginBtn').click(); });
|
||||
$('refreshBtn').addEventListener('click', refresh);
|
||||
$('logoutBtn').addEventListener('click', function () { if (sysTimer) clearInterval(sysTimer); sysTimer = null; sessionStorage.removeItem('upl_admin_token'); TOKEN = ''; subsData = []; showPanel(false); $('tokenInput').value = ''; });
|
||||
document.querySelector('.tabs').addEventListener('click', function (e) { if (e.target.dataset.tab) switchTab(e.target.dataset.tab); });
|
||||
$('filters').addEventListener('click', function (e) { if (e.target.dataset.f) { filter = e.target.dataset.f; [].forEach.call(this.children, function (b) { b.classList.toggle('active', b.dataset.f === filter); }); renderSubs(); } });
|
||||
$('list').addEventListener('click', function (e) {
|
||||
var a = e.target.closest('a[data-file]'); if (a) { downloadFile(a.dataset.file, a.dataset.name); return; }
|
||||
var b = e.target.closest('button[data-act]'); if (b) setEstado(b.dataset.ref, b.dataset.act);
|
||||
});
|
||||
|
||||
if (TOKEN) loadSubs().catch(function () { showPanel(false); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,401 +0,0 @@
|
|||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Fira Code';
|
||||
text-shadow: 10px 10px 20px rgba(0, 255, 76, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Fira Code', monospace;
|
||||
background: #000000;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 70px;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: "Retrolift", sans-serif;
|
||||
font-size: 4em;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 6px 6px 6px #73ff00;
|
||||
margin-bottom: 10px;
|
||||
|
||||
}
|
||||
|
||||
.glob-war:hover .logo { text-shadow: 2px 2px 8px #39ff14; }
|
||||
.int-sec:hover .logo { text-shadow: 2px 2px 8px #ff69b4; }
|
||||
.climate:hover .logo { text-shadow: 2px 2px 8px #ff4500; }
|
||||
.eco-corp:hover .logo { text-shadow: 2px 2px 8px #006400; }
|
||||
.popl-up:hover .logo { text-shadow: 2px 2px 8px #00008b; }
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #000000;
|
||||
color: #ff1a1a;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 40px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 6em; /* incrementa la distancia entre los elementos */
|
||||
}
|
||||
.nav-links a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
|
||||
padding: 20px 40px;
|
||||
box-shadow: 0 0 0px rgba(28, 103, 241, 0);
|
||||
|
||||
/* Estilo de los botones */
|
||||
position:relative;
|
||||
border: none;
|
||||
|
||||
transition: .4s ease-in;
|
||||
z-index: 1;
|
||||
width: 40vw;
|
||||
height: 20vh;
|
||||
border:3vw;
|
||||
|
||||
align-items: center;
|
||||
border: 3px solid ;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.3);
|
||||
|
||||
/* Estilo hover de los botones */
|
||||
|
||||
box-shadow: 2vw 1vw;
|
||||
|
||||
border: 3px solid black ;
|
||||
}
|
||||
|
||||
.glob-war:hover { color: #39ff14; }
|
||||
.int-sec:hover { color: #ff69b4; }
|
||||
.climate:hover { color: #ff4500; }
|
||||
.eco-corp:hover { color: #00fff2; }
|
||||
.popl-up:hover { color: #0066ff; }
|
||||
|
||||
.glob-war {
|
||||
color: #FF4136;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.int-sec {
|
||||
color: #0074D9;
|
||||
background-color: darkblue;
|
||||
}
|
||||
|
||||
.climate {
|
||||
color: #2ECC40;
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
.eco-corp {
|
||||
color: #FFDC00;
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
.popl-up {
|
||||
color: #FF851B;
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
.background {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 99%;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.background a {
|
||||
display: block;
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background img {
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
object-fit: cover;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background a img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background img:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
#sidebar.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background-image: url("/images/flujos7.jpg");
|
||||
background-size: cover;
|
||||
color: #39ff14;
|
||||
padding: 30px;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(-90%);
|
||||
transition: transform 0.3s ease-out;
|
||||
overflow: auto;
|
||||
font-size:18px;
|
||||
z-index: 3;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; /* Añade el borde al texto */
|
||||
}
|
||||
|
||||
|
||||
|
||||
#sidebar h2 {
|
||||
color: #39ff14;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: -1px 0 black, 0 3px black, 3px 0 black, 0 -1px black; /* Añade el borde al texto */
|
||||
}
|
||||
|
||||
|
||||
#sidebar:hover {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#sidebarToggle {
|
||||
position: absolute;
|
||||
left: 1em;
|
||||
top: 1em;
|
||||
background: #007BFF;
|
||||
color: #39ff14;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#sidebarToggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sidebar form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
font-size:20px;
|
||||
}
|
||||
|
||||
#sidebar label {
|
||||
color: #39ff14;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; /* Añade el borde al texto */
|
||||
}
|
||||
|
||||
#sidebar input {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14; /* Añade el borde verde al input */
|
||||
background: black; /* Cambia el fondo del input a negro */
|
||||
color: #39ff14; /* Cambia el color del texto en el input a verde */
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
#sidebar input:hover {
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#sidebar input[type="submit"] {
|
||||
color: #39ff14;
|
||||
background: #ff6600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#sidebar input[type="submit"]:hover {
|
||||
background: #ff6600;
|
||||
}
|
||||
|
||||
#sidebar select {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#sidebar select:hover { box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3); }
|
||||
#sidebar select option { background: #0a0a0a; color: #39ff14; }
|
||||
#sidebar .sim-val {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(57,255,20,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
background-color: #000000;
|
||||
color: #39ff14;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #39ff14;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #2ECC40;
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#climateContainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.5;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.nav-links a {
|
||||
font-size: 0.8em; /* reduce el tamaño de la fuente */
|
||||
padding: 10px 20px; /* reduce el padding */
|
||||
}
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2em; /* incrementa la distancia entre los elementos */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.fade-out {
|
||||
animation: fadeOut 2s forwards; /* Fades out over 3 seconds */
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {opacity: 1;}
|
||||
to {opacity: 0;}
|
||||
}
|
||||
|
||||
|
||||
.grafico {
|
||||
z-index: 1; /* Bring to front */
|
||||
/* Rest of your styles */
|
||||
}
|
||||
|
||||
|
||||
.grafico {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ===== Panel de detalle (split-screen) ===== */
|
||||
main.split-screen { display: flex; height: 100vh; }
|
||||
#graphPanel { flex: 1; position: relative; min-width: 0; transition: flex 0.3s ease; }
|
||||
#detailPanel {
|
||||
width: 0; max-width: 0; overflow: hidden;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
background: #0a0a0a; color: #fff;
|
||||
position: relative; z-index: 4;
|
||||
box-shadow: inset 2px 0 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel { width: 50%; max-width: 50%; }
|
||||
|
||||
#closeDetail {
|
||||
position: absolute; top: 12px; left: 12px;
|
||||
background: rgba(0,0,0,0.85); border: 1px solid #39ff14;
|
||||
color: #39ff14; font-size: 1em; font-family: 'Fira Code', monospace;
|
||||
cursor: pointer; border-radius: 4px; padding: 4px 12px; z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#closeDetail:hover { background: rgba(57,255,20,0.15); }
|
||||
|
||||
#detailContent {
|
||||
padding: 52px 18px 20px; overflow-y: auto;
|
||||
height: 100%; box-sizing: border-box;
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
main.split-screen.show-detail #detailContent { display: flex; }
|
||||
|
||||
.detail-meta { display: flex; justify-content: space-between; font-size: 0.72em; color: #39ff14; opacity: 0.75; margin-bottom: 6px; gap: 8px; }
|
||||
.detail-author { font-weight: bold; }
|
||||
.detail-date { text-align: right; white-space: nowrap; }
|
||||
.detail-title { font-size: 0.92em; color: #39ff14; margin: 0 0 12px; word-break: break-word; line-height: 1.3; }
|
||||
|
||||
.detail-relations { border-top: 1px solid rgba(57,255,20,0.25); border-bottom: 1px solid rgba(57,255,20,0.25); padding: 8px 0; margin-bottom: 14px; font-size: 0.78em; flex-shrink: 0; }
|
||||
.relations-count { color: rgba(57,255,20,0.8); display: block; margin-bottom: 6px; }
|
||||
.relations-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.relations-nav button { background: none; border: 1px solid #39ff14; color: #39ff14; cursor: pointer; padding: 3px 8px; border-radius: 3px; font-family: 'Fira Code', monospace; font-size: 0.85em; white-space: nowrap; transition: background 0.2s; }
|
||||
.relations-nav button:hover:not(:disabled) { background: rgba(57,255,20,0.12); }
|
||||
.relations-nav button:disabled { opacity: 0.3; cursor: default; }
|
||||
#relationLabel { flex: 1; text-align: center; color: rgba(255,255,255,0.55); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }
|
||||
|
||||
.detail-body { flex: 1; overflow-y: auto; white-space: pre-wrap; line-height: 1.55; font-size: 0.78em; color: #e0e0e0; font-family: 'Fira Code', monospace; min-height: 0; }
|
||||
.detail-img { width: 100%; max-height: 180px; object-fit: contain; margin-bottom: 12px; border: 1px solid rgba(57,255,20,0.3); border-radius: 3px; }
|
||||
#detailPanel .placeholder { color: rgba(255,255,255,0.3); font-style: italic; padding: 40px 20px; text-align: center; font-size: 0.85em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main.split-screen.show-detail #graphPanel { display: none; }
|
||||
main.split-screen.show-detail #detailPanel { width: 100%; max-width: 100%; }
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>CLIMATE</title>
|
||||
<link rel="stylesheet" href="climate.css">
|
||||
<link rel="stylesheet" href="sub-nav.css">
|
||||
<!-- Fuentes de Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="top-nav">
|
||||
<div class="section-buttons">
|
||||
|
||||
<div class="section-item">
|
||||
<a href="glob-war.html" class="section-btn glob-war-btn">GLOB-WAR</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="int-sec.html" class="section-btn int-sec-btn">INT-SEC</a>
|
||||
</div>
|
||||
|
||||
<div class="section-item current">
|
||||
<a href="climate.html" class="section-btn climate-btn">CLIMATE <span class="dropdown-arrow">▾</span></a>
|
||||
<div class="section-dropdown">
|
||||
<a href="#" onclick="setSubtema('cambio climático');return false;">Cambio Climático</a>
|
||||
<a href="#" onclick="setSubtema('desastres naturales');return false;">Desastres Naturales</a>
|
||||
<a href="#" onclick="setSubtema('conservación');return false;">Conservación</a>
|
||||
<a href="#" onclick="setSubtema('energía renovable');return false;">Energía Renovable</a>
|
||||
<a href="#" onclick="setSubtema('contaminacion');return false;">Escasez de Agua</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-item">
|
||||
<a href="eco-corp.html" class="section-btn eco-corp-btn">ECO-CORP</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="popl-up.html" class="section-btn popl-up-btn">POPL-UP</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function setSubtema(val) {
|
||||
var sel = document.getElementById('param2');
|
||||
if (sel) { sel.value = val; }
|
||||
var form = document.getElementById('paramForm');
|
||||
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* ── Dropdown touch (móvil) ── */
|
||||
var curr = document.querySelector('.section-item.current');
|
||||
if (curr) {
|
||||
curr.querySelector('.section-btn').addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
curr.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('touchend', function(e) {
|
||||
if (!curr.contains(e.target)) curr.classList.remove('open');
|
||||
});
|
||||
}
|
||||
/* ── Sidebar: botón toggle visible en móvil ── */
|
||||
var toggle = document.getElementById('sidebarToggle');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.style.display = 'block';
|
||||
toggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="split-screen">
|
||||
<div id="graphPanel" style="position:relative;">
|
||||
<button id="volverGrafoBtn">← Volver al grafo completo</button>
|
||||
<div id="climateContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<img src="/images/flujos6.jpg">
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div id="detailPanel">
|
||||
<button id="closeDetail">✕ cerrar</button>
|
||||
<div id="detailContent">
|
||||
<div class="detail-meta">
|
||||
<span class="detail-author"></span>
|
||||
<span class="detail-date"></span>
|
||||
</div>
|
||||
<h2 class="detail-title"></h2>
|
||||
<div class="detail-relations">
|
||||
<span class="relations-count"></span>
|
||||
<div class="relations-nav">
|
||||
<button id="prevRelation" disabled>← ant</button>
|
||||
<span id="relationLabel"></span>
|
||||
<button id="nextRelation" disabled>sig →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="sidebar">
|
||||
<h2>Parámetros</h2>
|
||||
|
||||
<form id="paramForm">
|
||||
<label for="fecha_inicio">Fecha de inicio:</label>
|
||||
<input type="date" id="fecha_inicio" name="fecha_inicio">
|
||||
|
||||
<label for="fecha_fin">Fecha de fin:</label>
|
||||
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||
|
||||
<label for="nodos">Nodos:</label>
|
||||
<input type="number" id="nodos" name="nodos" value="100" min="5" max="500">
|
||||
|
||||
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="25" value="8"
|
||||
oninput="document.getElementById('complejidadVal').textContent = this.value">
|
||||
|
||||
<label for="param1">Palabra clave:</label>
|
||||
<input type="text" id="param1" name="param1" placeholder="ej: glaciares, CO2...">
|
||||
|
||||
<label for="param2">Subtematica:</label>
|
||||
<select id="param2" name="param2">
|
||||
<option value="">— Todas —</option>
|
||||
<option value="conservación">conservación</option>
|
||||
<option value="cambio climático">cambio climático</option>
|
||||
<option value="energía renovable">energía renovable</option>
|
||||
<option value="desastres naturales">desastres naturales</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Aplicar">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Movemos la inclusión del script aquí, al final del body -->
|
||||
<script src="semana-toggle.js"></script>
|
||||
<script type="module" src="output_climate_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,394 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>COCONEWS</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Oswald:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="header-inner">
|
||||
<a href="/" class="back-link">← FLUJOS</a>
|
||||
<div class="logo">COCO<span>NEWS</span></div>
|
||||
<div class="header-tabs">
|
||||
<button class="tab-btn active" data-tab="news">NOTICIAS</button>
|
||||
<button class="tab-btn" data-tab="entities">ENTIDADES</button>
|
||||
<button class="tab-btn" data-tab="feeds">FEEDS</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stats-bar" id="statsBar">
|
||||
<div class="stat-item"><span class="stat-val" id="statTotal">—</span><span class="stat-label">noticias</span></div>
|
||||
<div class="stat-sep">|</div>
|
||||
<div class="stat-item"><span class="stat-val" id="statHoy">—</span><span class="stat-label">hoy</span></div>
|
||||
<div class="stat-sep">|</div>
|
||||
<div class="stat-item"><span class="stat-val" id="statFeeds">—</span><span class="stat-label">feeds</span></div>
|
||||
<div class="stat-sep">|</div>
|
||||
<div class="stat-item"><span class="stat-val" id="statTrad">—</span><span class="stat-label">traducidas</span></div>
|
||||
<div class="stat-sep">|</div>
|
||||
<div class="stat-item"><span class="stat-val" id="statNer">—</span><span class="stat-label">con NER</span></div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: NOTICIAS -->
|
||||
<div class="tab-content active" id="tab-news">
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="filter-group">
|
||||
<label>BUSCAR</label>
|
||||
<input type="text" id="searchInput" placeholder="palabra clave...">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>TEMA</label>
|
||||
<select id="temaSelect">
|
||||
<option value="">— todos —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>PAÍS / REGIÓN</label>
|
||||
<input type="text" id="paisInput" placeholder="ej: Rusia, China...">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>ENTIDAD</label>
|
||||
<input type="text" id="entidadInput" placeholder="persona u organización...">
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button id="applyBtn">APLICAR</button>
|
||||
<button id="resetBtn" class="btn-sec">RESET</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">TOP TEMAS</div>
|
||||
<div id="temasBars"></div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">IDIOMAS</div>
|
||||
<div id="idiomasBars"></div>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<button id="prevBtn" disabled>← PREV</button>
|
||||
<span id="pageInfo">1 / 1</span>
|
||||
<button id="nextBtn">NEXT →</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="news-grid" id="newsGrid">
|
||||
<div class="loading">cargando...</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: ENTIDADES -->
|
||||
<div class="tab-content" id="tab-entities">
|
||||
<div class="entities-layout">
|
||||
<div class="entity-panel">
|
||||
<div class="entity-panel-header">
|
||||
<span>PERSONAS</span>
|
||||
<div class="tipo-tabs">
|
||||
<button class="tipo-btn active" data-tipo="persona">PERSONAS</button>
|
||||
<button class="tipo-btn" data-tipo="organizacion">ORGS</button>
|
||||
<button class="tipo-btn" data-tipo="lugar">LUGARES</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="entidadesList" class="entities-list"></div>
|
||||
</div>
|
||||
<div class="entity-news">
|
||||
<div id="entityNewsTitle" class="entity-news-title">Selecciona una entidad</div>
|
||||
<div id="entityNewsGrid" class="news-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: FEEDS -->
|
||||
<div class="tab-content" id="tab-feeds">
|
||||
<div class="feeds-container">
|
||||
<div id="feedsList" class="feeds-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/coconews/api';
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
let currentFilters = {};
|
||||
let currentTipo = 'persona';
|
||||
|
||||
// ── TABS ──────────────────────────────────────────────────────────────────
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
|
||||
if (btn.dataset.tab === 'entities') loadEntidades(currentTipo);
|
||||
if (btn.dataset.tab === 'feeds') loadFeeds();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.tipo-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tipo-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentTipo = btn.dataset.tipo;
|
||||
loadEntidades(currentTipo);
|
||||
});
|
||||
});
|
||||
|
||||
// ── UTILS ─────────────────────────────────────────────────────────────────
|
||||
async function fetchJSON(url) {
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(r.statusText);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function fmtDate(d) {
|
||||
if (!d) return '';
|
||||
return new Date(d).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
}
|
||||
|
||||
// ── STATS ─────────────────────────────────────────────────────────────────
|
||||
async function loadStats() {
|
||||
try {
|
||||
const s = await fetchJSON(`${API}/stats`);
|
||||
document.getElementById('statTotal').textContent = s.total_news.toLocaleString();
|
||||
document.getElementById('statHoy').textContent = s.hoy;
|
||||
document.getElementById('statFeeds').textContent = s.feeds_activos;
|
||||
document.getElementById('statTrad').textContent = s.traducidas.toLocaleString();
|
||||
document.getElementById('statNer').textContent = (s.con_ner || 0).toLocaleString();
|
||||
|
||||
renderBars('temasBars', s.por_tema, s.traducidas, (item) => {
|
||||
document.getElementById('temaSelect').value = item._id;
|
||||
currentFilters.tema = item._id;
|
||||
loadNews(1);
|
||||
});
|
||||
renderBars('idiomasBars', s.por_idioma, s.traducidas);
|
||||
} catch(e) { console.warn('Stats:', e); }
|
||||
}
|
||||
|
||||
function renderBars(containerId, items, total, onClick) {
|
||||
const el = document.getElementById(containerId);
|
||||
if (!items || !items.length) { el.innerHTML = '<div class="empty-bar">—</div>'; return; }
|
||||
el.innerHTML = items.slice(0, 8).map(item => {
|
||||
const pct = total > 0 ? Math.round((item.count / total) * 100) : 0;
|
||||
const cursor = onClick ? 'style="cursor:pointer"' : '';
|
||||
return `
|
||||
<div class="bar-row" ${cursor} data-id="${item._id}">
|
||||
<span class="bar-label">${item._id || '?'}</span>
|
||||
<div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
|
||||
<span class="bar-count">${item.count}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
if (onClick) {
|
||||
el.querySelectorAll('.bar-row').forEach(row => {
|
||||
row.addEventListener('click', () => onClick({ _id: row.dataset.id }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── TEMAS SELECT ──────────────────────────────────────────────────────────
|
||||
async function loadTemas() {
|
||||
try {
|
||||
const temas = await fetchJSON(`${API}/temas`);
|
||||
const sel = document.getElementById('temaSelect');
|
||||
temas.forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t; opt.textContent = t;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
} catch(e) { console.warn('Temas:', e); }
|
||||
}
|
||||
|
||||
// ── NEWS CARDS ────────────────────────────────────────────────────────────
|
||||
function buildCard(n) {
|
||||
const texto = n.texto || n.titulo_original || '';
|
||||
const titulo = texto.split('\n')[0].slice(0, 130);
|
||||
const resumen = texto.split('\n').slice(2).join(' ').slice(0, 220);
|
||||
const img = n.imagen_url
|
||||
? `<div class="card-img"><img src="${n.imagen_url}" alt="" loading="lazy" onerror="this.parentElement.style.display='none'"></div>`
|
||||
: '';
|
||||
const entidades = (n.entidades || []).slice(0, 4).map(e =>
|
||||
`<span class="ent-tag ent-${e.tipo}" title="${e.tipo}">${e.texto}</span>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<article class="card" onclick="window.open('${n.url}','_blank')">
|
||||
${img}
|
||||
<div class="card-body">
|
||||
<div class="card-meta">
|
||||
<span class="tag-tema">${n.tema || ''}</span>
|
||||
<span class="card-fuente">${n.fuente || ''}</span>
|
||||
<span class="card-fecha">${fmtDate(n.fecha)}</span>
|
||||
</div>
|
||||
<h2 class="card-title">${titulo}</h2>
|
||||
${resumen ? `<p class="card-resumen">${resumen}…</p>` : ''}
|
||||
<div class="card-footer">
|
||||
<div class="card-ents">${entidades}</div>
|
||||
<div class="card-badges">
|
||||
${n.subtema ? `<span class="badge badge-pais">${n.subtema}</span>` : ''}
|
||||
${n.lang && n.lang !== 'es' ? `<span class="badge badge-lang">${n.lang.toUpperCase()}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
async function loadNews(page = 1) {
|
||||
const grid = document.getElementById('newsGrid');
|
||||
grid.innerHTML = '<div class="loading">cargando...</div>';
|
||||
const params = new URLSearchParams({ page, per_page: 24, ...currentFilters });
|
||||
try {
|
||||
const data = await fetchJSON(`${API}/news?${params}`);
|
||||
totalPages = data.total_pages || 1;
|
||||
currentPage = data.page;
|
||||
document.getElementById('pageInfo').textContent = `${currentPage} / ${totalPages}`;
|
||||
document.getElementById('prevBtn').disabled = currentPage <= 1;
|
||||
document.getElementById('nextBtn').disabled = currentPage >= totalPages;
|
||||
if (!data.news || data.news.length === 0) {
|
||||
grid.innerHTML = '<div class="loading">sin resultados</div>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = data.news.map(buildCard).join('');
|
||||
} catch(e) {
|
||||
grid.innerHTML = `<div class="loading error">Error: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── ENTIDADES ─────────────────────────────────────────────────────────────
|
||||
const wikiCache = {};
|
||||
async function getWikiImage(name) {
|
||||
if (wikiCache[name] !== undefined) return wikiCache[name];
|
||||
try {
|
||||
const url = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(name)}`;
|
||||
const r = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
||||
if (!r.ok) throw new Error();
|
||||
const d = await r.json();
|
||||
wikiCache[name] = d.thumbnail ? d.thumbnail.source : null;
|
||||
} catch { wikiCache[name] = null; }
|
||||
return wikiCache[name];
|
||||
}
|
||||
|
||||
function initials(name) {
|
||||
return name.split(' ').map(w => w[0]).slice(0,2).join('').toUpperCase();
|
||||
}
|
||||
|
||||
async function loadEntidades(tipo) {
|
||||
const list = document.getElementById('entidadesList');
|
||||
list.innerHTML = '<div class="loading">cargando...</div>';
|
||||
try {
|
||||
const data = await fetchJSON(`${API}/entidades?tipo=${tipo}&limit=40`);
|
||||
if (!data.length) { list.innerHTML = '<div class="loading">sin datos aún — el NER necesita procesar noticias primero</div>'; return; }
|
||||
const max = data[0].count;
|
||||
|
||||
list.innerHTML = data.map(e => `
|
||||
<div class="ent-row" data-texto="${e.texto}">
|
||||
<div class="ent-avatar" id="av-${btoa(e.texto).replace(/[^a-z0-9]/gi,'')}">${initials(e.texto)}</div>
|
||||
<div class="ent-info">
|
||||
<span class="ent-name">${e.texto}</span>
|
||||
<div class="ent-bar-row">
|
||||
<div class="ent-bar-track"><div class="ent-bar-fill" style="width:${Math.round(e.count/max*100)}%"></div></div>
|
||||
<span class="ent-count-label">${e.count} menciones</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
|
||||
list.querySelectorAll('.ent-row').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
list.querySelectorAll('.ent-row').forEach(r => r.classList.remove('selected'));
|
||||
row.classList.add('selected');
|
||||
loadEntityNews(row.dataset.texto);
|
||||
});
|
||||
});
|
||||
|
||||
if (tipo === 'persona') {
|
||||
data.forEach(async e => {
|
||||
const img = await getWikiImage(e.texto);
|
||||
if (!img) return;
|
||||
const avId = 'av-' + btoa(e.texto).replace(/[^a-z0-9]/gi,'');
|
||||
const el = document.getElementById(avId);
|
||||
if (el) el.innerHTML = `<img src="${img}" alt="${e.texto}" onerror="this.parentElement.innerHTML='${initials(e.texto)}'">`;
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
list.innerHTML = `<div class="loading error">${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEntityNews(entidad) {
|
||||
document.getElementById('entityNewsTitle').innerHTML = `${entidad} <small>noticias relacionadas</small>`;
|
||||
const grid = document.getElementById('entityNewsGrid');
|
||||
grid.innerHTML = '<div class="loading">cargando...</div>';
|
||||
try {
|
||||
const data = await fetchJSON(`${API}/news?entidad=${encodeURIComponent(entidad)}&per_page=12`);
|
||||
if (!data.news || !data.news.length) { grid.innerHTML = '<div class="loading">sin noticias</div>'; return; }
|
||||
grid.innerHTML = data.news.map(buildCard).join('');
|
||||
} catch(e) {
|
||||
grid.innerHTML = `<div class="loading error">${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── FEEDS ─────────────────────────────────────────────────────────────────
|
||||
async function loadFeeds() {
|
||||
const list = document.getElementById('feedsList');
|
||||
list.innerHTML = '<div class="loading">cargando...</div>';
|
||||
try {
|
||||
const data = await fetchJSON(`${API}/feeds?per_page=100`);
|
||||
list.innerHTML = data.feeds.map(f => `
|
||||
<div class="feed-row ${f.activo ? '' : 'feed-off'}">
|
||||
<div class="feed-info">
|
||||
<span class="feed-nombre">${f.nombre}</span>
|
||||
<span class="feed-tema tag-tema">${f.tema || ''}</span>
|
||||
${f.subtema ? `<span class="feed-sub">${f.subtema}</span>` : ''}
|
||||
</div>
|
||||
<div class="feed-meta">
|
||||
<span class="feed-lang">${f.idioma || ''}</span>
|
||||
<span class="feed-ok ${f.activo ? 'ok' : 'off'}">${f.activo ? '● activo' : '○ inactivo'}</span>
|
||||
${f.fallos > 0 ? `<span class="feed-fallos">${f.fallos} fallos</span>` : ''}
|
||||
${f.ultimo_ok ? `<span class="feed-date">${fmtDate(f.ultimo_ok)}</span>` : ''}
|
||||
</div>
|
||||
</div>`).join('');
|
||||
} catch(e) {
|
||||
list.innerHTML = `<div class="loading error">${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── EVENTOS ───────────────────────────────────────────────────────────────
|
||||
document.getElementById('applyBtn').addEventListener('click', () => {
|
||||
currentFilters = {};
|
||||
const q = document.getElementById('searchInput').value.trim();
|
||||
const tema = document.getElementById('temaSelect').value;
|
||||
const pais = document.getElementById('paisInput').value.trim();
|
||||
const ent = document.getElementById('entidadInput').value.trim();
|
||||
if (q) currentFilters.q = q;
|
||||
if (tema) currentFilters.tema = tema;
|
||||
if (pais) currentFilters.pais = pais;
|
||||
if (ent) currentFilters.entidad = ent;
|
||||
loadNews(1);
|
||||
});
|
||||
|
||||
document.getElementById('resetBtn').addEventListener('click', () => {
|
||||
['searchInput','paisInput','entidadInput'].forEach(id => document.getElementById(id).value = '');
|
||||
document.getElementById('temaSelect').value = '';
|
||||
currentFilters = {};
|
||||
loadNews(1);
|
||||
});
|
||||
|
||||
document.getElementById('searchInput').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') document.getElementById('applyBtn').click();
|
||||
});
|
||||
|
||||
document.getElementById('prevBtn').addEventListener('click', () => loadNews(currentPage - 1));
|
||||
document.getElementById('nextBtn').addEventListener('click', () => loadNews(currentPage + 1));
|
||||
|
||||
// ── INIT ──────────────────────────────────────────────────────────────────
|
||||
loadStats();
|
||||
loadTemas();
|
||||
loadNews(1);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,413 +0,0 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Oswald:wght@400;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #000;
|
||||
--surface: #0d0d0d;
|
||||
--surface2: #1a1a1a;
|
||||
--border: #2a2a2a;
|
||||
--accent: #00ff6a;
|
||||
--accent2: #00c4ff;
|
||||
--warn: #ff6a00;
|
||||
--text: #fff;
|
||||
--muted: #888;
|
||||
--font-head: 'Oswald', Impact, 'Arial Narrow', sans-serif;
|
||||
--font-mono: 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
html, body { height: 100%; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
|
||||
/* ── HEADER ── */
|
||||
header {
|
||||
background: #000;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
padding: 0 24px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.header-inner { display: flex; align-items: center; gap: 28px; width: 100%; }
|
||||
.back-link { color: var(--muted); font-size: 11px; letter-spacing: 1px; white-space: nowrap; font-family: var(--font-mono); }
|
||||
.back-link:hover { color: var(--accent); }
|
||||
|
||||
.logo {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
font-size: 26px;
|
||||
letter-spacing: 4px;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.logo span { color: var(--accent); }
|
||||
|
||||
.header-tabs { display: flex; gap: 2px; margin-left: auto; }
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: var(--muted);
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
padding: 6px 18px 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.tab-btn:hover { color: var(--text); }
|
||||
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
|
||||
/* ── STATS BAR ── */
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 10px 28px;
|
||||
border-right: 1px solid var(--border);
|
||||
flex: 1;
|
||||
}
|
||||
.stat-val {
|
||||
font-family: var(--font-head);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
line-height: 1;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.stat-sep { display: none; }
|
||||
|
||||
/* ── TABS ── */
|
||||
.tab-content { display: none; flex: 1; min-height: 0; }
|
||||
.tab-content.active { display: flex; flex-direction: column; flex: 1; }
|
||||
|
||||
/* ── NEWS LAYOUT ── */
|
||||
.layout { display: grid; grid-template-columns: 240px 1fr; flex: 1; min-height: 0; overflow: hidden; }
|
||||
|
||||
/* ── SIDEBAR ── */
|
||||
.sidebar {
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 20px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-group { display: flex; flex-direction: column; gap: 6px; }
|
||||
.filter-group label {
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
input, select {
|
||||
background: #000;
|
||||
border: 1px solid var(--border);
|
||||
border-bottom: 2px solid var(--border);
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
padding: 8px 10px;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
input:focus, select:focus { border-color: var(--accent); border-bottom-color: var(--accent); }
|
||||
select option { background: #111; }
|
||||
|
||||
.btn-row { display: flex; gap: 6px; }
|
||||
|
||||
button {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border: none;
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
padding: 9px 14px;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.15s;
|
||||
flex: 1;
|
||||
}
|
||||
button:hover { background: #fff; }
|
||||
button:disabled { opacity: 0.25; cursor: not-allowed; }
|
||||
button.btn-sec {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
button.btn-sec:hover { color: var(--text); border-color: var(--text); background: transparent; }
|
||||
|
||||
.sidebar-section { display: flex; flex-direction: column; gap: 8px; }
|
||||
.sidebar-title {
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.bar-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 50px 30px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.bar-row:hover { opacity: 0.75; }
|
||||
.bar-label {
|
||||
font-size: 11px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
font-family: var(--font-head);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.bar-track { height: 5px; background: var(--border); overflow: hidden; }
|
||||
.bar-fill { height: 100%; background: var(--accent); transition: width 0.4s; }
|
||||
.bar-count { font-size: 11px; color: var(--muted); text-align: right; font-family: var(--font-head); }
|
||||
.empty-bar { font-size: 11px; color: var(--muted); }
|
||||
|
||||
.pagination { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 12px; border-top: 1px solid var(--border); }
|
||||
.pagination button { padding: 6px 10px; font-size: 11px; flex: none; }
|
||||
#pageInfo { font-size: 12px; color: var(--muted); text-align: center; flex: 1; font-family: var(--font-head); letter-spacing: 1px; }
|
||||
|
||||
/* ── NEWS GRID ── */
|
||||
.news-grid {
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 14px;
|
||||
align-content: start;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading { grid-column: 1/-1; padding: 60px; text-align: center; color: var(--muted); font-size: 13px; letter-spacing: 2px; }
|
||||
.loading.error { color: var(--warn); }
|
||||
|
||||
/* ── CARD ── */
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 24px rgba(0,255,106,0.1);
|
||||
}
|
||||
.card-img { height: 160px; overflow: hidden; background: var(--surface2); position: relative; }
|
||||
.card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.card-body { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
|
||||
.card-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.tag-tema {
|
||||
font-family: var(--font-head);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
padding: 3px 8px;
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.card-fuente { font-size: 10px; color: var(--accent2); letter-spacing: 1px; font-family: var(--font-head); }
|
||||
.card-fecha { font-size: 10px; color: var(--muted); margin-left: auto; }
|
||||
|
||||
.card-title {
|
||||
font-family: var(--font-head);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.card-resumen { font-size: 11px; color: var(--muted); line-height: 1.6; }
|
||||
|
||||
.card-footer { display: flex; justify-content: space-between; align-items: flex-end; gap: 8px; margin-top: auto; padding-top: 8px; border-top: 1px solid var(--border); }
|
||||
.card-ents { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.card-badges { display: flex; gap: 4px; flex-shrink: 0; }
|
||||
|
||||
.ent-tag {
|
||||
font-family: var(--font-head);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 7px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.ent-persona { background: rgba(0,196,255,0.12); color: var(--accent2); border: 1px solid rgba(0,196,255,0.25); }
|
||||
.ent-organizacion { background: rgba(255,106,0,0.12); color: var(--warn); border: 1px solid rgba(255,106,0,0.25); }
|
||||
.ent-lugar { background: rgba(160,90,255,0.12); color: #b07aff; border: 1px solid rgba(160,90,255,0.25); }
|
||||
|
||||
.badge { font-family: var(--font-head); font-size: 10px; font-weight: 600; padding: 2px 7px; letter-spacing: 1px; text-transform: uppercase; }
|
||||
.badge-pais { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); }
|
||||
.badge-lang { background: rgba(0,255,106,0.1); color: var(--accent); border: 1px solid rgba(0,255,106,0.2); }
|
||||
|
||||
/* ── ENTIDADES TAB ── */
|
||||
.entities-layout { display: grid; grid-template-columns: 360px 1fr; flex: 1; min-height: 0; overflow: hidden; }
|
||||
|
||||
.entity-panel { background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
|
||||
.entity-panel-header { padding: 16px; border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; }
|
||||
|
||||
.tipo-tabs { display: flex; gap: 4px; }
|
||||
.tipo-btn {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.tipo-btn.active { color: #000; background: var(--accent); border-color: var(--accent); }
|
||||
|
||||
.entities-list { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 6px; }
|
||||
|
||||
.ent-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.ent-row:hover { border-color: var(--border); background: var(--surface2); }
|
||||
.ent-row.selected { border-color: var(--accent); background: rgba(0,255,106,0.05); }
|
||||
|
||||
.ent-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: var(--surface2);
|
||||
border: 2px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-head);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
}
|
||||
.ent-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.ent-row.selected .ent-avatar { border-color: var(--accent); }
|
||||
|
||||
.ent-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
||||
.ent-name {
|
||||
font-family: var(--font-head);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.ent-bar-row { display: flex; align-items: center; gap: 8px; }
|
||||
.ent-bar-track { flex: 1; height: 4px; background: var(--border); overflow: hidden; }
|
||||
.ent-bar-fill { height: 100%; background: var(--accent); transition: width 0.4s; }
|
||||
.ent-count-label { font-size: 11px; color: var(--muted); font-family: var(--font-head); letter-spacing: 1px; flex-shrink: 0; }
|
||||
|
||||
.entity-news { display: flex; flex-direction: column; overflow: hidden; }
|
||||
.entity-news-title {
|
||||
padding: 16px 20px;
|
||||
font-family: var(--font-head);
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 3px;
|
||||
color: var(--accent);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.entity-news-title small { font-size: 12px; color: var(--muted); margin-left: 8px; letter-spacing: 1px; }
|
||||
.entity-news .news-grid { flex: 1; overflow-y: auto; }
|
||||
|
||||
/* ── FEEDS TAB ── */
|
||||
.feeds-container { padding: 20px; overflow-y: auto; }
|
||||
.feeds-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.feed-row {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--border);
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.feed-row:hover { border-left-color: var(--accent); }
|
||||
.feed-off { opacity: 0.4; border-left-color: var(--muted) !important; }
|
||||
.feed-info { display: flex; align-items: center; gap: 12px; flex: 1; }
|
||||
.feed-nombre { font-family: var(--font-head); font-size: 16px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; }
|
||||
.feed-sub { font-size: 11px; color: var(--muted); }
|
||||
.feed-meta { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
|
||||
.feed-lang { font-size: 11px; color: var(--muted); font-family: var(--font-head); letter-spacing: 1px; }
|
||||
.feed-ok.ok { font-family: var(--font-head); font-size: 11px; color: var(--accent); letter-spacing: 1px; }
|
||||
.feed-ok.off { font-family: var(--font-head); font-size: 11px; color: var(--muted); letter-spacing: 1px; }
|
||||
.feed-fallos { font-size: 11px; color: var(--warn); font-family: var(--font-head); }
|
||||
.feed-date { font-size: 11px; color: var(--muted); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.layout, .entities-layout { grid-template-columns: 1fr; }
|
||||
.sidebar { border-right: none; border-bottom: 1px solid var(--border); max-height: 50vh; }
|
||||
.stats-bar { flex-wrap: wrap; }
|
||||
.stat-item { flex: 1; min-width: 80px; }
|
||||
}
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Fira Code';
|
||||
text-shadow: 10px 10px 20px rgba(0, 255, 76, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Fira Code', monospace;
|
||||
background: #000000;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 70px;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: "Retrolift", sans-serif;
|
||||
font-size: 4em;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 6px 6px 6px #73ff00;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.glob-war:hover .logo { text-shadow: 2px 2px 8px #39ff14; }
|
||||
.int-sec:hover .logo { text-shadow: 2px 2px 8px #ff69b4; }
|
||||
.climate:hover .logo { text-shadow: 2px 2px 8px #ff4500; }
|
||||
.eco-corp:hover .logo { text-shadow: 2px 2px 8px #006400; }
|
||||
.popl-up:hover .logo { text-shadow: 2px 2px 8px #00008b; }
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #000000;
|
||||
color: #ff1a1a;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 40px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 6em;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
|
||||
padding: 20px 40px;
|
||||
box-shadow: 0 0 0px rgba(28, 103, 241, 0);
|
||||
|
||||
position: relative;
|
||||
border: none;
|
||||
transition: .4s ease-in;
|
||||
z-index: 1;
|
||||
width: 40vw;
|
||||
height: 20vh;
|
||||
border: 3vw;
|
||||
align-items: center;
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.3);
|
||||
border: 3px solid black;
|
||||
}
|
||||
|
||||
.glob-war:hover { color: #39ff14; }
|
||||
.int-sec:hover { color: #ff69b4; }
|
||||
.climate:hover { color: #ff4500; }
|
||||
.eco-corp:hover { color: #00fff2; }
|
||||
.popl-up:hover { color: #0066ff; }
|
||||
|
||||
.glob-war { color: #FF4136; background-color: red; }
|
||||
.int-sec { color: #0074D9; background-color: darkblue; }
|
||||
.climate { color: #2ECC40; background-color: lightgreen; }
|
||||
.eco-corp { color: #FFDC00; background-color: yellow; }
|
||||
.popl-up { color: #FF851B; background-color: orange; }
|
||||
|
||||
.background {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 99%;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.background a {
|
||||
display: block;
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background img {
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
object-fit: cover;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background img:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Sidebar con comportamiento onhover */
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background-image: url("/images/flujos7.jpg");
|
||||
background-size: cover;
|
||||
color: #39ff14;
|
||||
padding: 30px;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(-220px);
|
||||
transition: transform 0.3s ease-out;
|
||||
overflow: auto;
|
||||
font-size: 18px;
|
||||
z-index: 3;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
|
||||
}
|
||||
|
||||
#sidebar:hover {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#sidebar h2 {
|
||||
color: #39ff14;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: -1px 0 black, 0 3px black, 3px 0 black, 0 -1px black;
|
||||
}
|
||||
|
||||
#sidebar form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#sidebar label {
|
||||
color: #39ff14;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
|
||||
}
|
||||
|
||||
#sidebar input {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
#sidebar input:hover {
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#sidebar input[type="submit"] {
|
||||
color: #39ff14;
|
||||
background: #ff6600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#sidebar input[type="submit"]:hover {
|
||||
background: #ff6600;
|
||||
}
|
||||
|
||||
#sidebar select {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#sidebar select:hover { box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3); }
|
||||
#sidebar select option { background: #0a0a0a; color: #39ff14; }
|
||||
#sidebar .sim-val {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(57,255,20,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
background-color: #000000;
|
||||
color: #39ff14;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #39ff14;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #2ECC40;
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Ajuste del iframe para que respete el espacio del sidebar */
|
||||
.iframe-container {
|
||||
width: calc(100% - 250px);
|
||||
height: 100vh;
|
||||
background: #000000;
|
||||
margin: 0 auto;
|
||||
border: none;
|
||||
z-index: 1;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
position: absolute;
|
||||
left: 250px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ===== Panel de detalle (split-screen) ===== */
|
||||
main.split-screen { display: flex; height: 100vh; }
|
||||
#graphPanel { flex: 1; position: relative; min-width: 0; transition: flex 0.3s ease; }
|
||||
#detailPanel {
|
||||
width: 0; max-width: 0; overflow: hidden;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
background: #0a0a0a; color: #fff;
|
||||
position: relative; z-index: 4;
|
||||
box-shadow: inset 2px 0 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel { width: 50%; max-width: 50%; }
|
||||
|
||||
#closeDetail {
|
||||
position: absolute; top: 12px; left: 12px;
|
||||
background: rgba(0,0,0,0.85); border: 1px solid #39ff14;
|
||||
color: #39ff14; font-size: 1em; font-family: 'Fira Code', monospace;
|
||||
cursor: pointer; border-radius: 4px; padding: 4px 12px; z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#closeDetail:hover { background: rgba(57,255,20,0.15); }
|
||||
|
||||
#detailContent {
|
||||
padding: 52px 18px 20px; overflow-y: auto;
|
||||
height: 100%; box-sizing: border-box;
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
main.split-screen.show-detail #detailContent { display: flex; }
|
||||
|
||||
.detail-meta { display: flex; justify-content: space-between; font-size: 0.72em; color: #39ff14; opacity: 0.75; margin-bottom: 6px; gap: 8px; }
|
||||
.detail-author { font-weight: bold; }
|
||||
.detail-date { text-align: right; white-space: nowrap; }
|
||||
.detail-title { font-size: 0.92em; color: #39ff14; margin: 0 0 12px; word-break: break-word; line-height: 1.3; }
|
||||
|
||||
.detail-relations { border-top: 1px solid rgba(57,255,20,0.25); border-bottom: 1px solid rgba(57,255,20,0.25); padding: 8px 0; margin-bottom: 14px; font-size: 0.78em; flex-shrink: 0; }
|
||||
.relations-count { color: rgba(57,255,20,0.8); display: block; margin-bottom: 6px; }
|
||||
.relations-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.relations-nav button { background: none; border: 1px solid #39ff14; color: #39ff14; cursor: pointer; padding: 3px 8px; border-radius: 3px; font-family: 'Fira Code', monospace; font-size: 0.85em; white-space: nowrap; transition: background 0.2s; }
|
||||
.relations-nav button:hover:not(:disabled) { background: rgba(57,255,20,0.12); }
|
||||
.relations-nav button:disabled { opacity: 0.3; cursor: default; }
|
||||
#relationLabel { flex: 1; text-align: center; color: rgba(255,255,255,0.55); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }
|
||||
|
||||
.detail-body { flex: 1; overflow-y: auto; white-space: pre-wrap; line-height: 1.55; font-size: 0.78em; color: #e0e0e0; font-family: 'Fira Code', monospace; min-height: 0; }
|
||||
.detail-img { width: 100%; max-height: 180px; object-fit: contain; margin-bottom: 12px; border: 1px solid rgba(57,255,20,0.3); border-radius: 3px; }
|
||||
#detailPanel .placeholder { color: rgba(255,255,255,0.3); font-style: italic; padding: 40px 20px; text-align: center; font-size: 0.85em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main.split-screen.show-detail #graphPanel { display: none; }
|
||||
main.split-screen.show-detail #detailPanel { width: 100%; max-width: 100%; }
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Economía y Corporaciones</title>
|
||||
<link rel="stylesheet" href="eco-corp.css">
|
||||
<link rel="stylesheet" href="sub-nav.css">
|
||||
<!-- Fuentes de Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Cargar D3.js -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="top-nav">
|
||||
<div class="section-buttons">
|
||||
|
||||
<div class="section-item">
|
||||
<a href="glob-war.html" class="section-btn glob-war-btn">GLOB-WAR</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="int-sec.html" class="section-btn int-sec-btn">INT-SEC</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="climate.html" class="section-btn climate-btn">CLIMATE</a>
|
||||
</div>
|
||||
|
||||
<div class="section-item current">
|
||||
<a href="eco-corp.html" class="section-btn eco-corp-btn">ECO-CORP <span class="dropdown-arrow">▾</span></a>
|
||||
<div class="section-dropdown">
|
||||
<a href="#" onclick="setSubtema('economía global');return false;">Economía Global</a>
|
||||
<a href="#" onclick="setSubtema('corporaciones multinacionales');return false;">Corporaciones Multinacionales</a>
|
||||
<a href="#" onclick="setSubtema('comercio internacional');return false;">Comercio Internacional</a>
|
||||
<a href="#" onclick="setSubtema('organismos financieros');return false;">Organismos Financieros</a>
|
||||
<a href="#" onclick="setSubtema('desigualdad económica');return false;">Desigualdad Económica</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-item">
|
||||
<a href="popl-up.html" class="section-btn popl-up-btn">POPL-UP</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function setSubtema(val) {
|
||||
var sel = document.getElementById('param2');
|
||||
if (sel) { sel.value = val; }
|
||||
var form = document.getElementById('paramForm');
|
||||
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* ── Dropdown touch (móvil) ── */
|
||||
var curr = document.querySelector('.section-item.current');
|
||||
if (curr) {
|
||||
curr.querySelector('.section-btn').addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
curr.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('touchend', function(e) {
|
||||
if (!curr.contains(e.target)) curr.classList.remove('open');
|
||||
});
|
||||
}
|
||||
/* ── Sidebar: botón toggle visible en móvil ── */
|
||||
var toggle = document.getElementById('sidebarToggle');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.style.display = 'block';
|
||||
toggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="split-screen">
|
||||
<div id="graphPanel" style="position:relative;">
|
||||
<button id="volverGrafoBtn">← Volver al grafo completo</button>
|
||||
<div id="ecoCorpContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div id="detailPanel">
|
||||
<button id="closeDetail">✕ cerrar</button>
|
||||
<div id="detailContent">
|
||||
<div class="detail-meta">
|
||||
<span class="detail-author"></span>
|
||||
<span class="detail-date"></span>
|
||||
</div>
|
||||
<h2 class="detail-title"></h2>
|
||||
<div class="detail-relations">
|
||||
<span class="relations-count"></span>
|
||||
<div class="relations-nav">
|
||||
<button id="prevRelation" disabled>← ant</button>
|
||||
<span id="relationLabel"></span>
|
||||
<button id="nextRelation" disabled>sig →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="sidebar">
|
||||
<h2>Parámetros</h2>
|
||||
<form id="paramForm">
|
||||
<label for="fecha_inicio">Fecha de inicio:</label>
|
||||
<input type="date" id="fecha_inicio" name="fecha_inicio">
|
||||
|
||||
<label for="fecha_fin">Fecha de fin:</label>
|
||||
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||
|
||||
<label for="nodos">Nodos:</label>
|
||||
<input type="number" id="nodos" name="nodos" value="100" min="5" max="500">
|
||||
|
||||
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="25" value="8"
|
||||
oninput="document.getElementById('complejidadVal').textContent = this.value">
|
||||
|
||||
<label for="param1">Palabra clave:</label>
|
||||
<input type="text" id="param1" name="param1" placeholder="ej: FMI, Amazon...">
|
||||
|
||||
<label for="param2">Subtematica:</label>
|
||||
<select id="param2" name="param2">
|
||||
<option value="">— Todas —</option>
|
||||
<option value="comercio internacional">comercio internacional</option>
|
||||
<option value="economía global">economía global</option>
|
||||
<option value="desigualdad económica">desigualdad económica</option>
|
||||
<option value="corporaciones multinacionales">corporaciones multinacionales</option>
|
||||
<option value="organismos financieros">organismos financieros</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Aplicar">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Incluir tu script al final del body -->
|
||||
<script src="semana-toggle.js"></script>
|
||||
<script type="module" src="output_eco_corp_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,406 +0,0 @@
|
|||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Fira Code';
|
||||
text-shadow: 10px 10px 20px rgba(0, 255, 76, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Fira Code', monospace;
|
||||
background-color: #000000;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 70px;
|
||||
background-color: #000000;
|
||||
color: #000000;
|
||||
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: "Retrolift", sans-serif;
|
||||
font-size: 4em;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 6px 6px 6px #73ff00;
|
||||
margin-bottom: 10px;
|
||||
|
||||
}
|
||||
|
||||
.glob-war:hover .logo { text-shadow: 2px 2px 8px #39ff14; }
|
||||
.int-sec:hover .logo { text-shadow: 2px 2px 8px #ff69b4; }
|
||||
.climate:hover .logo { text-shadow: 2px 2px 8px #ff4500; }
|
||||
.eco-corp:hover .logo { text-shadow: 2px 2px 8px #006400; }
|
||||
.popl-up:hover .logo { text-shadow: 2px 2px 8px #00008b; }
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #000000;
|
||||
color: #ff1a1a;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 40px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 6em; /* incrementa la distancia entre los elementos */
|
||||
}
|
||||
.nav-links a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
|
||||
padding: 20px 40px;
|
||||
box-shadow: 0 0 0px rgba(28, 103, 241, 0);
|
||||
|
||||
/* Estilo de los botones */
|
||||
position:relative;
|
||||
border: none;
|
||||
|
||||
transition: .4s ease-in;
|
||||
z-index: 1;
|
||||
width: 40vw;
|
||||
height: 20vh;
|
||||
border:3vw;
|
||||
|
||||
align-items: center;
|
||||
border: 3px solid ;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.3);
|
||||
|
||||
/* Estilo hover de los botones */
|
||||
|
||||
box-shadow: 2vw 1vw;
|
||||
|
||||
border: 3px solid black ;
|
||||
}
|
||||
|
||||
.glob-war:hover { color: #39ff14; }
|
||||
.int-sec:hover { color: #ff69b4; }
|
||||
.climate:hover { color: #ff4500; }
|
||||
.eco-corp:hover { color: #00fff2; }
|
||||
.popl-up:hover { color: #0066ff; }
|
||||
|
||||
.glob-war {
|
||||
color: #FF4136;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.int-sec {
|
||||
color: #0074D9;
|
||||
background-color: darkblue;
|
||||
}
|
||||
|
||||
.climate {
|
||||
color: #2ECC40;
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
.eco-corp {
|
||||
color: #FFDC00;
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
.popl-up {
|
||||
color: #FF851B;
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
.background {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 99%;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.background a {
|
||||
display: block;
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background img {
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
object-fit: cover;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background a img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background img:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
#sidebar.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background-image: url("/images/flujos7.jpg");
|
||||
background-size: cover;
|
||||
color: #39ff14;
|
||||
padding: 30px;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(-90%);
|
||||
transition: transform 0.3s ease-out;
|
||||
overflow: auto;
|
||||
font-size:18px;
|
||||
z-index: 3;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; /* Añade el borde al texto */
|
||||
}
|
||||
|
||||
|
||||
|
||||
#sidebar h2 {
|
||||
color: #39ff14;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: -1px 0 black, 0 3px black, 3px 0 black, 0 -1px black; /* Añade el borde al texto */
|
||||
}
|
||||
|
||||
|
||||
#sidebar:hover {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#sidebarToggle {
|
||||
position: absolute;
|
||||
left: 1em;
|
||||
top: 1em;
|
||||
background: #007BFF;
|
||||
color: #39ff14;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#sidebarToggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sidebar form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
font-size:20px;
|
||||
}
|
||||
|
||||
#sidebar label {
|
||||
color: #39ff14;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; /* Añade el borde al texto */
|
||||
}
|
||||
|
||||
#sidebar input {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14; /* Añade el borde verde al input */
|
||||
background: black; /* Cambia el fondo del input a negro */
|
||||
color: #39ff14; /* Cambia el color del texto en el input a verde */
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
#sidebar input:hover {
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#sidebar input[type="submit"] {
|
||||
color: #39ff14;
|
||||
background: #ff6600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#sidebar input[type="submit"]:hover {
|
||||
background: #ff6600;
|
||||
}
|
||||
|
||||
#sidebar select {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#sidebar select:hover {
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
#sidebar select option {
|
||||
background: #0a0a0a;
|
||||
color: #39ff14;
|
||||
}
|
||||
|
||||
#sidebar .sim-val {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(57,255,20,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
background-color: #000000;
|
||||
color: #39ff14;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #39ff14;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #2ECC40;
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#glob_war_container{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.5;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.nav-links a {
|
||||
font-size: 0.8em; /* reduce el tamaño de la fuente */
|
||||
padding: 10px 20px; /* reduce el padding */
|
||||
}
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2em; /* incrementa la distancia entre los elementos */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.fade-out {
|
||||
animation: fadeOut 3s forwards; /* Fades out over 3 seconds */
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {opacity: 1;}
|
||||
to {opacity: 0;}
|
||||
}
|
||||
|
||||
|
||||
.grafico {
|
||||
z-index: 1; /* Bring to front */
|
||||
/* Rest of your styles */
|
||||
}
|
||||
|
||||
|
||||
.grafico {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ===== Panel de detalle (split-screen) ===== */
|
||||
main.split-screen { display: flex; height: 100vh; }
|
||||
#graphPanel { flex: 1; position: relative; min-width: 0; transition: flex 0.3s ease; }
|
||||
#detailPanel {
|
||||
width: 0; max-width: 0; overflow: hidden;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
background: #0a0a0a; color: #fff;
|
||||
position: relative; z-index: 4;
|
||||
box-shadow: inset 2px 0 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel { width: 50%; max-width: 50%; }
|
||||
|
||||
#closeDetail {
|
||||
position: absolute; top: 12px; left: 12px;
|
||||
background: rgba(0,0,0,0.85); border: 1px solid #39ff14;
|
||||
color: #39ff14; font-size: 1em; font-family: 'Fira Code', monospace;
|
||||
cursor: pointer; border-radius: 4px; padding: 4px 12px; z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#closeDetail:hover { background: rgba(57,255,20,0.15); }
|
||||
|
||||
#detailContent {
|
||||
padding: 52px 18px 20px; overflow-y: auto;
|
||||
height: 100%; box-sizing: border-box;
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
main.split-screen.show-detail #detailContent { display: flex; }
|
||||
|
||||
.detail-meta { display: flex; justify-content: space-between; font-size: 0.72em; color: #39ff14; opacity: 0.75; margin-bottom: 6px; gap: 8px; }
|
||||
.detail-author { font-weight: bold; }
|
||||
.detail-date { text-align: right; white-space: nowrap; }
|
||||
.detail-title { font-size: 0.92em; color: #39ff14; margin: 0 0 12px; word-break: break-word; line-height: 1.3; }
|
||||
|
||||
.detail-relations { border-top: 1px solid rgba(57,255,20,0.25); border-bottom: 1px solid rgba(57,255,20,0.25); padding: 8px 0; margin-bottom: 14px; font-size: 0.78em; flex-shrink: 0; }
|
||||
.relations-count { color: rgba(57,255,20,0.8); display: block; margin-bottom: 6px; }
|
||||
.relations-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.relations-nav button { background: none; border: 1px solid #39ff14; color: #39ff14; cursor: pointer; padding: 3px 8px; border-radius: 3px; font-family: 'Fira Code', monospace; font-size: 0.85em; white-space: nowrap; transition: background 0.2s; }
|
||||
.relations-nav button:hover:not(:disabled) { background: rgba(57,255,20,0.12); }
|
||||
.relations-nav button:disabled { opacity: 0.3; cursor: default; }
|
||||
#relationLabel { flex: 1; text-align: center; color: rgba(255,255,255,0.55); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }
|
||||
|
||||
.detail-body { flex: 1; overflow-y: auto; white-space: pre-wrap; line-height: 1.55; font-size: 0.78em; color: #e0e0e0; font-family: 'Fira Code', monospace; min-height: 0; }
|
||||
.detail-img { width: 100%; max-height: 180px; object-fit: contain; margin-bottom: 12px; border: 1px solid rgba(57,255,20,0.3); border-radius: 3px; }
|
||||
#detailPanel .placeholder { color: rgba(255,255,255,0.3); font-style: italic; padding: 40px 20px; text-align: center; font-size: 0.85em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main.split-screen.show-detail #graphPanel { display: none; }
|
||||
main.split-screen.show-detail #detailPanel { width: 100%; max-width: 100%; }
|
||||
}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Glob-War</title>
|
||||
<link rel="stylesheet" href="glob-war.css">
|
||||
<link rel="stylesheet" href="sub-nav.css">
|
||||
<!-- Fuentes de Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="top-nav">
|
||||
<div class="section-buttons">
|
||||
|
||||
<div class="section-item current">
|
||||
<a href="glob-war.html" class="section-btn glob-war-btn">GLOB-WAR <span class="dropdown-arrow">▾</span></a>
|
||||
<div class="section-dropdown">
|
||||
<a href="#" onclick="setSubtema('conflictos internacionales');return false;">Conflictos Internacionales</a>
|
||||
<a href="#" onclick="setSubtema('guerras civiles');return false;">Guerras Civiles</a>
|
||||
<a href="#" onclick="setSubtema('terrorismo');return false;">Terrorismo</a>
|
||||
<a href="#" onclick="setSubtema('armas');return false;">Armas</a>
|
||||
<a href="#" onclick="setSubtema('alianzas militares');return false;">Alianzas Militares</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-item">
|
||||
<a href="int-sec.html" class="section-btn int-sec-btn">INT-SEC</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="climate.html" class="section-btn climate-btn">CLIMATE</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="eco-corp.html" class="section-btn eco-corp-btn">ECO-CORP</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="popl-up.html" class="section-btn popl-up-btn">POPL-UP</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function setSubtema(val) {
|
||||
var sel = document.getElementById('param2');
|
||||
if (sel) { sel.value = val; }
|
||||
var form = document.getElementById('paramForm');
|
||||
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* ── Dropdown touch (móvil) ── */
|
||||
var curr = document.querySelector('.section-item.current');
|
||||
if (curr) {
|
||||
curr.querySelector('.section-btn').addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
curr.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('touchend', function(e) {
|
||||
if (!curr.contains(e.target)) curr.classList.remove('open');
|
||||
});
|
||||
}
|
||||
/* ── Sidebar: botón toggle visible en móvil ── */
|
||||
var toggle = document.getElementById('sidebarToggle');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.style.display = 'block';
|
||||
toggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="split-screen">
|
||||
<div id="graphPanel" style="position:relative;">
|
||||
<button id="volverGrafoBtn">← Volver al grafo completo</button>
|
||||
<div id="globWarContainer" style="position: absolute; width: 100%; height: 100%;"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div id="detailPanel">
|
||||
<button id="closeDetail">✕ cerrar</button>
|
||||
<div id="detailContent">
|
||||
<div class="detail-meta">
|
||||
<span class="detail-author"></span>
|
||||
<span class="detail-date"></span>
|
||||
</div>
|
||||
<h2 class="detail-title"></h2>
|
||||
<div class="detail-relations">
|
||||
<span class="relations-count"></span>
|
||||
<div class="relations-nav">
|
||||
<button id="prevRelation" disabled>← ant</button>
|
||||
<span id="relationLabel"></span>
|
||||
<button id="nextRelation" disabled>sig →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="sidebar">
|
||||
<h2>Parámetros</h2>
|
||||
<form id="paramForm">
|
||||
<label for="fecha_inicio">Fecha de inicio:</label>
|
||||
<input type="date" id="fecha_inicio" name="fecha_inicio">
|
||||
|
||||
<label for="fecha_fin">Fecha de fin:</label>
|
||||
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||
|
||||
<label for="nodos">Nodos:</label>
|
||||
<input type="number" id="nodos" name="nodos" value="100" min="5" max="500">
|
||||
|
||||
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="25" value="8"
|
||||
oninput="document.getElementById('complejidadVal').textContent = this.value">
|
||||
|
||||
<label for="param1">Palabra clave:</label>
|
||||
<input type="text" id="param1" name="param1" placeholder="ej: misiles, OTAN...">
|
||||
|
||||
<label for="param2">Subtematica:</label>
|
||||
<select id="param2" name="param2">
|
||||
<option value="">— Todas —</option>
|
||||
<option value="armas">armas</option>
|
||||
<option value="terrorismo">terrorismo</option>
|
||||
<option value="guerras civiles">guerras civiles</option>
|
||||
<option value="conflictos internacionales">conflictos internacionales</option>
|
||||
<option value="alianzas militares">alianzas militares</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Aplicar">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Incluir tu script al final del body -->
|
||||
<script src="semana-toggle.js"></script>
|
||||
<script type="module" src="output_glob_war_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
|
@ -1,160 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<title>UP-LEAKS</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&family=Nosifer&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
/* selector de idioma */
|
||||
.lang-toggle {
|
||||
display: inline-flex; gap: 2px; margin-left: 10px; vertical-align: middle;
|
||||
border: 1px solid #39ff14; font-family: 'Fira Code', monospace; font-size: 12px;
|
||||
}
|
||||
.lang-toggle button {
|
||||
background: #000; color: #39ff14; border: none; padding: 4px 9px; cursor: pointer;
|
||||
font-family: inherit; font-size: inherit; letter-spacing: .05em; transition: background .15s, color .15s;
|
||||
}
|
||||
.lang-toggle button[aria-pressed="true"] { background: #39ff14; color: #000; font-weight: 700; }
|
||||
.lang-toggle button:hover:not([aria-pressed="true"]) { color: #7dff5e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<h1 class="title">UP-LEAKS</h1>
|
||||
<div class="header-buttons header-buttons--left">
|
||||
<a href="/coconews/" class="small-button coconews-btn">COCONEWS</a>
|
||||
<a href="journalist.html" class="small-button" data-i18n="nav_journalist">Periodistas</a>
|
||||
<a href="stats.html" class="small-button stats-btn">STATS</a>
|
||||
<span class="lang-toggle" role="group" aria-label="Idioma / Language">
|
||||
<button type="button" data-lang="es" aria-pressed="true">ES</button>
|
||||
<button type="button" data-lang="en" aria-pressed="false">EN</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
<ul class="nav-links">
|
||||
<li><a href="glob-war.html" class="glob-war">GLOB-WAR</a></li>
|
||||
<li><a href="int-sec.html" class="int-sec">INT-SEC</a></li>
|
||||
<li><a href="climate.html" class="climate">CLIMATE</a></li>
|
||||
<li><a href="eco-corp.html" class="eco-corp">ECO-CORP</a></li>
|
||||
<li><a href="popl-up.html" class="popl-up">Popl-up</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<div id="canvasContainer" style="position: absolute; width: 100%; height: 100%; opacity: 0.5;"></div>
|
||||
<div class="background">
|
||||
<a href="glob-war.html">
|
||||
<img class="img1" src="/images/flujos.jpg">
|
||||
</a>
|
||||
<a href="int-sec.html">
|
||||
<img class="img2" src="/images/fujos5.jpg">
|
||||
</a>
|
||||
<a href="climate.html">
|
||||
<img class="img3" src="/images/flujos6.jpg">
|
||||
</a>
|
||||
<a href="eco-corp.html">
|
||||
<img class="img4" src="/images/flujos4.jpg">
|
||||
</a>
|
||||
<a href="popl-up.html">
|
||||
<img class="img5" src="/images/flujos3.jpg">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Botones sobre las imágenes -->
|
||||
<div class="button-overlay">
|
||||
<div class="button-column">
|
||||
<a href="glob-war.html?subtematica=conflictos_internacionales" class="overlay-button" data-i18n="gw_1">Conflictos Internacionales</a>
|
||||
<a href="glob-war.html?subtematica=guerras_civiles" class="overlay-button" data-i18n="gw_2">Guerras Civiles</a>
|
||||
<a href="glob-war.html?subtematica=terrorismo" class="overlay-button" data-i18n="gw_3">Terrorismo</a>
|
||||
<a href="glob-war.html?subtematica=armas" class="overlay-button" data-i18n="gw_4">Armas</a>
|
||||
<a href="glob-war.html?subtematica=alianzas_militares" class="overlay-button" data-i18n="gw_5">Alianzas Militares</a>
|
||||
</div>
|
||||
<div class="button-column">
|
||||
<a href="int-sec.html?subtematica=inteligencia" class="overlay-button" data-i18n="is_1">Inteligencia</a>
|
||||
<a href="int-sec.html?subtematica=ciberseguridad" class="overlay-button" data-i18n="is_2">Ciberseguridad</a>
|
||||
<a href="int-sec.html?subtematica=espionaje" class="overlay-button" data-i18n="is_3">Espionaje</a>
|
||||
<a href="int-sec.html?subtematica=seguridad_nacional" class="overlay-button" data-i18n="is_4">Seguridad Nacional</a>
|
||||
<a href="int-sec.html?subtematica=contraterrorismo" class="overlay-button" data-i18n="is_5">Contraterrorismo</a>
|
||||
</div>
|
||||
<div class="button-column">
|
||||
<a href="climate.html?subtematica=cambio_climatico" class="overlay-button" data-i18n="cl_1">Cambio Climático</a>
|
||||
<a href="climate.html?subtematica=desastres_naturales" class="overlay-button" data-i18n="cl_2">Desastres Naturales</a>
|
||||
<a href="climate.html?subtematica=conservacion" class="overlay-button" data-i18n="cl_3">Conservación</a>
|
||||
<a href="climate.html?subtematica=energia_renovable" class="overlay-button" data-i18n="cl_4">Energía Renovable</a>
|
||||
<a href="climate.html?subtematica=contaminacion" class="overlay-button" data-i18n="cl_5">Escasez de agua</a>
|
||||
</div>
|
||||
<div class="button-column">
|
||||
<a href="eco-corp.html?subtematica=economia_global" class="overlay-button" data-i18n="ec_1">Economía Global</a>
|
||||
<a href="eco-corp.html?subtematica=corporaciones_multinacionales" class="overlay-button" data-i18n="ec_2">Corporaciones Multinacionales</a>
|
||||
<a href="eco-corp.html?subtematica=comercio_internacional" class="overlay-button" data-i18n="ec_3">Comercio Internacional</a>
|
||||
<a href="eco-corp.html?subtematica=organismos_financieros" class="overlay-button" data-i18n="ec_4">Organismos Financieros</a>
|
||||
<a href="eco-corp.html?subtematica=desigualdad_economica" class="overlay-button" data-i18n="ec_5">Desigualdad Económica</a>
|
||||
</div>
|
||||
<div class="button-column">
|
||||
<a href="popl-up.html?subtematica=sobrepoblacion" class="overlay-button" data-i18n="pu_1">Sobrepoblación</a>
|
||||
<a href="popl-up.html?subtematica=covid" class="overlay-button" data-i18n="pu_2">COVID</a>
|
||||
<a href="popl-up.html?subtematica=migraciones" class="overlay-button" data-i18n="pu_3">Migraciones</a>
|
||||
<a href="popl-up.html?subtematica=urbanizacion" class="overlay-button" data-i18n="pu_4">Urbanización</a>
|
||||
<a href="popl-up.html?subtematica=distribucion_edad" class="overlay-button" data-i18n="pu_5">Despoblación rural</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<button id="sidebarToggle" data-i18n="toggle_sidebar">Mostrar panel</button>
|
||||
|
||||
<footer>
|
||||
<p><a href="#">GitHub</a> | <a href="#">Telegram</a> | <a href="#">Email</a> | <a href="#" data-i18n="tor_site">Web de Tor</a></p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var I18N = {
|
||||
es: {
|
||||
nav_journalist: 'Periodistas', toggle_sidebar: 'Mostrar panel', tor_site: 'Web de Tor',
|
||||
gw_1: 'Conflictos Internacionales', gw_2: 'Guerras Civiles', gw_3: 'Terrorismo', gw_4: 'Armas', gw_5: 'Alianzas Militares',
|
||||
is_1: 'Inteligencia', is_2: 'Ciberseguridad', is_3: 'Espionaje', is_4: 'Seguridad Nacional', is_5: 'Contraterrorismo',
|
||||
cl_1: 'Cambio Climático', cl_2: 'Desastres Naturales', cl_3: 'Conservación', cl_4: 'Energía Renovable', cl_5: 'Escasez de agua',
|
||||
ec_1: 'Economía Global', ec_2: 'Corporaciones Multinacionales', ec_3: 'Comercio Internacional', ec_4: 'Organismos Financieros', ec_5: 'Desigualdad Económica',
|
||||
pu_1: 'Sobrepoblación', pu_2: 'COVID', pu_3: 'Migraciones', pu_4: 'Urbanización', pu_5: 'Despoblación rural'
|
||||
},
|
||||
en: {
|
||||
nav_journalist: 'Journalists', toggle_sidebar: 'Show panel', tor_site: 'Tor site',
|
||||
gw_1: 'International Conflicts', gw_2: 'Civil Wars', gw_3: 'Terrorism', gw_4: 'Weapons', gw_5: 'Military Alliances',
|
||||
is_1: 'Intelligence', is_2: 'Cybersecurity', is_3: 'Espionage', is_4: 'National Security', is_5: 'Counterterrorism',
|
||||
cl_1: 'Climate Change', cl_2: 'Natural Disasters', cl_3: 'Conservation', cl_4: 'Renewable Energy', cl_5: 'Water Scarcity',
|
||||
ec_1: 'Global Economy', ec_2: 'Multinational Corporations', ec_3: 'International Trade', ec_4: 'Financial Institutions', ec_5: 'Economic Inequality',
|
||||
pu_1: 'Overpopulation', pu_2: 'COVID', pu_3: 'Migration', pu_4: 'Urbanization', pu_5: 'Rural Depopulation'
|
||||
}
|
||||
};
|
||||
function apply(lang) {
|
||||
var dict = I18N[lang] || I18N.es;
|
||||
document.documentElement.lang = lang;
|
||||
document.querySelectorAll('[data-i18n]').forEach(function (el) {
|
||||
var k = el.getAttribute('data-i18n');
|
||||
if (dict[k]) el.textContent = dict[k];
|
||||
});
|
||||
document.querySelectorAll('.lang-toggle button').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', b.dataset.lang === lang ? 'true' : 'false');
|
||||
});
|
||||
try { localStorage.setItem('upl_lang', lang); } catch (e) {}
|
||||
}
|
||||
document.querySelectorAll('.lang-toggle button').forEach(function (b) {
|
||||
b.addEventListener('click', function () { apply(b.dataset.lang); });
|
||||
});
|
||||
var saved = 'es';
|
||||
try { saved = localStorage.getItem('upl_lang') || (navigator.language || 'es').slice(0, 2); } catch (e) {}
|
||||
apply(I18N[saved] ? saved : 'es');
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,291 +0,0 @@
|
|||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Fira Code';
|
||||
text-shadow: 10px 10px 20px rgba(0, 255, 76, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Fira Code', monospace;
|
||||
background: #000000;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 70px;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: "Retrolift", sans-serif;
|
||||
font-size: 4em;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 6px 6px 6px #73ff00;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.glob-war:hover .logo { text-shadow: 2px 2px 8px #39ff14; }
|
||||
.int-sec:hover .logo { text-shadow: 2px 2px 8px #ff69b4; }
|
||||
.climate:hover .logo { text-shadow: 2px 2px 8px #ff4500; }
|
||||
.eco-corp:hover .logo { text-shadow: 2px 2px 8px #006400; }
|
||||
.popl-up:hover .logo { text-shadow: 2px 2px 8px #00008b; }
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #000000;
|
||||
color: #ff1a1a;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 40px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 6em;
|
||||
}
|
||||
.nav-links a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
|
||||
padding: 20px 40px;
|
||||
box-shadow: 0 0 0px rgba(28, 103, 241, 0);
|
||||
position: relative;
|
||||
border: none;
|
||||
z-index: 1;
|
||||
width: 40vw;
|
||||
height: 20vh;
|
||||
align-items: center;
|
||||
border: 3px solid;
|
||||
}
|
||||
.nav-links a:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 2vw 1vw;
|
||||
border: 3px solid black;
|
||||
}
|
||||
|
||||
.glob-war { color: #FF4136; background-color: red; }
|
||||
.int-sec { color: #0074D9; background-color: darkblue; }
|
||||
.climate { color: #2ECC40; background-color: lightgreen; }
|
||||
.eco-corp { color: #FFDC00; background-color: yellow; }
|
||||
.popl-up { color: #FF851B; background-color: orange; }
|
||||
|
||||
.background {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 99%;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.background img {
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
object-fit: cover;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
.background img:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background-image: url("/images/flujos7.jpg");
|
||||
background-size: cover;
|
||||
color: #39ff14;
|
||||
padding: 30px;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(-90%); /* Oculta el 90%, deja 10% visible */
|
||||
transition: transform 0.3s ease-out;
|
||||
overflow: auto;
|
||||
font-size: 18px;
|
||||
z-index: 3;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
|
||||
}
|
||||
#sidebar:hover {
|
||||
transform: translateX(0); /* Aparece entero al hacer hover */
|
||||
}
|
||||
#sidebar.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#sidebar h2 {
|
||||
color: #39ff14;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: -1px 0 black, 0 3px black, 3px 0 black, 0 -1px black;
|
||||
}
|
||||
#sidebar form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
#sidebar label {
|
||||
color: #39ff14;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
|
||||
}
|
||||
#sidebar input {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
#sidebar input:hover {
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
#sidebar input[type="submit"] {
|
||||
color: #39ff14;
|
||||
background: #ff6600;
|
||||
cursor: pointer;
|
||||
}
|
||||
#sidebar input[type="submit"]:hover {
|
||||
background: #ff6600;
|
||||
}
|
||||
|
||||
#sidebar select {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#sidebar select:hover { box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3); }
|
||||
#sidebar select option { background: #0a0a0a; color: #39ff14; }
|
||||
#sidebar .sim-val {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(57,255,20,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
background-color: #000000;
|
||||
color: #39ff14;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
z-index: 1;
|
||||
}
|
||||
footer a {
|
||||
color: #39ff14;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
footer a:hover {
|
||||
color: #2ECC40;
|
||||
}
|
||||
footer p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#intSecContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background-color: #101020;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.nav-links a {
|
||||
font-size: 0.8em;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.nav-links {
|
||||
gap: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
animation: fadeOut 2s forwards;
|
||||
}
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
/* ===== Panel de detalle (split-screen) ===== */
|
||||
main.split-screen { display: flex; height: 100vh; }
|
||||
#graphPanel { flex: 1; position: relative; min-width: 0; transition: flex 0.3s ease; }
|
||||
#detailPanel {
|
||||
width: 0; max-width: 0; overflow: hidden;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
background: #0a0a0a; color: #fff;
|
||||
position: relative; z-index: 4;
|
||||
box-shadow: inset 2px 0 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel { width: 50%; max-width: 50%; }
|
||||
|
||||
#closeDetail {
|
||||
position: absolute; top: 12px; left: 12px;
|
||||
background: rgba(0,0,0,0.85); border: 1px solid #39ff14;
|
||||
color: #39ff14; font-size: 1em; font-family: 'Fira Code', monospace;
|
||||
cursor: pointer; border-radius: 4px; padding: 4px 12px; z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#closeDetail:hover { background: rgba(57,255,20,0.15); }
|
||||
|
||||
#detailContent {
|
||||
padding: 52px 18px 20px; overflow-y: auto;
|
||||
height: 100%; box-sizing: border-box;
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
main.split-screen.show-detail #detailContent { display: flex; }
|
||||
|
||||
.detail-meta { display: flex; justify-content: space-between; font-size: 0.72em; color: #39ff14; opacity: 0.75; margin-bottom: 6px; gap: 8px; }
|
||||
.detail-author { font-weight: bold; }
|
||||
.detail-date { text-align: right; white-space: nowrap; }
|
||||
.detail-title { font-size: 0.92em; color: #39ff14; margin: 0 0 12px; word-break: break-word; line-height: 1.3; }
|
||||
|
||||
.detail-relations { border-top: 1px solid rgba(57,255,20,0.25); border-bottom: 1px solid rgba(57,255,20,0.25); padding: 8px 0; margin-bottom: 14px; font-size: 0.78em; flex-shrink: 0; }
|
||||
.relations-count { color: rgba(57,255,20,0.8); display: block; margin-bottom: 6px; }
|
||||
.relations-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.relations-nav button { background: none; border: 1px solid #39ff14; color: #39ff14; cursor: pointer; padding: 3px 8px; border-radius: 3px; font-family: 'Fira Code', monospace; font-size: 0.85em; white-space: nowrap; transition: background 0.2s; }
|
||||
.relations-nav button:hover:not(:disabled) { background: rgba(57,255,20,0.12); }
|
||||
.relations-nav button:disabled { opacity: 0.3; cursor: default; }
|
||||
#relationLabel { flex: 1; text-align: center; color: rgba(255,255,255,0.55); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }
|
||||
|
||||
.detail-body { flex: 1; overflow-y: auto; white-space: pre-wrap; line-height: 1.55; font-size: 0.78em; color: #e0e0e0; font-family: 'Fira Code', monospace; min-height: 0; }
|
||||
.detail-img { width: 100%; max-height: 180px; object-fit: contain; margin-bottom: 12px; border: 1px solid rgba(57,255,20,0.3); border-radius: 3px; }
|
||||
#detailPanel .placeholder { color: rgba(255,255,255,0.3); font-style: italic; padding: 40px 20px; text-align: center; font-size: 0.85em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main.split-screen.show-detail #graphPanel { display: none; }
|
||||
main.split-screen.show-detail #detailPanel { width: 100%; max-width: 100%; }
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Inteligencia y Seguridad</title>
|
||||
<link rel="stylesheet" href="int-sec.css">
|
||||
<link rel="stylesheet" href="sub-nav.css">
|
||||
<!-- Fuentes de Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Cargar D3.js -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="top-nav">
|
||||
<div class="section-buttons">
|
||||
|
||||
<div class="section-item">
|
||||
<a href="glob-war.html" class="section-btn glob-war-btn">GLOB-WAR</a>
|
||||
</div>
|
||||
|
||||
<div class="section-item current">
|
||||
<a href="int-sec.html" class="section-btn int-sec-btn">INT-SEC <span class="dropdown-arrow">▾</span></a>
|
||||
<div class="section-dropdown">
|
||||
<a href="#" onclick="setSubtema('inteligencia');return false;">Inteligencia</a>
|
||||
<a href="#" onclick="setSubtema('ciberseguridad');return false;">Ciberseguridad</a>
|
||||
<a href="#" onclick="setSubtema('espionaje');return false;">Espionaje</a>
|
||||
<a href="#" onclick="setSubtema('seguridad nacional');return false;">Seguridad Nacional</a>
|
||||
<a href="#" onclick="setSubtema('contraterrorismo');return false;">Contraterrorismo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-item">
|
||||
<a href="climate.html" class="section-btn climate-btn">CLIMATE</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="eco-corp.html" class="section-btn eco-corp-btn">ECO-CORP</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="popl-up.html" class="section-btn popl-up-btn">POPL-UP</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function setSubtema(val) {
|
||||
var sel = document.getElementById('param2');
|
||||
if (sel) { sel.value = val; }
|
||||
var form = document.getElementById('paramForm');
|
||||
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* ── Dropdown touch (móvil) ── */
|
||||
var curr = document.querySelector('.section-item.current');
|
||||
if (curr) {
|
||||
curr.querySelector('.section-btn').addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
curr.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('touchend', function(e) {
|
||||
if (!curr.contains(e.target)) curr.classList.remove('open');
|
||||
});
|
||||
}
|
||||
/* ── Sidebar: botón toggle visible en móvil ── */
|
||||
var toggle = document.getElementById('sidebarToggle');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.style.display = 'block';
|
||||
toggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="split-screen">
|
||||
<!-- PANEL IZQUIERDO: el grafo -->
|
||||
<div id="graphPanel" style="position:relative;">
|
||||
<button id="volverGrafoBtn">← Volver al grafo completo</button>
|
||||
<div id="intSecContainer"></div>
|
||||
<div class="background">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<img src="/images/flujos.jpg">
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
const fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PANEL DERECHO: detalle de la noticia -->
|
||||
<div id="detailPanel">
|
||||
<button id="closeDetail">✕ cerrar</button>
|
||||
<div id="detailContent">
|
||||
<div class="detail-meta">
|
||||
<span class="detail-author"></span>
|
||||
<span class="detail-date"></span>
|
||||
</div>
|
||||
<h2 class="detail-title"></h2>
|
||||
<div class="detail-relations">
|
||||
<span class="relations-count"></span>
|
||||
<div class="relations-nav">
|
||||
<button id="prevRelation" disabled>← ant</button>
|
||||
<span id="relationLabel"></span>
|
||||
<button id="nextRelation" disabled>sig →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="sidebar">
|
||||
<h2>Parámetros</h2>
|
||||
<form id="paramForm">
|
||||
<label for="fecha_inicio">Fecha de inicio:</label>
|
||||
<input type="date" id="fecha_inicio" name="fecha_inicio">
|
||||
|
||||
<label for="fecha_fin">Fecha de fin:</label>
|
||||
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||
|
||||
<label for="nodos">Nodos:</label>
|
||||
<input type="number" id="nodos" name="nodos" value="100" min="5" max="500">
|
||||
|
||||
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="25" value="8"
|
||||
oninput="document.getElementById('complejidadVal').textContent = this.value">
|
||||
|
||||
<label for="param1">Palabra clave:</label>
|
||||
<input type="text" id="param1" name="param1" placeholder="ej: CIA, GCHQ...">
|
||||
|
||||
<label for="param2">Subtematica:</label>
|
||||
<select id="param2" name="param2">
|
||||
<option value="">— Todas —</option>
|
||||
<option value="inteligencia">inteligencia</option>
|
||||
<option value="seguridad nacional">seguridad nacional</option>
|
||||
<option value="espionaje">espionaje</option>
|
||||
<option value="ciberseguridad">ciberseguridad</option>
|
||||
<option value="contraterrorismo">contraterrorismo</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Aplicar">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Incluir tu script al final del body -->
|
||||
<script src="semana-toggle.js"></script>
|
||||
<script type="module" src="output_int_sec.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #000000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #ffffff; /* Cambiar el color del texto a blanco */
|
||||
}
|
||||
|
||||
/* Estilo del formulario */
|
||||
.login-container {
|
||||
max-width: 400px;
|
||||
margin: 30px auto;
|
||||
background-color: #111111; /* Fondo negro */
|
||||
border: 2px solid #39ff14; /* Borde verde claro */
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
position: relative; /* Añadimos posición relativa para que los elementos internos se posicionen respecto a este contenedor */
|
||||
}
|
||||
|
||||
|
||||
/* ... Estilos existentes ... */
|
||||
|
||||
/* Estilos para los enlaces dentro del formulario */
|
||||
.login-container a {
|
||||
color: #39ff14; /* Cambiar el color del enlace a verde claro */
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-container a:hover {
|
||||
color: #2ECC40; /* Cambiar el color del enlace en hover a verde más claro */
|
||||
}
|
||||
|
||||
/* ... Resto de tus estilos ... */
|
||||
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #39ff14; /* Cambiar el color del título a verde claro */
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 8px;
|
||||
color: #39ff14; /* Cambiar el color de las etiquetas a verde claro */
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #222222; /* Fondo negro */
|
||||
color: #ffffff; /* Texto blanco */
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
background-color: #007BFF;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type="submit"]:hover {
|
||||
background-color: #0056b3;
|
||||
border: 2px solid #39ff14; /* Borde verde claro en hover */
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 0px; /* Añadir margen inferior para separar el formulario del logo */
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 400px; /* Ajustar el tamaño de la imagen según sea necesario */
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Estilos para las imágenes de fondo */
|
||||
.canvas-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
min-height: 100vh; /* Ajusta la altura mínima para que ocupe toda la pantalla */
|
||||
margin: 20px 0;
|
||||
overflow: hidden; /* Evita que las imágenes desborden del contenedor */
|
||||
}
|
||||
|
||||
.image-left,
|
||||
.image-right {
|
||||
height: 100vh; /* Ajusta la altura para que ocupe toda la pantalla */
|
||||
width: auto; /* Ajusta el ancho de las imágenes para mantener la relación de aspecto */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
max-width: 40%; /* Ajusta el ancho máximo de las imágenes de fondo */
|
||||
}
|
||||
|
||||
.image-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.image-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.login-content {
|
||||
/* Ajusta estos estilos según tus necesidades */
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
max-width: 300px; /* Ajusta el ancho máximo del contenido */
|
||||
position: relative; /* Añadimos posición relativa para que los elementos internos se posicionen respecto a este contenedor */
|
||||
}
|
||||
|
||||
/* Añade el siguiente estilo para evitar que las imágenes desborden del contenedor */
|
||||
.login-content > img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
|
@ -1,291 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>UP-LEAKS · Buzón seguro para fuentes y periodistas</title>
|
||||
<style>
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }
|
||||
@font-face { font-family: 'Fira Code'; src: url('/fonts/fira-code-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }
|
||||
|
||||
:root {
|
||||
--neon: #39ff14; --neon-soft: #7dff5e; --neon-deep: #0e3a08;
|
||||
--ink: #eafbe6; --muted: #9db89a; --faint: #5d7359;
|
||||
--red: #ff3b3b; --line: rgba(57,255,20,.24); --line-hard: rgba(57,255,20,.45);
|
||||
--mono: 'Fira Code', ui-monospace, Menlo, Consolas, monospace;
|
||||
--glow: 0 0 10px rgba(57,255,20,.55);
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
color: var(--ink); font-family: var(--mono); font-size: 15px; line-height: 1.65;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0,0,0,.90) 0%, rgba(2,10,2,.93) 40%, rgba(0,0,0,.96) 100%),
|
||||
url('/images/flujos6.jpg') center top / cover fixed no-repeat;
|
||||
}
|
||||
a { color: var(--neon); text-decoration: none; }
|
||||
.wrap { max-width: 900px; margin: 0 auto; padding: 0 22px; }
|
||||
::selection { background: var(--neon); color: #000; }
|
||||
|
||||
/* ---------- buttons (acciones = botones reales) ---------- */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 9px; font-family: var(--mono); font-weight: 700;
|
||||
font-size: 13.5px; letter-spacing: .05em; padding: 13px 26px; cursor: pointer;
|
||||
border: 1.5px solid var(--neon); background: rgba(0,0,0,.35); color: var(--neon);
|
||||
text-transform: uppercase; transition: background .18s, color .18s, box-shadow .18s, transform .1s;
|
||||
}
|
||||
.btn:hover { background: var(--neon); color: #000; box-shadow: 0 0 22px -3px rgba(57,255,20,.75); }
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn-solid { background: var(--neon); color: #000; border-color: var(--neon); }
|
||||
.btn-solid:hover { background: var(--neon-soft); border-color: var(--neon-soft); box-shadow: 0 0 26px -2px rgba(57,255,20,.85); }
|
||||
.btn-ghost { border-color: var(--line-hard); color: var(--ink); background: rgba(0,0,0,.3); }
|
||||
.btn-ghost:hover { border-color: var(--neon); color: var(--neon); background: rgba(0,0,0,.3); box-shadow: none; }
|
||||
.btn-lg { font-size: 15px; padding: 16px 34px; }
|
||||
|
||||
/* ---------- top nav ---------- */
|
||||
header.nav {
|
||||
position: sticky; top: 0; z-index: 30; border-bottom: 1px solid var(--line);
|
||||
background: rgba(0,0,0,.78); backdrop-filter: blur(10px);
|
||||
}
|
||||
.nav .wrap { display: flex; align-items: center; justify-content: space-between; height: 68px; gap: 12px; }
|
||||
.brand { font-weight: 700; letter-spacing: .2em; font-size: 20px; }
|
||||
.brand b { color: var(--neon); text-shadow: var(--glow); }
|
||||
.nav-actions { display: flex; gap: 10px; }
|
||||
.nav-actions .btn { padding: 9px 16px; font-size: 12px; }
|
||||
|
||||
/* ---------- hero ---------- */
|
||||
.hero { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); }
|
||||
.hero::before {
|
||||
content: ""; position: absolute; inset: 0; z-index: 0;
|
||||
background: linear-gradient(115deg, rgba(0,0,0,.35) 0%, rgba(0,0,0,.72) 55%, rgba(2,12,2,.92) 100%),
|
||||
url('/images/flujos6.jpg') center / cover no-repeat;
|
||||
}
|
||||
.hero::after {
|
||||
content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
|
||||
background: radial-gradient(120% 90% at 15% 10%, rgba(57,255,20,.16), transparent 55%);
|
||||
}
|
||||
.hero .wrap { position: relative; z-index: 2; padding: 84px 22px 74px; }
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 26px; }
|
||||
.chip {
|
||||
font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--neon);
|
||||
border: 1px solid var(--line-hard); padding: 5px 12px; background: rgba(0,0,0,.4);
|
||||
}
|
||||
.chip::before { content: "● "; }
|
||||
.hero h1 {
|
||||
font-size: clamp(32px, 6vw, 58px); line-height: 1.06; font-weight: 700; letter-spacing: -.02em;
|
||||
max-width: 16ch; text-wrap: balance; text-shadow: 0 2px 30px rgba(0,0,0,.6);
|
||||
}
|
||||
.hero h1 em { color: var(--neon); font-style: normal; text-shadow: var(--glow); }
|
||||
.hero .lead { margin: 22px 0 34px; font-size: 17px; color: var(--ink); max-width: 60ch; opacity: .92; }
|
||||
.hero .lead b { color: var(--neon); }
|
||||
.hero .cta { display: flex; gap: 14px; flex-wrap: wrap; }
|
||||
|
||||
/* ---------- sections ---------- */
|
||||
section.block { padding: 60px 0; }
|
||||
.eyebrow { font-size: 12px; letter-spacing: .2em; text-transform: uppercase; color: var(--neon); margin-bottom: 10px; }
|
||||
.block h2 { font-size: clamp(22px, 3.4vw, 30px); font-weight: 700; letter-spacing: -.01em; margin-bottom: 8px; text-wrap: balance; }
|
||||
.block .sub { color: var(--muted); font-size: 15px; max-width: 62ch; }
|
||||
|
||||
/* security cards */
|
||||
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 34px; }
|
||||
@media (max-width: 720px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card {
|
||||
position: relative; border: 1px solid var(--line); background: rgba(4,10,4,.66);
|
||||
backdrop-filter: blur(6px); padding: 26px 22px; overflow: hidden;
|
||||
transition: border-color .2s, transform .2s, box-shadow .2s;
|
||||
}
|
||||
.card::before {
|
||||
content: ""; position: absolute; left: 0; top: 0; height: 3px; width: 100%;
|
||||
background: linear-gradient(90deg, var(--neon), transparent 70%);
|
||||
}
|
||||
.card:hover { border-color: var(--line-hard); transform: translateY(-4px); box-shadow: 0 18px 40px -22px rgba(57,255,20,.55); }
|
||||
.card .num { font-size: 34px; font-weight: 700; color: var(--neon); line-height: 1; text-shadow: var(--glow); }
|
||||
.card h3 { margin: 14px 0 8px; font-size: 18px; font-weight: 700; }
|
||||
.card p { color: var(--muted); font-size: 14px; line-height: 1.6; }
|
||||
.card code { color: var(--neon); background: rgba(0,0,0,.5); border: 1px solid var(--line); padding: 1px 6px; }
|
||||
|
||||
/* form */
|
||||
.form-sec { position: relative; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
||||
.form-sec::before {
|
||||
content: ""; position: absolute; inset: 0; z-index: 0; opacity: .5;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,.86), rgba(2,10,2,.92)), url('/images/flujos6.jpg') center / cover fixed;
|
||||
}
|
||||
.form-sec .wrap { position: relative; z-index: 1; padding: 62px 22px; }
|
||||
.panel {
|
||||
border: 1px solid var(--line-hard); background: rgba(3,8,3,.82); backdrop-filter: blur(10px);
|
||||
padding: 34px; box-shadow: 0 30px 70px -34px rgba(0,0,0,.9), inset 0 0 60px -40px rgba(57,255,20,.4);
|
||||
}
|
||||
.field { margin-bottom: 26px; }
|
||||
.field:last-of-type { margin-bottom: 0; }
|
||||
label.lbl { display: block; font-size: 14.5px; font-weight: 700; margin-bottom: 6px; color: var(--ink); }
|
||||
.num-tag { color: var(--neon); margin-right: 8px; }
|
||||
.hint { font-size: 13px; color: var(--muted); margin-bottom: 12px; }
|
||||
select, textarea, input[type="text"] {
|
||||
width: 100%; background: rgba(0,0,0,.55); border: 1.5px solid var(--line); color: var(--ink);
|
||||
padding: 14px; font-family: var(--mono); font-size: 15px; transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
textarea { min-height: 160px; resize: vertical; line-height: 1.6; }
|
||||
select:focus, textarea:focus, input[type="text"]:focus, input[type="file"]:focus {
|
||||
outline: none; border-color: var(--neon); box-shadow: 0 0 0 1px var(--neon), 0 0 18px -3px rgba(57,255,20,.6);
|
||||
}
|
||||
select {
|
||||
appearance: none; -webkit-appearance: none; cursor: pointer;
|
||||
background-image: linear-gradient(45deg, transparent 50%, var(--neon) 50%), linear-gradient(135deg, var(--neon) 50%, transparent 50%);
|
||||
background-position: calc(100% - 20px) center, calc(100% - 15px) center; background-size: 6px 6px, 6px 6px; background-repeat: no-repeat;
|
||||
}
|
||||
/* file input: el botón nativo estilizado como botón real */
|
||||
input[type="file"] {
|
||||
width: 100%; background: rgba(0,0,0,.55); border: 1.5px dashed var(--line); color: var(--muted);
|
||||
padding: 16px; font-family: var(--mono); font-size: 13.5px; cursor: pointer;
|
||||
}
|
||||
input[type="file"]::file-selector-button {
|
||||
font-family: var(--mono); font-weight: 700; font-size: 12.5px; text-transform: uppercase; letter-spacing: .05em;
|
||||
background: var(--neon); color: #000; border: none; padding: 10px 18px; margin-right: 16px; cursor: pointer;
|
||||
transition: background .15s, box-shadow .15s;
|
||||
}
|
||||
input[type="file"]::file-selector-button:hover { background: var(--neon-soft); box-shadow: 0 0 16px -2px rgba(57,255,20,.7); }
|
||||
|
||||
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
|
||||
.submit-row { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; margin-top: 30px; padding-top: 24px; border-top: 1px solid var(--line); }
|
||||
.submit-note { font-size: 12px; color: var(--faint); letter-spacing: .04em; }
|
||||
|
||||
.callout {
|
||||
display: flex; gap: 15px; border: 1px solid var(--line); border-left: 3px solid var(--neon);
|
||||
background: rgba(4,10,4,.6); backdrop-filter: blur(6px); padding: 22px 24px; font-size: 14px; color: var(--muted); line-height: 1.65;
|
||||
}
|
||||
.callout b { color: var(--ink); }
|
||||
.callout .ic { color: var(--neon); font-size: 20px; }
|
||||
|
||||
footer.foot { border-top: 1px solid var(--line); padding: 34px 0 60px; background: rgba(0,0,0,.4); }
|
||||
footer.foot p { font-size: 12px; color: var(--faint); margin: 5px 0; letter-spacing: .04em; }
|
||||
footer.foot .q { color: var(--muted); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="nav">
|
||||
<div class="wrap">
|
||||
<span class="brand"><b>UP</b>-LEAKS</span>
|
||||
<div class="nav-actions">
|
||||
<a class="btn btn-ghost" href="/">Grafo</a>
|
||||
<a class="btn btn-ghost" href="/stats.html">Datos</a>
|
||||
<a class="btn" href="#buzon">Enviar</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="wrap">
|
||||
<div class="chips">
|
||||
<span class="chip">Canal seguro</span>
|
||||
<span class="chip">Tor recomendado</span>
|
||||
<span class="chip">Sin registro de IP</span>
|
||||
</div>
|
||||
<h1>Filtra lo que el poder quiere <em>en silencio</em></h1>
|
||||
<p class="lead">UP-LEAKS conecta documentos filtrados con la actualidad para exponer los flujos de poder. Si tienes material de interés público —contratos, cables, correos, bases de datos, testimonios— este es tu canal. <b>No registramos tu IP ni te pedimos identificarte.</b></p>
|
||||
<div class="cta">
|
||||
<a class="btn btn-solid btn-lg" href="#buzon">Enviar material →</a>
|
||||
<a class="btn btn-lg" href="#seguridad">Cómo protegerte</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block" id="seguridad">
|
||||
<div class="wrap">
|
||||
<div class="eyebrow">Antes de enviar</div>
|
||||
<h2>Tres pasos para protegerte</h2>
|
||||
<p class="sub">Tu seguridad va primero. Dedica un minuto a esto antes de transmitir nada.</p>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<div class="num">01</div>
|
||||
<h3>Usa Tor</h3>
|
||||
<p>Entra con el <a href="https://www.torproject.org/es/" rel="noopener noreferrer">Navegador Tor</a> para que tu conexión no quede ligada a ti. Evita hacerlo desde la red de tu trabajo.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="num">02</div>
|
||||
<h3>Limpia los archivos</h3>
|
||||
<p>Los documentos guardan metadatos (autor, equipo, GPS). Bórralos con <code>mat2</code> antes de subirlos.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="num">03</div>
|
||||
<h3>No des detalles de más</h3>
|
||||
<p>No incluyas datos que solo tú podrías conocer. Cuanta menos gente tuvo acceso al material, más discreto conviene ser.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form-sec" id="buzon">
|
||||
<div class="wrap">
|
||||
<div class="eyebrow">Buzón de envío</div>
|
||||
<h2 style="margin-bottom:8px">Transmite tu material</h2>
|
||||
<p class="sub" style="margin-bottom:30px">Cifrado en tránsito. Sin cuentas, sin cookies, sin rastro de tu IP.</p>
|
||||
|
||||
<form class="panel" method="post" action="/api/submit" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="render" value="html">
|
||||
|
||||
<div class="field">
|
||||
<label class="lbl" for="tema"><span class="num-tag">01</span>Temática</label>
|
||||
<p class="hint">Encamina tu envío al área correcta.</p>
|
||||
<select id="tema" name="tema">
|
||||
<option value="guerra global">Guerra global / conflictos</option>
|
||||
<option value="inteligencia y seguridad">Inteligencia y seguridad</option>
|
||||
<option value="cambio climático">Cambio climático / medioambiente</option>
|
||||
<option value="economía y corporaciones">Economía y corporaciones</option>
|
||||
<option value="demografía y sociedad">Demografía y sociedad</option>
|
||||
<option value="otros" selected>Otros / no estoy seguro</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="lbl" for="mensaje"><span class="num-tag">02</span>Descripción del material</label>
|
||||
<p class="hint">¿Qué es, de dónde sale, por qué importa? No hace falta que sea largo.</p>
|
||||
<textarea id="mensaje" name="mensaje" maxlength="20000" placeholder="Cuéntanos qué nos envías y qué crees que revela…"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="lbl" for="adjuntos"><span class="num-tag">03</span>Adjuntos <span style="color:var(--faint);font-weight:400">— opcional · hasta 8 archivos · 50 MB c/u</span></label>
|
||||
<p class="hint">Documentos, hojas de cálculo, PDF, imágenes o archivos comprimidos.</p>
|
||||
<input type="file" id="adjuntos" name="adjuntos" multiple>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="lbl" for="contacto"><span class="num-tag">04</span>Contacto <span style="color:var(--faint);font-weight:400">— opcional</span></label>
|
||||
<p class="hint">Solo si quieres que podamos responderte. Signal, ProtonMail, clave PGP o una cuenta desechable. Puedes dejarlo en blanco.</p>
|
||||
<input type="text" id="contacto" name="contacto" maxlength="400" placeholder="p. ej. Signal: +…, o correo desechable">
|
||||
</div>
|
||||
|
||||
<div class="hp" aria-hidden="true">
|
||||
<label>No rellenar<input type="text" name="website" tabindex="-1" autocomplete="off"></label>
|
||||
</div>
|
||||
|
||||
<div class="submit-row">
|
||||
<button type="submit" class="btn btn-solid btn-lg">Enviar de forma segura →</button>
|
||||
<span class="submit-note">HTTPS · sin IP · sin cookies · sin JavaScript</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<div class="wrap">
|
||||
<div class="callout">
|
||||
<span class="ic" aria-hidden="true">▲</span>
|
||||
<div><b>Aviso honesto.</b> Este buzón cifra el envío en tránsito y no registra tu IP, pero <b>no sustituye a una plataforma tipo SecureDrop</b>. Para filtraciones de altísimo riesgo, combínalo con Tor, limpieza de metadatos y, si puedes, cifrado PGP del propio archivo.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="foot">
|
||||
<div class="wrap">
|
||||
<p>UP-LEAKS · flujos de información · software libre</p>
|
||||
<p class="q">“Los gigantes vistos en perspectiva parecen marionetas.”</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,373 +0,0 @@
|
|||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Fira Code';
|
||||
text-shadow: 10px 10px 20px rgba(0, 255, 76, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Fira Code', monospace;
|
||||
background: #000000;
|
||||
color: #000000;
|
||||
}
|
||||
header {
|
||||
position: relative;
|
||||
height: 100px;
|
||||
background-color: #000000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 400px;
|
||||
height: 140px;
|
||||
margin-left: 400px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.small-button {
|
||||
margin-left: 5px;
|
||||
margin-right: 3px;
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
background-color: #000000;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.small-button:hover {
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* ... Resto de tus estilos ... */
|
||||
|
||||
|
||||
/* ... Estilos existentes ... */
|
||||
|
||||
|
||||
|
||||
|
||||
.logo {
|
||||
/* Ajusta el tamaño de la imagen según tus necesidades */
|
||||
width: 400px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 20px;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.buttons a {
|
||||
display: block;
|
||||
margin-left: 10px;
|
||||
padding: 10px 15px;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.glob-war:hover .logo { text-shadow: 2px 2px 8px #39ff14; }
|
||||
.int-sec:hover .logo { text-shadow: 2px 2px 8px #ff69b4; }
|
||||
.climate:hover .logo { text-shadow: 2px 2px 8px #ff4500; }
|
||||
.eco-corp:hover .logo { text-shadow: 2px 2px 8px #006400; }
|
||||
.popl-up:hover .logo { text-shadow: 2px 2px 8px #00008b; }
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #000000;
|
||||
color: #ff1a1a;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 40px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 6em; /* incrementa la distancia entre los elementos */
|
||||
}
|
||||
.nav-links a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
|
||||
padding: 20px 40px;
|
||||
box-shadow: 0 0 0px rgba(28, 103, 241, 0);
|
||||
|
||||
/* Estilo de los botones */
|
||||
position:relative;
|
||||
border: none;
|
||||
|
||||
transition: .4s ease-in;
|
||||
z-index: 1;
|
||||
width: 40vw;
|
||||
height: 20vh;
|
||||
border:3vw;
|
||||
|
||||
align-items: center;
|
||||
border: 3px solid ;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.3);
|
||||
|
||||
/* Estilo hover de los botones */
|
||||
|
||||
box-shadow: 2vw 1vw;
|
||||
|
||||
border: 3px solid black ;
|
||||
}
|
||||
|
||||
.glob-war:hover { color: #39ff14; }
|
||||
.int-sec:hover { color: #ff69b4; }
|
||||
.climate:hover { color: #ff4500; }
|
||||
.eco-corp:hover { color: #00fff2; }
|
||||
.popl-up:hover { color: #0066ff; }
|
||||
|
||||
.glob-war {
|
||||
color: #FF4136;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.int-sec {
|
||||
color: #0074D9;
|
||||
background-color: darkblue;
|
||||
}
|
||||
|
||||
.climate {
|
||||
color: #2ECC40;
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
.eco-corp {
|
||||
color: #FFDC00;
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
.popl-up {
|
||||
color: #FF851B;
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
.background {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 99%;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.background a {
|
||||
display: block;
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background img {
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
object-fit: cover;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background a img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background img:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
#sidebar.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background-image: url("/images/flujos7.jpg");
|
||||
background-size: cover;
|
||||
color: #39ff14;
|
||||
padding: 30px;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(-90%);
|
||||
transition: transform 0.3s ease-out;
|
||||
overflow: auto;
|
||||
font-size:18px;
|
||||
z-index: 3;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; /* Añade el borde al texto */
|
||||
}
|
||||
|
||||
|
||||
|
||||
#sidebar h2 {
|
||||
color: #39ff14;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: -1px 0 black, 0 3px black, 3px 0 black, 0 -1px black; /* Añade el borde al texto */
|
||||
}
|
||||
|
||||
|
||||
#sidebar:hover {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#sidebarToggle {
|
||||
position: absolute;
|
||||
left: 1em;
|
||||
top: 1em;
|
||||
background: #007BFF;
|
||||
color: #39ff14;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#sidebarToggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sidebar form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
font-size:20px;
|
||||
}
|
||||
|
||||
#sidebar label {
|
||||
color: #39ff14;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; /* Añade el borde al texto */
|
||||
}
|
||||
|
||||
#sidebar input {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14; /* Añade el borde verde al input */
|
||||
background: black; /* Cambia el fondo del input a negro */
|
||||
color: #39ff14; /* Cambia el color del texto en el input a verde */
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
#sidebar input:hover {
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#sidebar input[type="submit"] {
|
||||
color: #39ff14;
|
||||
background: #ff6600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#sidebar input[type="submit"]:hover {
|
||||
background: #ff6600;
|
||||
}
|
||||
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
background-color: #000000;
|
||||
color: #39ff14;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #39ff14;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #2ECC40;
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#canvasContainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
/* ... Estilos existentes ... */
|
||||
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.nav-links a {
|
||||
font-size: 0.8em; /* reduce el tamaño de la fuente */
|
||||
padding: 10px 20px; /* reduce el padding */
|
||||
}
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2em; /* incrementa la distancia entre los elementos */
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Journalist Enter</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<img src="/images/flujos_logo5.png" alt="FLUJØS 3D Logo" class="logo">
|
||||
<div class="header-buttons">
|
||||
<a href="login.html" class="small-button">Login/Sign Up</a>
|
||||
<div class="small-button">Language</div> <!-- El botón de idioma con el dropdown se implementará más adelante -->
|
||||
<a href="journalist.html" class="small-button">Journalist</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Sidebar Toggle Button -->
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<!-- Graficos generados por el script de output.js -->
|
||||
<div id="canvasContainer"></div>
|
||||
<div class="background">
|
||||
<!-- Enlaces a otras páginas -->
|
||||
<a href="glob-war.html">
|
||||
<img class="img1" src="/images/flujos.jpg">
|
||||
</a>
|
||||
<a href="int-sec.html">
|
||||
<img class="img2" src="/images/fujos5.jpg">
|
||||
</a>
|
||||
<a href="climate.html">
|
||||
<img class="img3" src="/images/flujos6.jpg">
|
||||
</a>
|
||||
<a href="eco-corp.html">
|
||||
<img class="img4" src="/images/flujos4.jpg">
|
||||
</a>
|
||||
<a href="popl-up.html">
|
||||
<img class="img5" src="/images/flujos3.jpg">
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Sidebar with Parameters -->
|
||||
<div id="sidebar">
|
||||
<h2>Parámetros</h2>
|
||||
<form>
|
||||
<!-- Aquí van los parámetros específicos para añadir nodos -->
|
||||
<label for="nodo_nombre">Nombre del Nodo:</label>
|
||||
<input type="text" id="nodo_nombre" name="nodo_nombre" required>
|
||||
|
||||
<label for="nodo_descripcion">Descripción del Nodo:</label>
|
||||
<input type="text" id="nodo_descripcion" name="nodo_descripcion" required>
|
||||
|
||||
<label for="nodo_imagen">Imagen del Nodo:</label>
|
||||
<input type="file" id="nodo_imagen" name="nodo_imagen" accept="image/*" required>
|
||||
|
||||
<input type="submit" value="Añadir Nodo">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p><a href="#">GitHub</a> | <a href="#">Telegram</a> | <a href="#">Email</a> | <a href="#">Web de Tor</a></p>
|
||||
</footer>
|
||||
|
||||
<!-- Script para los gráficos generados por output.js -->
|
||||
<script src="output.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,438 +0,0 @@
|
|||
import { AmbientLight, DirectionalLight, Vector3, REVISION } from "./three.module.js";
|
||||
|
||||
|
||||
const three = window.THREE
|
||||
? window.THREE // Prefer consumption from global THREE, if exists
|
||||
: { AmbientLight, DirectionalLight, Vector3, REVISION };
|
||||
|
||||
import { DragControls as ThreeDragControls } from './DragControls.js';
|
||||
|
||||
import ThreeForceGraph from "./three-forcegraph.js";
|
||||
import ThreeRenderObjects from "./three-render-objects.js";
|
||||
|
||||
import accessorFn from "./accessor-fn.js";
|
||||
import Kapsule from "./kapsule.js";
|
||||
|
||||
import linkKapsule from './kapsule-link.js';
|
||||
|
||||
//
|
||||
|
||||
const CAMERA_DISTANCE2NODES_FACTOR = 170;
|
||||
|
||||
//
|
||||
|
||||
// Expose config from forceGraph
|
||||
const bindFG = linkKapsule('forceGraph', ThreeForceGraph);
|
||||
const linkedFGProps = Object.assign(...[
|
||||
'jsonUrl',
|
||||
'graphData',
|
||||
'numDimensions',
|
||||
'dagMode',
|
||||
'dagLevelDistance',
|
||||
'dagNodeFilter',
|
||||
'onDagError',
|
||||
'nodeRelSize',
|
||||
'nodeId',
|
||||
'nodeVal',
|
||||
'nodeResolution',
|
||||
'nodeColor',
|
||||
'nodeAutoColorBy',
|
||||
'nodeOpacity',
|
||||
'nodeVisibility',
|
||||
'nodeThreeObject',
|
||||
'nodeThreeObjectExtend',
|
||||
'linkSource',
|
||||
'linkTarget',
|
||||
'linkVisibility',
|
||||
'linkColor',
|
||||
'linkAutoColorBy',
|
||||
'linkOpacity',
|
||||
'linkWidth',
|
||||
'linkResolution',
|
||||
'linkCurvature',
|
||||
'linkCurveRotation',
|
||||
'linkMaterial',
|
||||
'linkThreeObject',
|
||||
'linkThreeObjectExtend',
|
||||
'linkPositionUpdate',
|
||||
'linkDirectionalArrowLength',
|
||||
'linkDirectionalArrowColor',
|
||||
'linkDirectionalArrowRelPos',
|
||||
'linkDirectionalArrowResolution',
|
||||
'linkDirectionalParticles',
|
||||
'linkDirectionalParticleSpeed',
|
||||
'linkDirectionalParticleWidth',
|
||||
'linkDirectionalParticleColor',
|
||||
'linkDirectionalParticleResolution',
|
||||
'forceEngine',
|
||||
'd3AlphaDecay',
|
||||
'd3VelocityDecay',
|
||||
'd3AlphaMin',
|
||||
'ngraphPhysics',
|
||||
'warmupTicks',
|
||||
'cooldownTicks',
|
||||
'cooldownTime',
|
||||
'onEngineTick',
|
||||
'onEngineStop'
|
||||
].map(p => ({ [p]: bindFG.linkProp(p)})));
|
||||
const linkedFGMethods = Object.assign(...[
|
||||
'refresh',
|
||||
'getGraphBbox',
|
||||
'd3Force',
|
||||
'd3ReheatSimulation',
|
||||
'emitParticle'
|
||||
].map(p => ({ [p]: bindFG.linkMethod(p)})));
|
||||
|
||||
// Expose config from renderObjs
|
||||
const bindRenderObjs = linkKapsule('renderObjs', ThreeRenderObjects);
|
||||
const linkedRenderObjsProps = Object.assign(...[
|
||||
'width',
|
||||
'height',
|
||||
'backgroundColor',
|
||||
'showNavInfo',
|
||||
'enablePointerInteraction'
|
||||
].map(p => ({ [p]: bindRenderObjs.linkProp(p)})));
|
||||
const linkedRenderObjsMethods = Object.assign(
|
||||
...[
|
||||
'lights',
|
||||
'cameraPosition',
|
||||
'postProcessingComposer'
|
||||
].map(p => ({ [p]: bindRenderObjs.linkMethod(p)})),
|
||||
{
|
||||
graph2ScreenCoords: bindRenderObjs.linkMethod('getScreenCoords'),
|
||||
screen2GraphCoords: bindRenderObjs.linkMethod('getSceneCoords')
|
||||
}
|
||||
);
|
||||
|
||||
//
|
||||
|
||||
export default Kapsule({
|
||||
|
||||
props: {
|
||||
nodeLabel: { default: 'name', triggerUpdate: false },
|
||||
linkLabel: { default: 'name', triggerUpdate: false },
|
||||
linkHoverPrecision: { default: 1, onChange: (p, state) => state.renderObjs.lineHoverPrecision(p), triggerUpdate: false },
|
||||
enableNavigationControls: {
|
||||
default: true,
|
||||
onChange(enable, state) {
|
||||
const controls = state.renderObjs.controls();
|
||||
if (controls) {
|
||||
controls.enabled = enable;
|
||||
// trigger mouseup on re-enable to prevent sticky controls
|
||||
enable && controls.domElement && controls.domElement.dispatchEvent(new PointerEvent('pointerup'));
|
||||
}
|
||||
},
|
||||
triggerUpdate: false
|
||||
},
|
||||
enableNodeDrag: { default: true, triggerUpdate: false },
|
||||
onNodeDrag: { default: () => {}, triggerUpdate: false },
|
||||
onNodeDragEnd: { default: () => {}, triggerUpdate: false },
|
||||
onNodeClick: { triggerUpdate: false },
|
||||
onNodeRightClick: { triggerUpdate: false },
|
||||
onNodeHover: { triggerUpdate: false },
|
||||
onLinkClick: { triggerUpdate: false },
|
||||
onLinkRightClick: { triggerUpdate: false },
|
||||
onLinkHover: { triggerUpdate: false },
|
||||
onBackgroundClick: { triggerUpdate: false },
|
||||
onBackgroundRightClick: { triggerUpdate: false },
|
||||
...linkedFGProps,
|
||||
...linkedRenderObjsProps
|
||||
},
|
||||
|
||||
methods: {
|
||||
zoomToFit: function(state, transitionDuration, padding, ...bboxArgs) {
|
||||
state.renderObjs.fitToBbox(
|
||||
state.forceGraph.getGraphBbox(...bboxArgs),
|
||||
transitionDuration,
|
||||
padding
|
||||
);
|
||||
return this;
|
||||
},
|
||||
pauseAnimation: function(state) {
|
||||
if (state.animationFrameRequestId !== null) {
|
||||
cancelAnimationFrame(state.animationFrameRequestId);
|
||||
state.animationFrameRequestId = null;
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
resumeAnimation: function(state) {
|
||||
if (state.animationFrameRequestId === null) {
|
||||
this._animationCycle();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
_animationCycle(state) {
|
||||
if (state.enablePointerInteraction) {
|
||||
// reset canvas cursor (override dragControls cursor)
|
||||
this.renderer().domElement.style.cursor = null;
|
||||
}
|
||||
|
||||
// Frame cycle
|
||||
state.forceGraph.tickFrame();
|
||||
state.renderObjs.tick();
|
||||
state.animationFrameRequestId = requestAnimationFrame(this._animationCycle);
|
||||
},
|
||||
scene: state => state.renderObjs.scene(), // Expose scene
|
||||
camera: state => state.renderObjs.camera(), // Expose camera
|
||||
renderer: state => state.renderObjs.renderer(), // Expose renderer
|
||||
controls: state => state.renderObjs.controls(), // Expose controls
|
||||
tbControls: state => state.renderObjs.tbControls(), // To be deprecated
|
||||
_destructor: function() {
|
||||
this.pauseAnimation();
|
||||
this.graphData({ nodes: [], links: []});
|
||||
},
|
||||
...linkedFGMethods,
|
||||
...linkedRenderObjsMethods
|
||||
},
|
||||
|
||||
stateInit: ({ controlType, rendererConfig, extraRenderers }) => {
|
||||
const forceGraph = new ThreeForceGraph();
|
||||
return {
|
||||
forceGraph,
|
||||
renderObjs: ThreeRenderObjects({ controlType, rendererConfig, extraRenderers })
|
||||
.objects([forceGraph]) // Populate scene
|
||||
.lights([
|
||||
new three.AmbientLight(0xcccccc, Math.PI),
|
||||
new three.DirectionalLight(0xffffff, 0.6 * Math.PI)
|
||||
])
|
||||
}
|
||||
},
|
||||
|
||||
init: function(domNode, state) {
|
||||
// Wipe DOM
|
||||
domNode.innerHTML = '';
|
||||
|
||||
// Add relative container
|
||||
domNode.appendChild(state.container = document.createElement('div'));
|
||||
state.container.style.position = 'relative';
|
||||
|
||||
// Add renderObjs
|
||||
const roDomNode = document.createElement('div');
|
||||
state.container.appendChild(roDomNode);
|
||||
state.renderObjs(roDomNode);
|
||||
const camera = state.renderObjs.camera();
|
||||
const renderer = state.renderObjs.renderer();
|
||||
const controls = state.renderObjs.controls();
|
||||
controls.enabled = !!state.enableNavigationControls;
|
||||
state.lastSetCameraZ = camera.position.z;
|
||||
|
||||
// Add info space
|
||||
let infoElem;
|
||||
state.container.appendChild(infoElem = document.createElement('div'));
|
||||
infoElem.className = 'graph-info-msg';
|
||||
infoElem.textContent = '';
|
||||
|
||||
// config forcegraph
|
||||
state.forceGraph
|
||||
.onLoading(() => { infoElem.textContent = 'Loading...' })
|
||||
.onFinishLoading(() => { infoElem.textContent = '' })
|
||||
.onUpdate(() => {
|
||||
// sync graph data structures
|
||||
state.graphData = state.forceGraph.graphData();
|
||||
|
||||
// re-aim camera, if still in default position (not user modified)
|
||||
if (camera.position.x === 0 && camera.position.y === 0 && camera.position.z === state.lastSetCameraZ && state.graphData.nodes.length) {
|
||||
camera.lookAt(state.forceGraph.position);
|
||||
state.lastSetCameraZ = camera.position.z = Math.cbrt(state.graphData.nodes.length) * CAMERA_DISTANCE2NODES_FACTOR;
|
||||
}
|
||||
})
|
||||
.onFinishUpdate(() => {
|
||||
// Setup node drag interaction
|
||||
if (state._dragControls) {
|
||||
const curNodeDrag = state.graphData.nodes.find(node => node.__initialFixedPos && !node.__disposeControlsAfterDrag); // detect if there's a node being dragged using the existing drag controls
|
||||
if (curNodeDrag) {
|
||||
curNodeDrag.__disposeControlsAfterDrag = true; // postpone previous controls disposal until drag ends
|
||||
} else {
|
||||
state._dragControls.dispose(); // cancel previous drag controls
|
||||
}
|
||||
|
||||
state._dragControls = undefined;
|
||||
}
|
||||
|
||||
if (state.enableNodeDrag && state.enablePointerInteraction && state.forceEngine === 'd3') { // Can't access node positions programmatically in ngraph
|
||||
const dragControls = state._dragControls = new ThreeDragControls(
|
||||
state.graphData.nodes.map(node => node.__threeObj).filter(obj => obj),
|
||||
camera,
|
||||
renderer.domElement
|
||||
);
|
||||
|
||||
dragControls.addEventListener('dragstart', function (event) {
|
||||
controls.enabled = false; // Disable controls while dragging
|
||||
|
||||
// track drag object movement
|
||||
event.object.__initialPos = event.object.position.clone();
|
||||
event.object.__prevPos = event.object.position.clone();
|
||||
|
||||
const node = getGraphObj(event.object).__data;
|
||||
!node.__initialFixedPos && (node.__initialFixedPos = {fx: node.fx, fy: node.fy, fz: node.fz});
|
||||
!node.__initialPos && (node.__initialPos = {x: node.x, y: node.y, z: node.z});
|
||||
|
||||
// lock node
|
||||
['x', 'y', 'z'].forEach(c => node[`f${c}`] = node[c]);
|
||||
|
||||
// drag cursor
|
||||
renderer.domElement.classList.add('grabbable');
|
||||
});
|
||||
|
||||
dragControls.addEventListener('drag', function (event) {
|
||||
const nodeObj = getGraphObj(event.object);
|
||||
|
||||
if (!event.object.hasOwnProperty('__graphObjType')) {
|
||||
// If dragging a child of the node, update the node object instead
|
||||
const initPos = event.object.__initialPos;
|
||||
const prevPos = event.object.__prevPos;
|
||||
const newPos = event.object.position;
|
||||
|
||||
nodeObj.position.add(newPos.clone().sub(prevPos)); // translate node object by the motion delta
|
||||
prevPos.copy(newPos);
|
||||
newPos.copy(initPos); // reset child back to its initial position
|
||||
}
|
||||
|
||||
const node = nodeObj.__data;
|
||||
const newPos = nodeObj.position;
|
||||
const translate = {x: newPos.x - node.x, y: newPos.y - node.y, z: newPos.z - node.z};
|
||||
// Move fx/fy/fz (and x/y/z) of nodes based on object new position
|
||||
['x', 'y', 'z'].forEach(c => node[`f${c}`] = node[c] = newPos[c]);
|
||||
|
||||
state.forceGraph
|
||||
.d3AlphaTarget(0.3) // keep engine running at low intensity throughout drag
|
||||
.resetCountdown(); // prevent freeze while dragging
|
||||
|
||||
node.__dragged = true;
|
||||
state.onNodeDrag(node, translate);
|
||||
});
|
||||
|
||||
dragControls.addEventListener('dragend', function (event) {
|
||||
delete(event.object.__initialPos); // remove tracking attributes
|
||||
delete(event.object.__prevPos);
|
||||
|
||||
const node = getGraphObj(event.object).__data;
|
||||
|
||||
// dispose previous controls if needed
|
||||
if (node.__disposeControlsAfterDrag) {
|
||||
dragControls.dispose();
|
||||
delete(node.__disposeControlsAfterDrag);
|
||||
}
|
||||
|
||||
const initFixedPos = node.__initialFixedPos;
|
||||
const initPos = node.__initialPos;
|
||||
const translate = {x: initPos.x - node.x, y: initPos.y - node.y, z: initPos.z - node.z};
|
||||
if (initFixedPos) {
|
||||
['x', 'y', 'z'].forEach(c => {
|
||||
const fc = `f${c}`;
|
||||
if (initFixedPos[fc] === undefined) {
|
||||
delete(node[fc])
|
||||
}
|
||||
});
|
||||
delete(node.__initialFixedPos);
|
||||
delete(node.__initialPos);
|
||||
if (node.__dragged) {
|
||||
delete(node.__dragged);
|
||||
state.onNodeDragEnd(node, translate);
|
||||
}
|
||||
}
|
||||
|
||||
state.forceGraph
|
||||
.d3AlphaTarget(0) // release engine low intensity
|
||||
.resetCountdown(); // let the engine readjust after releasing fixed nodes
|
||||
|
||||
if (state.enableNavigationControls) {
|
||||
controls.enabled = true; // Re-enable controls
|
||||
controls.domElement && controls.domElement.ownerDocument && controls.domElement.ownerDocument.dispatchEvent(
|
||||
// simulate mouseup to ensure the controls don't take over after dragend
|
||||
new PointerEvent('pointerup', { pointerType: 'touch' })
|
||||
);
|
||||
}
|
||||
|
||||
// clear cursor
|
||||
renderer.domElement.classList.remove('grabbable');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// config renderObjs
|
||||
three.REVISION < 155 && (state.renderObjs.renderer().useLegacyLights = false); // force behavior for three < 155
|
||||
state.renderObjs
|
||||
.hoverOrderComparator((a, b) => {
|
||||
// Prioritize graph objects
|
||||
const aObj = getGraphObj(a);
|
||||
if (!aObj) return 1;
|
||||
const bObj = getGraphObj(b);
|
||||
if (!bObj) return -1;
|
||||
|
||||
// Prioritize nodes over links
|
||||
const isNode = o => o.__graphObjType === 'node';
|
||||
return isNode(bObj) - isNode(aObj);
|
||||
})
|
||||
.tooltipContent(obj => {
|
||||
const graphObj = getGraphObj(obj);
|
||||
return graphObj ? accessorFn(state[`${graphObj.__graphObjType}Label`])(graphObj.__data) || '' : '';
|
||||
})
|
||||
.hoverDuringDrag(false)
|
||||
.onHover(obj => {
|
||||
// Update tooltip and trigger onHover events
|
||||
const hoverObj = getGraphObj(obj);
|
||||
|
||||
if (hoverObj !== state.hoverObj) {
|
||||
const prevObjType = state.hoverObj ? state.hoverObj.__graphObjType : null;
|
||||
const prevObjData = state.hoverObj ? state.hoverObj.__data : null;
|
||||
const objType = hoverObj ? hoverObj.__graphObjType : null;
|
||||
const objData = hoverObj ? hoverObj.__data : null;
|
||||
if (prevObjType && prevObjType !== objType) {
|
||||
// Hover out
|
||||
const fn = state[`on${prevObjType === 'node' ? 'Node' : 'Link'}Hover`];
|
||||
fn && fn(null, prevObjData);
|
||||
}
|
||||
if (objType) {
|
||||
// Hover in
|
||||
const fn = state[`on${objType === 'node' ? 'Node' : 'Link'}Hover`];
|
||||
fn && fn(objData, prevObjType === objType ? prevObjData : null);
|
||||
}
|
||||
|
||||
// set pointer if hovered object is clickable
|
||||
renderer.domElement.classList[
|
||||
((hoverObj && state[`on${objType === 'node' ? 'Node' : 'Link'}Click`]) || (!hoverObj && state.onBackgroundClick)) ? 'add' : 'remove'
|
||||
]('clickable');
|
||||
|
||||
state.hoverObj = hoverObj;
|
||||
}
|
||||
})
|
||||
.clickAfterDrag(false)
|
||||
.onClick((obj, ev) => {
|
||||
const graphObj = getGraphObj(obj);
|
||||
if (graphObj) {
|
||||
const fn = state[`on${graphObj.__graphObjType === 'node' ? 'Node' : 'Link'}Click`];
|
||||
fn && fn(graphObj.__data, ev);
|
||||
} else {
|
||||
state.onBackgroundClick && state.onBackgroundClick(ev);
|
||||
}
|
||||
})
|
||||
.onRightClick((obj, ev) => {
|
||||
// Handle right-click events
|
||||
const graphObj = getGraphObj(obj);
|
||||
if (graphObj) {
|
||||
const fn = state[`on${graphObj.__graphObjType === 'node' ? 'Node' : 'Link'}RightClick`];
|
||||
fn && fn(graphObj.__data, ev);
|
||||
} else {
|
||||
state.onBackgroundRightClick && state.onBackgroundRightClick(ev);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
// Kick-off renderer
|
||||
this._animationCycle();
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
function getGraphObj(object) {
|
||||
let obj = object;
|
||||
// recurse up object chain until finding the graph object
|
||||
while (obj && !obj.hasOwnProperty('__graphObjType')) {
|
||||
obj = obj.parent;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
|
@ -1,410 +0,0 @@
|
|||
import {
|
||||
Controls,
|
||||
Matrix4,
|
||||
Plane,
|
||||
Raycaster,
|
||||
Vector2,
|
||||
Vector3,
|
||||
MOUSE,
|
||||
TOUCH
|
||||
} from './three.module.js';
|
||||
|
||||
const _plane = new Plane();
|
||||
|
||||
const _pointer = new Vector2();
|
||||
const _offset = new Vector3();
|
||||
const _diff = new Vector2();
|
||||
const _previousPointer = new Vector2();
|
||||
const _intersection = new Vector3();
|
||||
const _worldPosition = new Vector3();
|
||||
const _inverseMatrix = new Matrix4();
|
||||
|
||||
const _up = new Vector3();
|
||||
const _right = new Vector3();
|
||||
|
||||
let _selected = null, _hovered = null;
|
||||
const _intersections = [];
|
||||
|
||||
const STATE = {
|
||||
NONE: - 1,
|
||||
PAN: 0,
|
||||
ROTATE: 1
|
||||
};
|
||||
|
||||
class DragControls extends Controls {
|
||||
|
||||
constructor( objects, camera, domElement = null ) {
|
||||
|
||||
super( camera, domElement );
|
||||
|
||||
this.objects = objects;
|
||||
|
||||
this.recursive = true;
|
||||
this.transformGroup = false;
|
||||
this.rotateSpeed = 1;
|
||||
|
||||
this.raycaster = new Raycaster();
|
||||
|
||||
// interaction
|
||||
|
||||
this.mouseButtons = { LEFT: MOUSE.PAN, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.ROTATE };
|
||||
this.touches = { ONE: TOUCH.PAN };
|
||||
|
||||
// event listeners
|
||||
|
||||
this._onPointerMove = onPointerMove.bind( this );
|
||||
this._onPointerDown = onPointerDown.bind( this );
|
||||
this._onPointerCancel = onPointerCancel.bind( this );
|
||||
this._onContextMenu = onContextMenu.bind( this );
|
||||
|
||||
//
|
||||
|
||||
if ( domElement !== null ) {
|
||||
|
||||
this.connect();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
connect() {
|
||||
|
||||
this.domElement.addEventListener( 'pointermove', this._onPointerMove );
|
||||
this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
|
||||
this.domElement.addEventListener( 'pointerup', this._onPointerCancel );
|
||||
this.domElement.addEventListener( 'pointerleave', this._onPointerCancel );
|
||||
this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
|
||||
|
||||
this.domElement.style.touchAction = 'none'; // disable touch scroll
|
||||
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
||||
this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
|
||||
this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
|
||||
this.domElement.removeEventListener( 'pointerup', this._onPointerCancel );
|
||||
this.domElement.removeEventListener( 'pointerleave', this._onPointerCancel );
|
||||
this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
|
||||
|
||||
this.domElement.style.touchAction = 'auto';
|
||||
this.domElement.style.cursor = '';
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
this.disconnect();
|
||||
|
||||
}
|
||||
|
||||
_updatePointer( event ) {
|
||||
|
||||
const rect = this.domElement.getBoundingClientRect();
|
||||
|
||||
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
|
||||
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
|
||||
|
||||
}
|
||||
|
||||
_updateState( event ) {
|
||||
|
||||
// determine action
|
||||
|
||||
let action;
|
||||
|
||||
if ( event.pointerType === 'touch' ) {
|
||||
|
||||
action = this.touches.ONE;
|
||||
|
||||
} else {
|
||||
|
||||
switch ( event.button ) {
|
||||
|
||||
case 0:
|
||||
|
||||
action = this.mouseButtons.LEFT;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
|
||||
action = this.mouseButtons.MIDDLE;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
|
||||
action = this.mouseButtons.RIGHT;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
action = null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// determine state
|
||||
|
||||
switch ( action ) {
|
||||
|
||||
case MOUSE.PAN:
|
||||
case TOUCH.PAN:
|
||||
|
||||
this.state = STATE.PAN;
|
||||
|
||||
break;
|
||||
|
||||
case MOUSE.ROTATE:
|
||||
case TOUCH.ROTATE:
|
||||
|
||||
this.state = STATE.ROTATE;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
this.state = STATE.NONE;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getRaycaster() {
|
||||
|
||||
console.warn( 'THREE.DragControls: getRaycaster() has been deprecated. Use controls.raycaster instead.' ); // @deprecated r169
|
||||
|
||||
return this.raycaster;
|
||||
|
||||
}
|
||||
|
||||
setObjects( objects ) {
|
||||
|
||||
console.warn( 'THREE.DragControls: setObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169
|
||||
|
||||
this.objects = objects;
|
||||
|
||||
}
|
||||
|
||||
getObjects() {
|
||||
|
||||
console.warn( 'THREE.DragControls: getObjects() has been deprecated. Use controls.objects instead.' ); // @deprecated r169
|
||||
|
||||
return this.objects;
|
||||
|
||||
}
|
||||
|
||||
activate() {
|
||||
|
||||
console.warn( 'THREE.DragControls: activate() has been renamed to connect().' ); // @deprecated r169
|
||||
this.connect();
|
||||
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
|
||||
console.warn( 'THREE.DragControls: deactivate() has been renamed to disconnect().' ); // @deprecated r169
|
||||
this.disconnect();
|
||||
|
||||
}
|
||||
|
||||
set mode( value ) {
|
||||
|
||||
console.warn( 'THREE.DragControls: The .mode property has been removed. Define the type of transformation via the .mouseButtons or .touches properties.' ); // @deprecated r169
|
||||
|
||||
}
|
||||
|
||||
get mode() {
|
||||
|
||||
console.warn( 'THREE.DragControls: The .mode property has been removed. Define the type of transformation via the .mouseButtons or .touches properties.' ); // @deprecated r169
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function onPointerMove( event ) {
|
||||
|
||||
const camera = this.object;
|
||||
const domElement = this.domElement;
|
||||
const raycaster = this.raycaster;
|
||||
|
||||
if ( this.enabled === false ) return;
|
||||
|
||||
this._updatePointer( event );
|
||||
|
||||
raycaster.setFromCamera( _pointer, camera );
|
||||
|
||||
if ( _selected ) {
|
||||
|
||||
if ( this.state === STATE.PAN ) {
|
||||
|
||||
if ( raycaster.ray.intersectPlane( _plane, _intersection ) ) {
|
||||
|
||||
_selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
|
||||
|
||||
}
|
||||
|
||||
} else if ( this.state === STATE.ROTATE ) {
|
||||
|
||||
_diff.subVectors( _pointer, _previousPointer ).multiplyScalar( this.rotateSpeed );
|
||||
_selected.rotateOnWorldAxis( _up, _diff.x );
|
||||
_selected.rotateOnWorldAxis( _right.normalize(), - _diff.y );
|
||||
|
||||
}
|
||||
|
||||
this.dispatchEvent( { type: 'drag', object: _selected } );
|
||||
|
||||
_previousPointer.copy( _pointer );
|
||||
|
||||
} else {
|
||||
|
||||
// hover support
|
||||
|
||||
if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) {
|
||||
|
||||
_intersections.length = 0;
|
||||
|
||||
raycaster.setFromCamera( _pointer, camera );
|
||||
raycaster.intersectObjects( this.objects, this.recursive, _intersections );
|
||||
|
||||
if ( _intersections.length > 0 ) {
|
||||
|
||||
const object = _intersections[ 0 ].object;
|
||||
|
||||
_plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) );
|
||||
|
||||
if ( _hovered !== object && _hovered !== null ) {
|
||||
|
||||
this.dispatchEvent( { type: 'hoveroff', object: _hovered } );
|
||||
|
||||
domElement.style.cursor = 'auto';
|
||||
_hovered = null;
|
||||
|
||||
}
|
||||
|
||||
if ( _hovered !== object ) {
|
||||
|
||||
this.dispatchEvent( { type: 'hoveron', object: object } );
|
||||
|
||||
domElement.style.cursor = 'pointer';
|
||||
_hovered = object;
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if ( _hovered !== null ) {
|
||||
|
||||
this.dispatchEvent( { type: 'hoveroff', object: _hovered } );
|
||||
|
||||
domElement.style.cursor = 'auto';
|
||||
_hovered = null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_previousPointer.copy( _pointer );
|
||||
|
||||
}
|
||||
|
||||
function onPointerDown( event ) {
|
||||
|
||||
const camera = this.object;
|
||||
const domElement = this.domElement;
|
||||
const raycaster = this.raycaster;
|
||||
|
||||
if ( this.enabled === false ) return;
|
||||
|
||||
this._updatePointer( event );
|
||||
this._updateState( event );
|
||||
|
||||
_intersections.length = 0;
|
||||
|
||||
raycaster.setFromCamera( _pointer, camera );
|
||||
raycaster.intersectObjects( this.objects, this.recursive, _intersections );
|
||||
|
||||
if ( _intersections.length > 0 ) {
|
||||
|
||||
if ( this.transformGroup === true ) {
|
||||
|
||||
// look for the outermost group in the object's upper hierarchy
|
||||
|
||||
_selected = findGroup( _intersections[ 0 ].object );
|
||||
|
||||
} else {
|
||||
|
||||
_selected = _intersections[ 0 ].object;
|
||||
|
||||
}
|
||||
|
||||
_plane.setFromNormalAndCoplanarPoint( camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
|
||||
|
||||
if ( raycaster.ray.intersectPlane( _plane, _intersection ) ) {
|
||||
|
||||
if ( this.state === STATE.PAN ) {
|
||||
|
||||
_inverseMatrix.copy( _selected.parent.matrixWorld ).invert();
|
||||
_offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
|
||||
|
||||
} else if ( this.state === STATE.ROTATE ) {
|
||||
|
||||
// the controls only support Y+ up
|
||||
_up.set( 0, 1, 0 ).applyQuaternion( camera.quaternion ).normalize();
|
||||
_right.set( 1, 0, 0 ).applyQuaternion( camera.quaternion ).normalize();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
domElement.style.cursor = 'move';
|
||||
|
||||
this.dispatchEvent( { type: 'dragstart', object: _selected } );
|
||||
|
||||
}
|
||||
|
||||
_previousPointer.copy( _pointer );
|
||||
|
||||
}
|
||||
|
||||
function onPointerCancel() {
|
||||
|
||||
if ( this.enabled === false ) return;
|
||||
|
||||
if ( _selected ) {
|
||||
|
||||
this.dispatchEvent( { type: 'dragend', object: _selected } );
|
||||
|
||||
_selected = null;
|
||||
|
||||
}
|
||||
|
||||
this.domElement.style.cursor = _hovered ? 'pointer' : 'auto';
|
||||
|
||||
this.state = STATE.NONE;
|
||||
|
||||
}
|
||||
|
||||
function onContextMenu( event ) {
|
||||
|
||||
if ( this.enabled === false ) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
}
|
||||
|
||||
function findGroup( obj, group = null ) {
|
||||
|
||||
if ( obj.isGroup ) group = obj;
|
||||
|
||||
if ( obj.parent === null ) return group;
|
||||
|
||||
return findGroup( obj.parent, group );
|
||||
|
||||
}
|
||||
|
||||
export { DragControls };
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// Version 1.5.0 accessor-fn - https://github.com/vasturiano/accessor-fn
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.accessorFn = factory());
|
||||
})(this, (function () { 'use strict';
|
||||
|
||||
var index = (function (p) {
|
||||
return typeof p === 'function' ? p // fn
|
||||
: typeof p === 'string' ? function (obj) {
|
||||
return obj[p];
|
||||
} // property name
|
||||
: function (obj) {
|
||||
return p;
|
||||
};
|
||||
}); // constant
|
||||
|
||||
return index;
|
||||
|
||||
}));
|
||||
//# sourceMappingURL=accessor-fn.js.map
|
||||
export default TableCsv
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
export default function(kapsulePropName, kapsuleType) {
|
||||
|
||||
const dummyK = new kapsuleType(); // To extract defaults
|
||||
dummyK._destructor && dummyK._destructor();
|
||||
|
||||
return {
|
||||
linkProp: function(prop) { // link property config
|
||||
return {
|
||||
default: dummyK[prop](),
|
||||
onChange(v, state) { state[kapsulePropName][prop](v) },
|
||||
triggerUpdate: false
|
||||
}
|
||||
},
|
||||
linkMethod: function(method) { // link method pass-through
|
||||
return function(state, ...args) {
|
||||
const kapsuleInstance = state[kapsulePropName];
|
||||
const returnVal = kapsuleInstance[method](...args);
|
||||
|
||||
return returnVal === kapsuleInstance
|
||||
? this // chain based on the parent object, not the inner kapsule
|
||||
: returnVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,716 +0,0 @@
|
|||
// Version 1.14.4 kapsule - https://github.com/vasturiano/kapsule
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Kapsule = factory());
|
||||
})(this, (function () { 'use strict';
|
||||
|
||||
function _iterableToArrayLimit(arr, i) {
|
||||
var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"];
|
||||
if (null != _i) {
|
||||
var _s,
|
||||
_e,
|
||||
_x,
|
||||
_r,
|
||||
_arr = [],
|
||||
_n = !0,
|
||||
_d = !1;
|
||||
try {
|
||||
if (_x = (_i = _i.call(arr)).next, 0 === i) {
|
||||
if (Object(_i) !== _i) return;
|
||||
_n = !1;
|
||||
} else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0);
|
||||
} catch (err) {
|
||||
_d = !0, _e = err;
|
||||
} finally {
|
||||
try {
|
||||
if (!_n && null != _i.return && (_r = _i.return(), Object(_r) !== _r)) return;
|
||||
} finally {
|
||||
if (_d) throw _e;
|
||||
}
|
||||
}
|
||||
return _arr;
|
||||
}
|
||||
}
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
function _defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];
|
||||
descriptor.enumerable = descriptor.enumerable || false;
|
||||
descriptor.configurable = true;
|
||||
if ("value" in descriptor) descriptor.writable = true;
|
||||
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
|
||||
}
|
||||
}
|
||||
function _createClass(Constructor, protoProps, staticProps) {
|
||||
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
|
||||
if (staticProps) _defineProperties(Constructor, staticProps);
|
||||
Object.defineProperty(Constructor, "prototype", {
|
||||
writable: false
|
||||
});
|
||||
return Constructor;
|
||||
}
|
||||
function _slicedToArray(arr, i) {
|
||||
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
|
||||
}
|
||||
function _arrayWithHoles(arr) {
|
||||
if (Array.isArray(arr)) return arr;
|
||||
}
|
||||
function _unsupportedIterableToArray(o, minLen) {
|
||||
if (!o) return;
|
||||
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
|
||||
var n = Object.prototype.toString.call(o).slice(8, -1);
|
||||
if (n === "Object" && o.constructor) n = o.constructor.name;
|
||||
if (n === "Map" || n === "Set") return Array.from(o);
|
||||
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
|
||||
}
|
||||
function _arrayLikeToArray(arr, len) {
|
||||
if (len == null || len > arr.length) len = arr.length;
|
||||
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
|
||||
return arr2;
|
||||
}
|
||||
function _nonIterableRest() {
|
||||
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
||||
}
|
||||
function _toPrimitive(input, hint) {
|
||||
if (typeof input !== "object" || input === null) return input;
|
||||
var prim = input[Symbol.toPrimitive];
|
||||
if (prim !== undefined) {
|
||||
var res = prim.call(input, hint || "default");
|
||||
if (typeof res !== "object") return res;
|
||||
throw new TypeError("@@toPrimitive must return a primitive value.");
|
||||
}
|
||||
return (hint === "string" ? String : Number)(input);
|
||||
}
|
||||
function _toPropertyKey(arg) {
|
||||
var key = _toPrimitive(arg, "string");
|
||||
return typeof key === "symbol" ? key : String(key);
|
||||
}
|
||||
|
||||
/** Detect free variable `global` from Node.js. */
|
||||
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
|
||||
|
||||
var freeGlobal$1 = freeGlobal;
|
||||
|
||||
/** Detect free variable `self`. */
|
||||
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
|
||||
|
||||
/** Used as a reference to the global object. */
|
||||
var root = freeGlobal$1 || freeSelf || Function('return this')();
|
||||
|
||||
var root$1 = root;
|
||||
|
||||
/** Built-in value references. */
|
||||
var Symbol$1 = root$1.Symbol;
|
||||
|
||||
var Symbol$2 = Symbol$1;
|
||||
|
||||
/** Used for built-in method references. */
|
||||
var objectProto$1 = Object.prototype;
|
||||
|
||||
/** Used to check objects for own properties. */
|
||||
var hasOwnProperty = objectProto$1.hasOwnProperty;
|
||||
|
||||
/**
|
||||
* Used to resolve the
|
||||
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
|
||||
* of values.
|
||||
*/
|
||||
var nativeObjectToString$1 = objectProto$1.toString;
|
||||
|
||||
/** Built-in value references. */
|
||||
var symToStringTag$1 = Symbol$2 ? Symbol$2.toStringTag : undefined;
|
||||
|
||||
/**
|
||||
* A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
|
||||
*
|
||||
* @private
|
||||
* @param {*} value The value to query.
|
||||
* @returns {string} Returns the raw `toStringTag`.
|
||||
*/
|
||||
function getRawTag(value) {
|
||||
var isOwn = hasOwnProperty.call(value, symToStringTag$1),
|
||||
tag = value[symToStringTag$1];
|
||||
|
||||
try {
|
||||
value[symToStringTag$1] = undefined;
|
||||
var unmasked = true;
|
||||
} catch (e) {}
|
||||
|
||||
var result = nativeObjectToString$1.call(value);
|
||||
if (unmasked) {
|
||||
if (isOwn) {
|
||||
value[symToStringTag$1] = tag;
|
||||
} else {
|
||||
delete value[symToStringTag$1];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Used for built-in method references. */
|
||||
var objectProto = Object.prototype;
|
||||
|
||||
/**
|
||||
* Used to resolve the
|
||||
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
|
||||
* of values.
|
||||
*/
|
||||
var nativeObjectToString = objectProto.toString;
|
||||
|
||||
/**
|
||||
* Converts `value` to a string using `Object.prototype.toString`.
|
||||
*
|
||||
* @private
|
||||
* @param {*} value The value to convert.
|
||||
* @returns {string} Returns the converted string.
|
||||
*/
|
||||
function objectToString(value) {
|
||||
return nativeObjectToString.call(value);
|
||||
}
|
||||
|
||||
/** `Object#toString` result references. */
|
||||
var nullTag = '[object Null]',
|
||||
undefinedTag = '[object Undefined]';
|
||||
|
||||
/** Built-in value references. */
|
||||
var symToStringTag = Symbol$2 ? Symbol$2.toStringTag : undefined;
|
||||
|
||||
/**
|
||||
* The base implementation of `getTag` without fallbacks for buggy environments.
|
||||
*
|
||||
* @private
|
||||
* @param {*} value The value to query.
|
||||
* @returns {string} Returns the `toStringTag`.
|
||||
*/
|
||||
function baseGetTag(value) {
|
||||
if (value == null) {
|
||||
return value === undefined ? undefinedTag : nullTag;
|
||||
}
|
||||
return (symToStringTag && symToStringTag in Object(value))
|
||||
? getRawTag(value)
|
||||
: objectToString(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if `value` is object-like. A value is object-like if it's not `null`
|
||||
* and has a `typeof` result of "object".
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 4.0.0
|
||||
* @category Lang
|
||||
* @param {*} value The value to check.
|
||||
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
|
||||
* @example
|
||||
*
|
||||
* _.isObjectLike({});
|
||||
* // => true
|
||||
*
|
||||
* _.isObjectLike([1, 2, 3]);
|
||||
* // => true
|
||||
*
|
||||
* _.isObjectLike(_.noop);
|
||||
* // => false
|
||||
*
|
||||
* _.isObjectLike(null);
|
||||
* // => false
|
||||
*/
|
||||
function isObjectLike(value) {
|
||||
return value != null && typeof value == 'object';
|
||||
}
|
||||
|
||||
/** `Object#toString` result references. */
|
||||
var symbolTag = '[object Symbol]';
|
||||
|
||||
/**
|
||||
* Checks if `value` is classified as a `Symbol` primitive or object.
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 4.0.0
|
||||
* @category Lang
|
||||
* @param {*} value The value to check.
|
||||
* @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
|
||||
* @example
|
||||
*
|
||||
* _.isSymbol(Symbol.iterator);
|
||||
* // => true
|
||||
*
|
||||
* _.isSymbol('abc');
|
||||
* // => false
|
||||
*/
|
||||
function isSymbol(value) {
|
||||
return typeof value == 'symbol' ||
|
||||
(isObjectLike(value) && baseGetTag(value) == symbolTag);
|
||||
}
|
||||
|
||||
/** Used to match a single whitespace character. */
|
||||
var reWhitespace = /\s/;
|
||||
|
||||
/**
|
||||
* Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace
|
||||
* character of `string`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} string The string to inspect.
|
||||
* @returns {number} Returns the index of the last non-whitespace character.
|
||||
*/
|
||||
function trimmedEndIndex(string) {
|
||||
var index = string.length;
|
||||
|
||||
while (index-- && reWhitespace.test(string.charAt(index))) {}
|
||||
return index;
|
||||
}
|
||||
|
||||
/** Used to match leading whitespace. */
|
||||
var reTrimStart = /^\s+/;
|
||||
|
||||
/**
|
||||
* The base implementation of `_.trim`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} string The string to trim.
|
||||
* @returns {string} Returns the trimmed string.
|
||||
*/
|
||||
function baseTrim(string) {
|
||||
return string
|
||||
? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '')
|
||||
: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if `value` is the
|
||||
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
|
||||
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 0.1.0
|
||||
* @category Lang
|
||||
* @param {*} value The value to check.
|
||||
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
|
||||
* @example
|
||||
*
|
||||
* _.isObject({});
|
||||
* // => true
|
||||
*
|
||||
* _.isObject([1, 2, 3]);
|
||||
* // => true
|
||||
*
|
||||
* _.isObject(_.noop);
|
||||
* // => true
|
||||
*
|
||||
* _.isObject(null);
|
||||
* // => false
|
||||
*/
|
||||
function isObject(value) {
|
||||
var type = typeof value;
|
||||
return value != null && (type == 'object' || type == 'function');
|
||||
}
|
||||
|
||||
/** Used as references for various `Number` constants. */
|
||||
var NAN = 0 / 0;
|
||||
|
||||
/** Used to detect bad signed hexadecimal string values. */
|
||||
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
|
||||
|
||||
/** Used to detect binary string values. */
|
||||
var reIsBinary = /^0b[01]+$/i;
|
||||
|
||||
/** Used to detect octal string values. */
|
||||
var reIsOctal = /^0o[0-7]+$/i;
|
||||
|
||||
/** Built-in method references without a dependency on `root`. */
|
||||
var freeParseInt = parseInt;
|
||||
|
||||
/**
|
||||
* Converts `value` to a number.
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 4.0.0
|
||||
* @category Lang
|
||||
* @param {*} value The value to process.
|
||||
* @returns {number} Returns the number.
|
||||
* @example
|
||||
*
|
||||
* _.toNumber(3.2);
|
||||
* // => 3.2
|
||||
*
|
||||
* _.toNumber(Number.MIN_VALUE);
|
||||
* // => 5e-324
|
||||
*
|
||||
* _.toNumber(Infinity);
|
||||
* // => Infinity
|
||||
*
|
||||
* _.toNumber('3.2');
|
||||
* // => 3.2
|
||||
*/
|
||||
function toNumber(value) {
|
||||
if (typeof value == 'number') {
|
||||
return value;
|
||||
}
|
||||
if (isSymbol(value)) {
|
||||
return NAN;
|
||||
}
|
||||
if (isObject(value)) {
|
||||
var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
|
||||
value = isObject(other) ? (other + '') : other;
|
||||
}
|
||||
if (typeof value != 'string') {
|
||||
return value === 0 ? value : +value;
|
||||
}
|
||||
value = baseTrim(value);
|
||||
var isBinary = reIsBinary.test(value);
|
||||
return (isBinary || reIsOctal.test(value))
|
||||
? freeParseInt(value.slice(2), isBinary ? 2 : 8)
|
||||
: (reIsBadHex.test(value) ? NAN : +value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timestamp of the number of milliseconds that have elapsed since
|
||||
* the Unix epoch (1 January 1970 00:00:00 UTC).
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 2.4.0
|
||||
* @category Date
|
||||
* @returns {number} Returns the timestamp.
|
||||
* @example
|
||||
*
|
||||
* _.defer(function(stamp) {
|
||||
* console.log(_.now() - stamp);
|
||||
* }, _.now());
|
||||
* // => Logs the number of milliseconds it took for the deferred invocation.
|
||||
*/
|
||||
var now = function() {
|
||||
return root$1.Date.now();
|
||||
};
|
||||
|
||||
var now$1 = now;
|
||||
|
||||
/** Error message constants. */
|
||||
var FUNC_ERROR_TEXT = 'Expected a function';
|
||||
|
||||
/* Built-in method references for those with the same name as other `lodash` methods. */
|
||||
var nativeMax = Math.max,
|
||||
nativeMin = Math.min;
|
||||
|
||||
/**
|
||||
* Creates a debounced function that delays invoking `func` until after `wait`
|
||||
* milliseconds have elapsed since the last time the debounced function was
|
||||
* invoked. The debounced function comes with a `cancel` method to cancel
|
||||
* delayed `func` invocations and a `flush` method to immediately invoke them.
|
||||
* Provide `options` to indicate whether `func` should be invoked on the
|
||||
* leading and/or trailing edge of the `wait` timeout. The `func` is invoked
|
||||
* with the last arguments provided to the debounced function. Subsequent
|
||||
* calls to the debounced function return the result of the last `func`
|
||||
* invocation.
|
||||
*
|
||||
* **Note:** If `leading` and `trailing` options are `true`, `func` is
|
||||
* invoked on the trailing edge of the timeout only if the debounced function
|
||||
* is invoked more than once during the `wait` timeout.
|
||||
*
|
||||
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
|
||||
* until to the next tick, similar to `setTimeout` with a timeout of `0`.
|
||||
*
|
||||
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
|
||||
* for details over the differences between `_.debounce` and `_.throttle`.
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 0.1.0
|
||||
* @category Function
|
||||
* @param {Function} func The function to debounce.
|
||||
* @param {number} [wait=0] The number of milliseconds to delay.
|
||||
* @param {Object} [options={}] The options object.
|
||||
* @param {boolean} [options.leading=false]
|
||||
* Specify invoking on the leading edge of the timeout.
|
||||
* @param {number} [options.maxWait]
|
||||
* The maximum time `func` is allowed to be delayed before it's invoked.
|
||||
* @param {boolean} [options.trailing=true]
|
||||
* Specify invoking on the trailing edge of the timeout.
|
||||
* @returns {Function} Returns the new debounced function.
|
||||
* @example
|
||||
*
|
||||
* // Avoid costly calculations while the window size is in flux.
|
||||
* jQuery(window).on('resize', _.debounce(calculateLayout, 150));
|
||||
*
|
||||
* // Invoke `sendMail` when clicked, debouncing subsequent calls.
|
||||
* jQuery(element).on('click', _.debounce(sendMail, 300, {
|
||||
* 'leading': true,
|
||||
* 'trailing': false
|
||||
* }));
|
||||
*
|
||||
* // Ensure `batchLog` is invoked once after 1 second of debounced calls.
|
||||
* var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
|
||||
* var source = new EventSource('/stream');
|
||||
* jQuery(source).on('message', debounced);
|
||||
*
|
||||
* // Cancel the trailing debounced invocation.
|
||||
* jQuery(window).on('popstate', debounced.cancel);
|
||||
*/
|
||||
function debounce(func, wait, options) {
|
||||
var lastArgs,
|
||||
lastThis,
|
||||
maxWait,
|
||||
result,
|
||||
timerId,
|
||||
lastCallTime,
|
||||
lastInvokeTime = 0,
|
||||
leading = false,
|
||||
maxing = false,
|
||||
trailing = true;
|
||||
|
||||
if (typeof func != 'function') {
|
||||
throw new TypeError(FUNC_ERROR_TEXT);
|
||||
}
|
||||
wait = toNumber(wait) || 0;
|
||||
if (isObject(options)) {
|
||||
leading = !!options.leading;
|
||||
maxing = 'maxWait' in options;
|
||||
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
|
||||
trailing = 'trailing' in options ? !!options.trailing : trailing;
|
||||
}
|
||||
|
||||
function invokeFunc(time) {
|
||||
var args = lastArgs,
|
||||
thisArg = lastThis;
|
||||
|
||||
lastArgs = lastThis = undefined;
|
||||
lastInvokeTime = time;
|
||||
result = func.apply(thisArg, args);
|
||||
return result;
|
||||
}
|
||||
|
||||
function leadingEdge(time) {
|
||||
// Reset any `maxWait` timer.
|
||||
lastInvokeTime = time;
|
||||
// Start the timer for the trailing edge.
|
||||
timerId = setTimeout(timerExpired, wait);
|
||||
// Invoke the leading edge.
|
||||
return leading ? invokeFunc(time) : result;
|
||||
}
|
||||
|
||||
function remainingWait(time) {
|
||||
var timeSinceLastCall = time - lastCallTime,
|
||||
timeSinceLastInvoke = time - lastInvokeTime,
|
||||
timeWaiting = wait - timeSinceLastCall;
|
||||
|
||||
return maxing
|
||||
? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
|
||||
: timeWaiting;
|
||||
}
|
||||
|
||||
function shouldInvoke(time) {
|
||||
var timeSinceLastCall = time - lastCallTime,
|
||||
timeSinceLastInvoke = time - lastInvokeTime;
|
||||
|
||||
// Either this is the first call, activity has stopped and we're at the
|
||||
// trailing edge, the system time has gone backwards and we're treating
|
||||
// it as the trailing edge, or we've hit the `maxWait` limit.
|
||||
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
|
||||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
|
||||
}
|
||||
|
||||
function timerExpired() {
|
||||
var time = now$1();
|
||||
if (shouldInvoke(time)) {
|
||||
return trailingEdge(time);
|
||||
}
|
||||
// Restart the timer.
|
||||
timerId = setTimeout(timerExpired, remainingWait(time));
|
||||
}
|
||||
|
||||
function trailingEdge(time) {
|
||||
timerId = undefined;
|
||||
|
||||
// Only invoke if we have `lastArgs` which means `func` has been
|
||||
// debounced at least once.
|
||||
if (trailing && lastArgs) {
|
||||
return invokeFunc(time);
|
||||
}
|
||||
lastArgs = lastThis = undefined;
|
||||
return result;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (timerId !== undefined) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
lastInvokeTime = 0;
|
||||
lastArgs = lastCallTime = lastThis = timerId = undefined;
|
||||
}
|
||||
|
||||
function flush() {
|
||||
return timerId === undefined ? result : trailingEdge(now$1());
|
||||
}
|
||||
|
||||
function debounced() {
|
||||
var time = now$1(),
|
||||
isInvoking = shouldInvoke(time);
|
||||
|
||||
lastArgs = arguments;
|
||||
lastThis = this;
|
||||
lastCallTime = time;
|
||||
|
||||
if (isInvoking) {
|
||||
if (timerId === undefined) {
|
||||
return leadingEdge(lastCallTime);
|
||||
}
|
||||
if (maxing) {
|
||||
// Handle invocations in a tight loop.
|
||||
clearTimeout(timerId);
|
||||
timerId = setTimeout(timerExpired, wait);
|
||||
return invokeFunc(lastCallTime);
|
||||
}
|
||||
}
|
||||
if (timerId === undefined) {
|
||||
timerId = setTimeout(timerExpired, wait);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
debounced.cancel = cancel;
|
||||
debounced.flush = flush;
|
||||
return debounced;
|
||||
}
|
||||
|
||||
var Prop = /*#__PURE__*/_createClass(function Prop(name, _ref) {
|
||||
var _ref$default = _ref["default"],
|
||||
defaultVal = _ref$default === void 0 ? null : _ref$default,
|
||||
_ref$triggerUpdate = _ref.triggerUpdate,
|
||||
triggerUpdate = _ref$triggerUpdate === void 0 ? true : _ref$triggerUpdate,
|
||||
_ref$onChange = _ref.onChange,
|
||||
onChange = _ref$onChange === void 0 ? function (newVal, state) {} : _ref$onChange;
|
||||
_classCallCheck(this, Prop);
|
||||
this.name = name;
|
||||
this.defaultVal = defaultVal;
|
||||
this.triggerUpdate = triggerUpdate;
|
||||
this.onChange = onChange;
|
||||
});
|
||||
function index (_ref2) {
|
||||
var _ref2$stateInit = _ref2.stateInit,
|
||||
stateInit = _ref2$stateInit === void 0 ? function () {
|
||||
return {};
|
||||
} : _ref2$stateInit,
|
||||
_ref2$props = _ref2.props,
|
||||
rawProps = _ref2$props === void 0 ? {} : _ref2$props,
|
||||
_ref2$methods = _ref2.methods,
|
||||
methods = _ref2$methods === void 0 ? {} : _ref2$methods,
|
||||
_ref2$aliases = _ref2.aliases,
|
||||
aliases = _ref2$aliases === void 0 ? {} : _ref2$aliases,
|
||||
_ref2$init = _ref2.init,
|
||||
initFn = _ref2$init === void 0 ? function () {} : _ref2$init,
|
||||
_ref2$update = _ref2.update,
|
||||
updateFn = _ref2$update === void 0 ? function () {} : _ref2$update;
|
||||
// Parse props into Prop instances
|
||||
var props = Object.keys(rawProps).map(function (propName) {
|
||||
return new Prop(propName, rawProps[propName]);
|
||||
});
|
||||
return function () {
|
||||
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
||||
// Holds component state
|
||||
var state = Object.assign({}, stateInit instanceof Function ? stateInit(options) : stateInit,
|
||||
// Support plain objects for backwards compatibility
|
||||
{
|
||||
initialised: false
|
||||
});
|
||||
|
||||
// keeps track of which props triggered an update
|
||||
var changedProps = {};
|
||||
|
||||
// Component constructor
|
||||
function comp(nodeElement) {
|
||||
initStatic(nodeElement, options);
|
||||
digest();
|
||||
return comp;
|
||||
}
|
||||
var initStatic = function initStatic(nodeElement, options) {
|
||||
initFn.call(comp, nodeElement, state, options);
|
||||
state.initialised = true;
|
||||
};
|
||||
var digest = debounce(function () {
|
||||
if (!state.initialised) {
|
||||
return;
|
||||
}
|
||||
updateFn.call(comp, state, changedProps);
|
||||
changedProps = {};
|
||||
}, 1);
|
||||
|
||||
// Getter/setter methods
|
||||
props.forEach(function (prop) {
|
||||
comp[prop.name] = getSetProp(prop);
|
||||
function getSetProp(_ref3) {
|
||||
var prop = _ref3.name,
|
||||
_ref3$triggerUpdate = _ref3.triggerUpdate,
|
||||
redigest = _ref3$triggerUpdate === void 0 ? false : _ref3$triggerUpdate,
|
||||
_ref3$onChange = _ref3.onChange,
|
||||
onChange = _ref3$onChange === void 0 ? function (newVal, state) {} : _ref3$onChange,
|
||||
_ref3$defaultVal = _ref3.defaultVal,
|
||||
defaultVal = _ref3$defaultVal === void 0 ? null : _ref3$defaultVal;
|
||||
return function (_) {
|
||||
var curVal = state[prop];
|
||||
if (!arguments.length) {
|
||||
return curVal;
|
||||
} // Getter mode
|
||||
|
||||
var val = _ === undefined ? defaultVal : _; // pick default if value passed is undefined
|
||||
state[prop] = val;
|
||||
onChange.call(comp, val, state, curVal);
|
||||
|
||||
// track changed props
|
||||
!changedProps.hasOwnProperty(prop) && (changedProps[prop] = curVal);
|
||||
if (redigest) {
|
||||
digest();
|
||||
}
|
||||
return comp;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Other methods
|
||||
Object.keys(methods).forEach(function (methodName) {
|
||||
comp[methodName] = function () {
|
||||
var _methods$methodName;
|
||||
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
||||
args[_key] = arguments[_key];
|
||||
}
|
||||
return (_methods$methodName = methods[methodName]).call.apply(_methods$methodName, [comp, state].concat(args));
|
||||
};
|
||||
});
|
||||
|
||||
// Link aliases
|
||||
Object.entries(aliases).forEach(function (_ref4) {
|
||||
var _ref5 = _slicedToArray(_ref4, 2),
|
||||
alias = _ref5[0],
|
||||
target = _ref5[1];
|
||||
return comp[alias] = comp[target];
|
||||
});
|
||||
|
||||
// Reset all component props to their default value
|
||||
comp.resetProps = function () {
|
||||
props.forEach(function (prop) {
|
||||
comp[prop.name](prop.defaultVal);
|
||||
});
|
||||
return comp;
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
comp.resetProps(); // Apply all prop defaults
|
||||
state._rerender = digest; // Expose digest method
|
||||
|
||||
return comp;
|
||||
};
|
||||
}
|
||||
|
||||
return index;
|
||||
|
||||
}));
|
||||
//# sourceMappingURL=kapsule.js.map
|
||||
export default TableCsv
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
// output_climate_pruebas.js
|
||||
import ForceGraph3D from 'https://esm.sh/3d-force-graph?external=three';
|
||||
import * as THREE from 'three';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const elem = document.getElementById('climateContainer');
|
||||
const form = document.getElementById('paramForm');
|
||||
|
||||
let lastGraphData = { nodes: [], links: [] };
|
||||
let fullGraphData = null;
|
||||
let currentRelatedNodes = [];
|
||||
let currentRelatedIdx = 0;
|
||||
let currentNode = null;
|
||||
|
||||
const textureCache = {};
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||
.linkColor(() => '#42FF00')
|
||||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
// Imagen local (nodos-imagen) directa; og:image remota de noticias via
|
||||
// proxy same-origin /api/img para que WebGL pueda texturizarla (CORS).
|
||||
let src = null, esNoticia = false;
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
src = node.image_url;
|
||||
} else if (node.imagen_url) {
|
||||
src = '/api/img?u=' + encodeURIComponent(node.imagen_url);
|
||||
esNoticia = true;
|
||||
}
|
||||
if (!src) return null;
|
||||
const texture = getTexture(src);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
// miniaturas de noticia mas pequenas que los nodos-imagen dedicados
|
||||
if (esNoticia) sprite.scale.set(84, 55, 1);
|
||||
else sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3');
|
||||
|
||||
graph.d3Force('charge').strength(-150);
|
||||
graph.d3Force('link').distance(70).strength(0.3);
|
||||
graph.d3Force('center').strength(0.05);
|
||||
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||
}, 500);
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
graph.width(elem.clientWidth);
|
||||
graph.height(elem.clientHeight);
|
||||
centerGraph();
|
||||
});
|
||||
|
||||
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||
function formatDate(fecha) {
|
||||
if (!fecha) return '';
|
||||
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||
}
|
||||
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||
|
||||
function getRelated(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
return lastGraphData.nodes.filter(n => relIds.has(n.id));
|
||||
}
|
||||
|
||||
function renderDetailPanel(node, related, idx) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (!detailContent || !split) return;
|
||||
|
||||
detailContent.querySelector('.detail-author').textContent = getAuthor(node);
|
||||
detailContent.querySelector('.detail-date').textContent = formatDate(node.fecha || node.date);
|
||||
detailContent.querySelector('.detail-title').textContent = formatTitle(node.id);
|
||||
|
||||
const relCount = detailContent.querySelector('.relations-count');
|
||||
const relLabel = document.getElementById('relationLabel');
|
||||
const prevBtn = document.getElementById('prevRelation');
|
||||
const nextBtn = document.getElementById('nextRelation');
|
||||
relCount.textContent = `${related.length} relación${related.length !== 1 ? 'es' : ''}`;
|
||||
if (related.length > 0) {
|
||||
relLabel.textContent = formatTitle(related[idx].id);
|
||||
prevBtn.disabled = idx <= 0;
|
||||
nextBtn.disabled = idx >= related.length - 1;
|
||||
} else {
|
||||
relLabel.textContent = '—';
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
}
|
||||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
const _imgSrc = (node.type === 'imagen' && node.image_url) ? node.image_url
|
||||
: (node.imagen_url || null);
|
||||
if (_imgSrc) {
|
||||
const img = document.createElement('img');
|
||||
img.src = _imgSrc;
|
||||
img.className = 'detail-img';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => img.remove();
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
text.textContent = node.content || node.texto || node.descripcion || 'Sin contenido disponible.';
|
||||
body.appendChild(text);
|
||||
|
||||
if (related.length > 0) {
|
||||
const conexBtn = document.createElement('button');
|
||||
conexBtn.id = 'verConexionesBtn';
|
||||
conexBtn.textContent = '⬡ Ver solo conexiones de este nodo';
|
||||
conexBtn.addEventListener('click', () => showEgoGraph(node));
|
||||
body.appendChild(conexBtn);
|
||||
}
|
||||
|
||||
split.classList.add('show-detail');
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
}
|
||||
|
||||
function showEgoGraph(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const egoNodes = lastGraphData.nodes.filter(n => n.id === nid || relIds.has(n.id));
|
||||
const egoIds = new Set(egoNodes.map(n => n.id));
|
||||
const egoLinks = lastGraphData.links.filter(lk => {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
return egoIds.has(s) && egoIds.has(t);
|
||||
});
|
||||
fullGraphData = lastGraphData;
|
||||
graph.graphData({ nodes: egoNodes, links: egoLinks });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
function restoreFullGraph() {
|
||||
if (fullGraphData) {
|
||||
lastGraphData = fullGraphData;
|
||||
fullGraphData = null;
|
||||
graph.graphData({ nodes: lastGraphData.nodes, links: lastGraphData.links });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeDetail(node) {
|
||||
currentNode = node;
|
||||
currentRelatedNodes = getRelated(node);
|
||||
currentRelatedIdx = 0;
|
||||
renderDetailPanel(node, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
|
||||
document.getElementById('prevRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx > 0) {
|
||||
currentRelatedIdx--;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('nextRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx < currentRelatedNodes.length - 1) {
|
||||
currentRelatedIdx++;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('closeDetail').addEventListener('click', () => {
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.remove('show-detail');
|
||||
restoreFullGraph();
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
});
|
||||
|
||||
document.getElementById('volverGrafoBtn').addEventListener('click', () => {
|
||||
restoreFullGraph();
|
||||
});
|
||||
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
params.append('tema', 'cambio climático');
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
url += `?${params.toString()}`;
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
const data = await response.json();
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndRender() {
|
||||
const paramsObj = {
|
||||
subtematica: document.getElementById('param2').value,
|
||||
palabraClave: document.getElementById('param1').value,
|
||||
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||
fechaFin: document.getElementById('fecha_fin').value,
|
||||
nodos: document.getElementById('nodos').value,
|
||||
complejidad: document.getElementById('complejidad').value,
|
||||
};
|
||||
const graphData = await getData(paramsObj);
|
||||
if (!graphData) return;
|
||||
lastGraphData = graphData;
|
||||
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||
centerGraph();
|
||||
}
|
||||
|
||||
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||
fetchAndRender();
|
||||
});
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
// output_eco_corp.js
|
||||
import ForceGraph3D from 'https://esm.sh/3d-force-graph?external=three';
|
||||
import * as THREE from 'three';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const elem = document.getElementById('ecoCorpContainer');
|
||||
const form = document.getElementById('paramForm');
|
||||
|
||||
let lastGraphData = { nodes: [], links: [] };
|
||||
let fullGraphData = null;
|
||||
let currentRelatedNodes = [];
|
||||
let currentRelatedIdx = 0;
|
||||
let currentNode = null;
|
||||
|
||||
const textureCache = {};
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||
.linkColor(() => 'yellow')
|
||||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
// Imagen local (nodos-imagen) directa; og:image remota de noticias via
|
||||
// proxy same-origin /api/img para que WebGL pueda texturizarla (CORS).
|
||||
let src = null, esNoticia = false;
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
src = node.image_url;
|
||||
} else if (node.imagen_url) {
|
||||
src = '/api/img?u=' + encodeURIComponent(node.imagen_url);
|
||||
esNoticia = true;
|
||||
}
|
||||
if (!src) return null;
|
||||
const texture = getTexture(src);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
// miniaturas de noticia mas pequenas que los nodos-imagen dedicados
|
||||
if (esNoticia) sprite.scale.set(84, 55, 1);
|
||||
else sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3');
|
||||
|
||||
graph.d3Force('charge').strength(-150);
|
||||
graph.d3Force('link').distance(70).strength(0.3);
|
||||
graph.d3Force('center').strength(0.05);
|
||||
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||
}, 500);
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
graph.width(elem.clientWidth);
|
||||
graph.height(elem.clientHeight);
|
||||
centerGraph();
|
||||
});
|
||||
|
||||
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||
function formatDate(fecha) {
|
||||
if (!fecha) return '';
|
||||
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||
}
|
||||
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||
|
||||
function getRelated(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
return lastGraphData.nodes.filter(n => relIds.has(n.id));
|
||||
}
|
||||
|
||||
function renderDetailPanel(node, related, idx) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (!detailContent || !split) return;
|
||||
|
||||
detailContent.querySelector('.detail-author').textContent = getAuthor(node);
|
||||
detailContent.querySelector('.detail-date').textContent = formatDate(node.fecha || node.date);
|
||||
detailContent.querySelector('.detail-title').textContent = formatTitle(node.id);
|
||||
|
||||
const relCount = detailContent.querySelector('.relations-count');
|
||||
const relLabel = document.getElementById('relationLabel');
|
||||
const prevBtn = document.getElementById('prevRelation');
|
||||
const nextBtn = document.getElementById('nextRelation');
|
||||
relCount.textContent = `${related.length} relación${related.length !== 1 ? 'es' : ''}`;
|
||||
if (related.length > 0) {
|
||||
relLabel.textContent = formatTitle(related[idx].id);
|
||||
prevBtn.disabled = idx <= 0;
|
||||
nextBtn.disabled = idx >= related.length - 1;
|
||||
} else {
|
||||
relLabel.textContent = '—';
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
}
|
||||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
const _imgSrc = (node.type === 'imagen' && node.image_url) ? node.image_url
|
||||
: (node.imagen_url || null);
|
||||
if (_imgSrc) {
|
||||
const img = document.createElement('img');
|
||||
img.src = _imgSrc;
|
||||
img.className = 'detail-img';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => img.remove();
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
text.textContent = node.content || node.texto || node.descripcion || 'Sin contenido disponible.';
|
||||
body.appendChild(text);
|
||||
|
||||
if (related.length > 0) {
|
||||
const conexBtn = document.createElement('button');
|
||||
conexBtn.id = 'verConexionesBtn';
|
||||
conexBtn.textContent = '⬡ Ver solo conexiones de este nodo';
|
||||
conexBtn.addEventListener('click', () => showEgoGraph(node));
|
||||
body.appendChild(conexBtn);
|
||||
}
|
||||
|
||||
split.classList.add('show-detail');
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
}
|
||||
|
||||
function showEgoGraph(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const egoNodes = lastGraphData.nodes.filter(n => n.id === nid || relIds.has(n.id));
|
||||
const egoIds = new Set(egoNodes.map(n => n.id));
|
||||
const egoLinks = lastGraphData.links.filter(lk => {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
return egoIds.has(s) && egoIds.has(t);
|
||||
});
|
||||
fullGraphData = lastGraphData;
|
||||
graph.graphData({ nodes: egoNodes, links: egoLinks });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
function restoreFullGraph() {
|
||||
if (fullGraphData) {
|
||||
lastGraphData = fullGraphData;
|
||||
fullGraphData = null;
|
||||
graph.graphData({ nodes: lastGraphData.nodes, links: lastGraphData.links });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeDetail(node) {
|
||||
currentNode = node;
|
||||
currentRelatedNodes = getRelated(node);
|
||||
currentRelatedIdx = 0;
|
||||
renderDetailPanel(node, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
|
||||
document.getElementById('prevRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx > 0) {
|
||||
currentRelatedIdx--;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('nextRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx < currentRelatedNodes.length - 1) {
|
||||
currentRelatedIdx++;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('closeDetail').addEventListener('click', () => {
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.remove('show-detail');
|
||||
restoreFullGraph();
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
});
|
||||
|
||||
document.getElementById('volverGrafoBtn').addEventListener('click', () => {
|
||||
restoreFullGraph();
|
||||
});
|
||||
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
params.append('tema', 'economía y corporaciones');
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
url += `?${params.toString()}`;
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
const data = await response.json();
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndRender() {
|
||||
const paramsObj = {
|
||||
subtematica: document.getElementById('param2').value,
|
||||
palabraClave: document.getElementById('param1').value,
|
||||
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||
fechaFin: document.getElementById('fecha_fin').value,
|
||||
nodos: document.getElementById('nodos').value,
|
||||
complejidad: document.getElementById('complejidad').value,
|
||||
};
|
||||
const graphData = await getData(paramsObj);
|
||||
if (!graphData) return;
|
||||
lastGraphData = graphData;
|
||||
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||
centerGraph();
|
||||
}
|
||||
|
||||
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||
fetchAndRender();
|
||||
});
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
// output_glob_war.js
|
||||
|
||||
// Obtener el contenedor del gráfico
|
||||
const elem = document.getElementById('globWarContainer');
|
||||
|
||||
// Cache de texturas para no recargar la misma imagen
|
||||
const textureCache = {};
|
||||
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) {
|
||||
textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
}
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
// Inicializar el gráfico
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(node => node.type === 'imagen' ? 0 : 2)
|
||||
.linkColor(() => 'rgba(0,255,80,0.4)')
|
||||
.onNodeClick(node => showNodeContent(node.content))
|
||||
.onNodeHover(node => {
|
||||
elem.style.cursor = node ? 'pointer' : null;
|
||||
})
|
||||
.nodeThreeObject(node => {
|
||||
if (node.type !== 'imagen' || !node.image_url) return null;
|
||||
|
||||
const texture = getTexture(node.image_url);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(14, 9, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3')
|
||||
.d3Force('charge', d3.forceManyBody().strength(-10))
|
||||
.d3Force('link', d3.forceLink().distance(30).strength(1));
|
||||
|
||||
// Función para obtener datos del servidor
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Tema por defecto: "guerra global"
|
||||
params.append('tema', 'guerra global');
|
||||
|
||||
// Agregar parámetros del formulario si existen
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key]) {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
url += `?${params.toString()}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
console.log('Datos recibidos del servidor:', data);
|
||||
|
||||
// Filtrar enlaces para nodos existentes
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(l => nodeIds.has(l.source) && nodeIds.has(l.target));
|
||||
|
||||
console.log('Enlaces tras filtrar:', data.links);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos del servidor:', error);
|
||||
return { nodes: [], links: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// Mostrar contenido del nodo al hacer click
|
||||
function showNodeContent(content) {
|
||||
console.log('Contenido del nodo:', content);
|
||||
// aquí podrías mostrarlo en un panel si quieres
|
||||
}
|
||||
|
||||
// Centrar y escalar el gráfico
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
const width = elem.clientWidth;
|
||||
const height = elem.clientHeight;
|
||||
const padding = Math.min(width, height) * 0.1;
|
||||
graph.zoomToFit(400, padding);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Al cambiar el tamaño de la ventana, reajustar
|
||||
window.addEventListener('resize', () => {
|
||||
graph.width(elem.clientWidth);
|
||||
graph.height(elem.clientHeight);
|
||||
centerGraph();
|
||||
});
|
||||
|
||||
// Gestionar el submit del formulario de parámetros
|
||||
document.getElementById('paramForm').addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
|
||||
const subtematica = document.getElementById('param2').value;
|
||||
const palabraClave = document.getElementById('param1').value;
|
||||
const fechaInicio = document.getElementById('fecha_inicio').value;
|
||||
const fechaFin = document.getElementById('fecha_fin').value;
|
||||
const nodos = document.getElementById('nodos').value;
|
||||
const complejidad = document.getElementById('complejidad').value;
|
||||
|
||||
const paramsObj = {
|
||||
subtematica,
|
||||
palabraClave,
|
||||
fechaInicio,
|
||||
fechaFin,
|
||||
nodos,
|
||||
complejidad
|
||||
};
|
||||
|
||||
getData(paramsObj).then(data => {
|
||||
graph.graphData(data);
|
||||
centerGraph();
|
||||
});
|
||||
});
|
||||
|
||||
// Cargar datos iniciales (guerra global) y renderizar
|
||||
getData().then(data => {
|
||||
if (data.nodes && data.nodes.length) {
|
||||
console.log(`Se recibieron ${data.nodes.length} nodos.`);
|
||||
graph.graphData(data);
|
||||
centerGraph();
|
||||
} else {
|
||||
console.warn('No se recibieron nodos para "guerra global".');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
// output_glob_war.js
|
||||
import ForceGraph3D from 'https://esm.sh/3d-force-graph?external=three';
|
||||
import * as THREE from 'three';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const elem = document.getElementById('globWarContainer');
|
||||
const form = document.getElementById('paramForm');
|
||||
|
||||
let lastGraphData = { nodes: [], links: [] };
|
||||
let fullGraphData = null;
|
||||
let currentRelatedNodes = [];
|
||||
let currentRelatedIdx = 0;
|
||||
let currentNode = null;
|
||||
|
||||
const textureCache = {};
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||
.linkColor(() => 'red')
|
||||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
// Imagen local (nodos-imagen) directa; og:image remota de noticias via
|
||||
// proxy same-origin /api/img para que WebGL pueda texturizarla (CORS).
|
||||
let src = null, esNoticia = false;
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
src = node.image_url;
|
||||
} else if (node.imagen_url) {
|
||||
src = '/api/img?u=' + encodeURIComponent(node.imagen_url);
|
||||
esNoticia = true;
|
||||
}
|
||||
if (!src) return null;
|
||||
const texture = getTexture(src);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
// miniaturas de noticia mas pequenas que los nodos-imagen dedicados
|
||||
if (esNoticia) sprite.scale.set(84, 55, 1);
|
||||
else sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3');
|
||||
|
||||
graph.d3Force('charge').strength(-150);
|
||||
graph.d3Force('link').distance(70).strength(0.3);
|
||||
graph.d3Force('center').strength(0.05);
|
||||
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||
}, 500);
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
graph.width(elem.clientWidth);
|
||||
graph.height(elem.clientHeight);
|
||||
centerGraph();
|
||||
});
|
||||
|
||||
// --- Detail panel helpers ---
|
||||
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||
function formatDate(fecha) {
|
||||
if (!fecha) return '';
|
||||
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||
}
|
||||
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||
|
||||
function getRelated(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
return lastGraphData.nodes.filter(n => relIds.has(n.id));
|
||||
}
|
||||
|
||||
function renderDetailPanel(node, related, idx) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (!detailContent || !split) return;
|
||||
|
||||
detailContent.querySelector('.detail-author').textContent = getAuthor(node);
|
||||
detailContent.querySelector('.detail-date').textContent = formatDate(node.fecha || node.date);
|
||||
detailContent.querySelector('.detail-title').textContent = formatTitle(node.id);
|
||||
|
||||
const relCount = detailContent.querySelector('.relations-count');
|
||||
const relLabel = document.getElementById('relationLabel');
|
||||
const prevBtn = document.getElementById('prevRelation');
|
||||
const nextBtn = document.getElementById('nextRelation');
|
||||
relCount.textContent = `${related.length} relación${related.length !== 1 ? 'es' : ''}`;
|
||||
if (related.length > 0) {
|
||||
relLabel.textContent = formatTitle(related[idx].id);
|
||||
prevBtn.disabled = idx <= 0;
|
||||
nextBtn.disabled = idx >= related.length - 1;
|
||||
} else {
|
||||
relLabel.textContent = '—';
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
}
|
||||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
const _imgSrc = (node.type === 'imagen' && node.image_url) ? node.image_url
|
||||
: (node.imagen_url || null);
|
||||
if (_imgSrc) {
|
||||
const img = document.createElement('img');
|
||||
img.src = _imgSrc;
|
||||
img.className = 'detail-img';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => img.remove();
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
text.textContent = node.content || node.texto || node.descripcion || 'Sin contenido disponible.';
|
||||
body.appendChild(text);
|
||||
|
||||
if (related.length > 0) {
|
||||
const conexBtn = document.createElement('button');
|
||||
conexBtn.id = 'verConexionesBtn';
|
||||
conexBtn.textContent = '⬡ Ver solo conexiones de este nodo';
|
||||
conexBtn.addEventListener('click', () => showEgoGraph(node));
|
||||
body.appendChild(conexBtn);
|
||||
}
|
||||
|
||||
split.classList.add('show-detail');
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
}
|
||||
|
||||
function showEgoGraph(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const egoNodes = lastGraphData.nodes.filter(n => n.id === nid || relIds.has(n.id));
|
||||
const egoIds = new Set(egoNodes.map(n => n.id));
|
||||
const egoLinks = lastGraphData.links.filter(lk => {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
return egoIds.has(s) && egoIds.has(t);
|
||||
});
|
||||
fullGraphData = lastGraphData;
|
||||
graph.graphData({ nodes: egoNodes, links: egoLinks });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
function restoreFullGraph() {
|
||||
if (fullGraphData) {
|
||||
lastGraphData = fullGraphData;
|
||||
fullGraphData = null;
|
||||
graph.graphData({ nodes: lastGraphData.nodes, links: lastGraphData.links });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeDetail(node) {
|
||||
currentNode = node;
|
||||
currentRelatedNodes = getRelated(node);
|
||||
currentRelatedIdx = 0;
|
||||
renderDetailPanel(node, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
|
||||
document.getElementById('prevRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx > 0) {
|
||||
currentRelatedIdx--;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('nextRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx < currentRelatedNodes.length - 1) {
|
||||
currentRelatedIdx++;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('closeDetail').addEventListener('click', () => {
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.remove('show-detail');
|
||||
restoreFullGraph();
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
});
|
||||
|
||||
document.getElementById('volverGrafoBtn').addEventListener('click', () => {
|
||||
restoreFullGraph();
|
||||
});
|
||||
|
||||
// --- Data fetch ---
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
params.append('tema', 'guerra global');
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
url += `?${params.toString()}`;
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
const data = await response.json();
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndRender() {
|
||||
const paramsObj = {
|
||||
subtematica: document.getElementById('param2').value,
|
||||
palabraClave: document.getElementById('param1').value,
|
||||
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||
fechaFin: document.getElementById('fecha_fin').value,
|
||||
nodos: document.getElementById('nodos').value,
|
||||
complejidad: document.getElementById('complejidad').value,
|
||||
};
|
||||
const graphData = await getData(paramsObj);
|
||||
if (!graphData) return;
|
||||
lastGraphData = graphData;
|
||||
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||
centerGraph();
|
||||
}
|
||||
|
||||
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||
fetchAndRender();
|
||||
});
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
// output_int_sec.js
|
||||
import ForceGraph3D from 'https://esm.sh/3d-force-graph?external=three';
|
||||
import * as THREE from 'three';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const elem = document.getElementById('intSecContainer');
|
||||
const form = document.getElementById('paramForm');
|
||||
|
||||
let lastGraphData = { nodes: [], links: [] };
|
||||
let fullGraphData = null;
|
||||
let currentRelatedNodes = [];
|
||||
let currentRelatedIdx = 0;
|
||||
let currentNode = null;
|
||||
|
||||
const textureCache = {};
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||
.linkColor(() => 'blue')
|
||||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
// Imagen local (nodos-imagen) directa; og:image remota de noticias via
|
||||
// proxy same-origin /api/img para que WebGL pueda texturizarla (CORS).
|
||||
let src = null, esNoticia = false;
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
src = node.image_url;
|
||||
} else if (node.imagen_url) {
|
||||
src = '/api/img?u=' + encodeURIComponent(node.imagen_url);
|
||||
esNoticia = true;
|
||||
}
|
||||
if (!src) return null;
|
||||
const texture = getTexture(src);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
// miniaturas de noticia mas pequenas que los nodos-imagen dedicados
|
||||
if (esNoticia) sprite.scale.set(84, 55, 1);
|
||||
else sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3');
|
||||
|
||||
graph.d3Force('charge').strength(-150);
|
||||
graph.d3Force('link').distance(70).strength(0.3);
|
||||
graph.d3Force('center').strength(0.05);
|
||||
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||
}, 500);
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
graph.width(elem.clientWidth);
|
||||
graph.height(elem.clientHeight);
|
||||
centerGraph();
|
||||
});
|
||||
|
||||
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||
function formatDate(fecha) {
|
||||
if (!fecha) return '';
|
||||
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||
}
|
||||
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||
|
||||
function getRelated(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
return lastGraphData.nodes.filter(n => relIds.has(n.id));
|
||||
}
|
||||
|
||||
function renderDetailPanel(node, related, idx) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (!detailContent || !split) return;
|
||||
|
||||
detailContent.querySelector('.detail-author').textContent = getAuthor(node);
|
||||
detailContent.querySelector('.detail-date').textContent = formatDate(node.fecha || node.date);
|
||||
detailContent.querySelector('.detail-title').textContent = formatTitle(node.id);
|
||||
|
||||
const relCount = detailContent.querySelector('.relations-count');
|
||||
const relLabel = document.getElementById('relationLabel');
|
||||
const prevBtn = document.getElementById('prevRelation');
|
||||
const nextBtn = document.getElementById('nextRelation');
|
||||
relCount.textContent = `${related.length} relación${related.length !== 1 ? 'es' : ''}`;
|
||||
if (related.length > 0) {
|
||||
relLabel.textContent = formatTitle(related[idx].id);
|
||||
prevBtn.disabled = idx <= 0;
|
||||
nextBtn.disabled = idx >= related.length - 1;
|
||||
} else {
|
||||
relLabel.textContent = '—';
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
}
|
||||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
const _imgSrc = (node.type === 'imagen' && node.image_url) ? node.image_url
|
||||
: (node.imagen_url || null);
|
||||
if (_imgSrc) {
|
||||
const img = document.createElement('img');
|
||||
img.src = _imgSrc;
|
||||
img.className = 'detail-img';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => img.remove();
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
text.textContent = node.content || node.texto || node.descripcion || 'Sin contenido disponible.';
|
||||
body.appendChild(text);
|
||||
|
||||
if (related.length > 0) {
|
||||
const conexBtn = document.createElement('button');
|
||||
conexBtn.id = 'verConexionesBtn';
|
||||
conexBtn.textContent = '⬡ Ver solo conexiones de este nodo';
|
||||
conexBtn.addEventListener('click', () => showEgoGraph(node));
|
||||
body.appendChild(conexBtn);
|
||||
}
|
||||
|
||||
split.classList.add('show-detail');
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
}
|
||||
|
||||
function showEgoGraph(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const egoNodes = lastGraphData.nodes.filter(n => n.id === nid || relIds.has(n.id));
|
||||
const egoIds = new Set(egoNodes.map(n => n.id));
|
||||
const egoLinks = lastGraphData.links.filter(lk => {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
return egoIds.has(s) && egoIds.has(t);
|
||||
});
|
||||
fullGraphData = lastGraphData;
|
||||
graph.graphData({ nodes: egoNodes, links: egoLinks });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
function restoreFullGraph() {
|
||||
if (fullGraphData) {
|
||||
lastGraphData = fullGraphData;
|
||||
fullGraphData = null;
|
||||
graph.graphData({ nodes: lastGraphData.nodes, links: lastGraphData.links });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeDetail(node) {
|
||||
currentNode = node;
|
||||
currentRelatedNodes = getRelated(node);
|
||||
currentRelatedIdx = 0;
|
||||
renderDetailPanel(node, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
|
||||
document.getElementById('prevRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx > 0) {
|
||||
currentRelatedIdx--;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('nextRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx < currentRelatedNodes.length - 1) {
|
||||
currentRelatedIdx++;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('closeDetail').addEventListener('click', () => {
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.remove('show-detail');
|
||||
restoreFullGraph();
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
});
|
||||
|
||||
document.getElementById('volverGrafoBtn').addEventListener('click', () => {
|
||||
restoreFullGraph();
|
||||
});
|
||||
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
params.append('tema', 'inteligencia y seguridad');
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
url += `?${params.toString()}`;
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
const data = await response.json();
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndRender() {
|
||||
const paramsObj = {
|
||||
subtematica: document.getElementById('param2').value,
|
||||
palabraClave: document.getElementById('param1').value,
|
||||
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||
fechaFin: document.getElementById('fecha_fin').value,
|
||||
nodos: document.getElementById('nodos').value,
|
||||
complejidad: document.getElementById('complejidad').value,
|
||||
};
|
||||
const graphData = await getData(paramsObj);
|
||||
if (!graphData) return;
|
||||
lastGraphData = graphData;
|
||||
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||
centerGraph();
|
||||
}
|
||||
|
||||
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||
fetchAndRender();
|
||||
});
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
// output_popl_up.js
|
||||
|
||||
// Obtener el contenedor del gráfico
|
||||
const elem = document.getElementById('poplUpContainer');
|
||||
|
||||
// Inicializar el gráfico
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel('id')
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(5)
|
||||
.linkColor(() => 'green')
|
||||
.onNodeClick(node => showNodeContent(node.content))
|
||||
.onNodeHover(node => {
|
||||
elem.style.cursor = node ? 'pointer' : null;
|
||||
})
|
||||
.forceEngine('d3')
|
||||
.d3Force('charge', d3.forceManyBody().strength(-10))
|
||||
.d3Force('link', d3.forceLink().distance(30).strength(1));
|
||||
|
||||
// Función para obtener datos del servidor
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Fijar el tema principal
|
||||
params.append('tema', 'movimientos populares y levantamientos');
|
||||
|
||||
// Agregar otros parámetros si se han definido
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key]) {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
url += `?${params.toString()}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📦 Datos recibidos:', data);
|
||||
|
||||
// Validar y limpiar links
|
||||
const nodeIds = new Set(data.nodes.map(node => node.id));
|
||||
data.links = data.links.filter(link => {
|
||||
const valid = nodeIds.has(link.source) && nodeIds.has(link.target);
|
||||
if (!valid) {
|
||||
console.log(`❌ Enlace inválido eliminado: ${link.source} -> ${link.target}`);
|
||||
}
|
||||
return valid;
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('⚠️ Error al obtener datos:', error);
|
||||
return { nodes: [], links: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// Mostrar contenido del nodo en consola (puedes ampliar esta función)
|
||||
function showNodeContent(content) {
|
||||
console.log('🧠 Contenido del nodo:', content);
|
||||
}
|
||||
|
||||
// Centrar el gráfico tras dibujarlo
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
const width = elem.clientWidth;
|
||||
const height = elem.clientHeight;
|
||||
const padding = Math.min(width, height) * 0.1;
|
||||
graph.zoomToFit(400, padding);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Redibujar al cambiar tamaño ventana
|
||||
window.addEventListener('resize', () => {
|
||||
graph.width(elem.clientWidth);
|
||||
graph.height(elem.clientHeight);
|
||||
centerGraph();
|
||||
});
|
||||
|
||||
// Manejar el formulario
|
||||
document.getElementById('paramForm').addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const subtematica = document.getElementById('param2').value;
|
||||
const palabraClave = document.getElementById('param1').value;
|
||||
const fechaInicio = document.getElementById('fecha_inicio').value;
|
||||
const fechaFin = document.getElementById('fecha_fin').value;
|
||||
const nodos = document.getElementById('nodos').value;
|
||||
const complejidad = document.getElementById('complejidad').value;
|
||||
|
||||
const paramsObj = {
|
||||
subtematica,
|
||||
palabraClave,
|
||||
fechaInicio,
|
||||
fechaFin,
|
||||
nodos,
|
||||
complejidad,
|
||||
};
|
||||
|
||||
getData(paramsObj).then(graphData => {
|
||||
if (graphData) {
|
||||
console.log("✅ Redibujando con nuevos datos...");
|
||||
graph.graphData(graphData);
|
||||
centerGraph();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Obtener datos iniciales
|
||||
getData().then(graphData => {
|
||||
if (graphData && graphData.nodes && graphData.nodes.length > 0) {
|
||||
console.log(`✅ Se recibieron ${graphData.nodes.length} nodos.`);
|
||||
graph.graphData(graphData);
|
||||
centerGraph();
|
||||
} else {
|
||||
console.warn("⚠️ No se recibieron nodos para mostrar.");
|
||||
console.log("🔍 Respuesta completa:", graphData);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
// output_popl_up_pruebas.js
|
||||
import ForceGraph3D from 'https://esm.sh/3d-force-graph?external=three';
|
||||
import * as THREE from 'three';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const elem = document.getElementById('poplUpContainer');
|
||||
const form = document.getElementById('paramForm');
|
||||
|
||||
let lastGraphData = { nodes: [], links: [] };
|
||||
let fullGraphData = null;
|
||||
let currentRelatedNodes = [];
|
||||
let currentRelatedIdx = 0;
|
||||
let currentNode = null;
|
||||
|
||||
const textureCache = {};
|
||||
function getTexture(url) {
|
||||
if (!textureCache[url]) textureCache[url] = new THREE.TextureLoader().load(url);
|
||||
return textureCache[url];
|
||||
}
|
||||
|
||||
const graph = ForceGraph3D()(elem)
|
||||
.backgroundColor('#000000')
|
||||
.nodeLabel(node => node.type === 'imagen' ? (node.label || node.id) : node.id)
|
||||
.nodeAutoColorBy('group')
|
||||
.nodeVal(node => node.type === 'imagen' ? 20 : 2)
|
||||
.linkColor(() => 'orange')
|
||||
.onNodeClick(node => showNodeDetail(node))
|
||||
.onNodeHover(node => { elem.style.cursor = node ? 'pointer' : null; })
|
||||
.nodeThreeObject(node => {
|
||||
// Imagen local (nodos-imagen) directa; og:image remota de noticias via
|
||||
// proxy same-origin /api/img para que WebGL pueda texturizarla (CORS).
|
||||
let src = null, esNoticia = false;
|
||||
if (node.type === 'imagen' && node.image_url) {
|
||||
src = node.image_url;
|
||||
} else if (node.imagen_url) {
|
||||
src = '/api/img?u=' + encodeURIComponent(node.imagen_url);
|
||||
esNoticia = true;
|
||||
}
|
||||
if (!src) return null;
|
||||
const texture = getTexture(src);
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: texture, depthWrite: false });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
// miniaturas de noticia mas pequenas que los nodos-imagen dedicados
|
||||
if (esNoticia) sprite.scale.set(84, 55, 1);
|
||||
else sprite.scale.set(160, 104, 1);
|
||||
return sprite;
|
||||
})
|
||||
.nodeThreeObjectExtend(false)
|
||||
.forceEngine('d3');
|
||||
|
||||
graph.d3Force('charge').strength(-150);
|
||||
graph.d3Force('link').distance(70).strength(0.3);
|
||||
graph.d3Force('center').strength(0.05);
|
||||
|
||||
function centerGraph() {
|
||||
setTimeout(() => {
|
||||
graph.zoomToFit(400, Math.min(elem.clientWidth, elem.clientHeight) * 0.1);
|
||||
}, 500);
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
graph.width(elem.clientWidth);
|
||||
graph.height(elem.clientHeight);
|
||||
centerGraph();
|
||||
});
|
||||
|
||||
function getAuthor(node) { return node.autor || node.fuente || node.source || ''; }
|
||||
function formatDate(fecha) {
|
||||
if (!fecha) return '';
|
||||
try { return new Date(fecha).toLocaleDateString('es-ES'); } catch { return String(fecha); }
|
||||
}
|
||||
function formatTitle(id) { return String(id).replace(/_/g, ' '); }
|
||||
|
||||
function getRelated(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
return lastGraphData.nodes.filter(n => relIds.has(n.id));
|
||||
}
|
||||
|
||||
function renderDetailPanel(node, related, idx) {
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (!detailContent || !split) return;
|
||||
|
||||
detailContent.querySelector('.detail-author').textContent = getAuthor(node);
|
||||
detailContent.querySelector('.detail-date').textContent = formatDate(node.fecha || node.date);
|
||||
detailContent.querySelector('.detail-title').textContent = formatTitle(node.id);
|
||||
|
||||
const relCount = detailContent.querySelector('.relations-count');
|
||||
const relLabel = document.getElementById('relationLabel');
|
||||
const prevBtn = document.getElementById('prevRelation');
|
||||
const nextBtn = document.getElementById('nextRelation');
|
||||
relCount.textContent = `${related.length} relación${related.length !== 1 ? 'es' : ''}`;
|
||||
if (related.length > 0) {
|
||||
relLabel.textContent = formatTitle(related[idx].id);
|
||||
prevBtn.disabled = idx <= 0;
|
||||
nextBtn.disabled = idx >= related.length - 1;
|
||||
} else {
|
||||
relLabel.textContent = '—';
|
||||
prevBtn.disabled = true;
|
||||
nextBtn.disabled = true;
|
||||
}
|
||||
|
||||
const body = detailContent.querySelector('.detail-body');
|
||||
body.innerHTML = '';
|
||||
const _imgSrc = (node.type === 'imagen' && node.image_url) ? node.image_url
|
||||
: (node.imagen_url || null);
|
||||
if (_imgSrc) {
|
||||
const img = document.createElement('img');
|
||||
img.src = _imgSrc;
|
||||
img.className = 'detail-img';
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => img.remove();
|
||||
body.appendChild(img);
|
||||
}
|
||||
const text = document.createElement('span');
|
||||
text.textContent = node.content || node.texto || node.descripcion || 'Sin contenido disponible.';
|
||||
body.appendChild(text);
|
||||
|
||||
if (related.length > 0) {
|
||||
const conexBtn = document.createElement('button');
|
||||
conexBtn.id = 'verConexionesBtn';
|
||||
conexBtn.textContent = '⬡ Ver solo conexiones de este nodo';
|
||||
conexBtn.addEventListener('click', () => showEgoGraph(node));
|
||||
body.appendChild(conexBtn);
|
||||
}
|
||||
|
||||
split.classList.add('show-detail');
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
}
|
||||
|
||||
function showEgoGraph(node) {
|
||||
const nid = node.id;
|
||||
const relIds = new Set();
|
||||
for (const lk of lastGraphData.links) {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
if (s === nid) relIds.add(t);
|
||||
else if (t === nid) relIds.add(s);
|
||||
}
|
||||
const egoNodes = lastGraphData.nodes.filter(n => n.id === nid || relIds.has(n.id));
|
||||
const egoIds = new Set(egoNodes.map(n => n.id));
|
||||
const egoLinks = lastGraphData.links.filter(lk => {
|
||||
const s = typeof lk.source === 'object' ? lk.source.id : lk.source;
|
||||
const t = typeof lk.target === 'object' ? lk.target.id : lk.target;
|
||||
return egoIds.has(s) && egoIds.has(t);
|
||||
});
|
||||
fullGraphData = lastGraphData;
|
||||
graph.graphData({ nodes: egoNodes, links: egoLinks });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
function restoreFullGraph() {
|
||||
if (fullGraphData) {
|
||||
lastGraphData = fullGraphData;
|
||||
fullGraphData = null;
|
||||
graph.graphData({ nodes: lastGraphData.nodes, links: lastGraphData.links });
|
||||
centerGraph();
|
||||
document.getElementById('volverGrafoBtn').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeDetail(node) {
|
||||
currentNode = node;
|
||||
currentRelatedNodes = getRelated(node);
|
||||
currentRelatedIdx = 0;
|
||||
renderDetailPanel(node, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
|
||||
document.getElementById('prevRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx > 0) {
|
||||
currentRelatedIdx--;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('nextRelation').addEventListener('click', () => {
|
||||
if (currentRelatedIdx < currentRelatedNodes.length - 1) {
|
||||
currentRelatedIdx++;
|
||||
renderDetailPanel(currentNode, currentRelatedNodes, currentRelatedIdx);
|
||||
}
|
||||
});
|
||||
document.getElementById('closeDetail').addEventListener('click', () => {
|
||||
const split = document.querySelector('main.split-screen');
|
||||
if (split) split.classList.remove('show-detail');
|
||||
restoreFullGraph();
|
||||
setTimeout(() => { graph.width(elem.clientWidth); graph.height(elem.clientHeight); }, 350);
|
||||
});
|
||||
|
||||
document.getElementById('volverGrafoBtn').addEventListener('click', () => {
|
||||
restoreFullGraph();
|
||||
});
|
||||
|
||||
async function getData(paramsObj = {}) {
|
||||
try {
|
||||
let url = '/api/data';
|
||||
const params = new URLSearchParams();
|
||||
params.append('tema', 'demografía y sociedad');
|
||||
for (const key in paramsObj) {
|
||||
if (paramsObj[key] !== undefined && paramsObj[key] !== '') {
|
||||
params.append(key, paramsObj[key]);
|
||||
}
|
||||
}
|
||||
url += `?${params.toString()}`;
|
||||
console.log('Fetch URL:', url);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
const data = await response.json();
|
||||
const nodeIds = new Set(data.nodes.map(n => n.id));
|
||||
data.links = data.links.filter(lk => nodeIds.has(lk.source) && nodeIds.has(lk.target));
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndRender() {
|
||||
const paramsObj = {
|
||||
subtematica: document.getElementById('param2').value,
|
||||
palabraClave: document.getElementById('param1').value,
|
||||
fechaInicio: document.getElementById('fecha_inicio').value,
|
||||
fechaFin: document.getElementById('fecha_fin').value,
|
||||
nodos: document.getElementById('nodos').value,
|
||||
complejidad: document.getElementById('complejidad').value,
|
||||
};
|
||||
const graphData = await getData(paramsObj);
|
||||
if (!graphData) return;
|
||||
lastGraphData = graphData;
|
||||
graph.graphData({ nodes: graphData.nodes, links: graphData.links });
|
||||
centerGraph();
|
||||
}
|
||||
|
||||
form.addEventListener('submit', event => { event.preventDefault(); fetchAndRender(); });
|
||||
fetchAndRender();
|
||||
});
|
||||
|
|
@ -1,289 +0,0 @@
|
|||
/* La familia "Retrolift" no se distribuye con el repo: no hay @font-face.
|
||||
Las reglas que la piden caen a sans-serif. Para usarla, deja el .woff2 en
|
||||
fonts/ y anade aqui la regla con una ruta relativa. */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Fira Code';
|
||||
text-shadow: 10px 10px 20px rgba(0, 255, 76, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Fira Code', monospace;
|
||||
background: #000000;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 70px;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: "Retrolift", sans-serif;
|
||||
font-size: 4em;
|
||||
font-weight: bold;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 6px 6px 6px #73ff00;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.popl-up:hover .logo { text-shadow: 2px 2px 8px #00008b; }
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #000000;
|
||||
color: #ff1a1a;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 40px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 6em;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
|
||||
padding: 20px 40px;
|
||||
box-shadow: 0 0 0px rgba(28, 103, 241, 0);
|
||||
position: relative;
|
||||
border: none;
|
||||
transition: .4s ease-in;
|
||||
z-index: 1;
|
||||
width: 40vw;
|
||||
height: 20vh;
|
||||
border: 3vw;
|
||||
align-items: center;
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.3);
|
||||
border: 3px solid black;
|
||||
}
|
||||
|
||||
.popl-up:hover { color: #0066ff; }
|
||||
.popl-up { color: #FF851B; background-color: orange; }
|
||||
|
||||
.background {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 99%;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.background a {
|
||||
display: block;
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background img {
|
||||
width: 20%;
|
||||
height: 90vh;
|
||||
object-fit: cover;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.background img:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background-image: url("/images/flujos7.jpg");
|
||||
background-size: cover;
|
||||
color: #39ff14;
|
||||
padding: 30px;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(-220px);
|
||||
transition: transform 0.3s ease-out;
|
||||
overflow: auto;
|
||||
font-size: 18px;
|
||||
z-index: 3;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
|
||||
}
|
||||
|
||||
#sidebar:hover {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#sidebar h2 {
|
||||
color: #39ff14;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: -1px 0 black, 0 3px black, 3px 0 black, 0 -1px black;
|
||||
}
|
||||
|
||||
#sidebar form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#sidebar label {
|
||||
color: #39ff14;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
|
||||
}
|
||||
|
||||
#sidebar input {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
#sidebar input:hover {
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#sidebar input[type="submit"] {
|
||||
color: #39ff14;
|
||||
background: #ff6600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#sidebar input[type="submit"]:hover {
|
||||
background: #ff6600;
|
||||
}
|
||||
|
||||
#sidebar select {
|
||||
padding: 10px;
|
||||
border: 2px solid #39ff14;
|
||||
background: black;
|
||||
color: #39ff14;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
#sidebar select:hover { box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.3); }
|
||||
#sidebar select option { background: #0a0a0a; color: #39ff14; }
|
||||
#sidebar .sim-val {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(57,255,20,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
background-color: #000000;
|
||||
color: #39ff14;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #39ff14;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #2ECC40;
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
width: calc(100% - 250px);
|
||||
height: 100vh;
|
||||
background: #000000;
|
||||
margin: 0 auto;
|
||||
border: none;
|
||||
z-index: 1;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
position: absolute;
|
||||
left: 250px;
|
||||
}
|
||||
|
||||
|
||||
/* ===== Panel de detalle (split-screen) ===== */
|
||||
main.split-screen { display: flex; height: 100vh; }
|
||||
#graphPanel { flex: 1; position: relative; min-width: 0; transition: flex 0.3s ease; }
|
||||
#detailPanel {
|
||||
width: 0; max-width: 0; overflow: hidden;
|
||||
transition: width 0.3s ease, max-width 0.3s ease;
|
||||
background: #0a0a0a; color: #fff;
|
||||
position: relative; z-index: 4;
|
||||
box-shadow: inset 2px 0 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
main.split-screen.show-detail #detailPanel { width: 50%; max-width: 50%; }
|
||||
|
||||
#closeDetail {
|
||||
position: absolute; top: 12px; left: 12px;
|
||||
background: rgba(0,0,0,0.85); border: 1px solid #39ff14;
|
||||
color: #39ff14; font-size: 1em; font-family: 'Fira Code', monospace;
|
||||
cursor: pointer; border-radius: 4px; padding: 4px 12px; z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#closeDetail:hover { background: rgba(57,255,20,0.15); }
|
||||
|
||||
#detailContent {
|
||||
padding: 52px 18px 20px; overflow-y: auto;
|
||||
height: 100%; box-sizing: border-box;
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
main.split-screen.show-detail #detailContent { display: flex; }
|
||||
|
||||
.detail-meta { display: flex; justify-content: space-between; font-size: 0.72em; color: #39ff14; opacity: 0.75; margin-bottom: 6px; gap: 8px; }
|
||||
.detail-author { font-weight: bold; }
|
||||
.detail-date { text-align: right; white-space: nowrap; }
|
||||
.detail-title { font-size: 0.92em; color: #39ff14; margin: 0 0 12px; word-break: break-word; line-height: 1.3; }
|
||||
|
||||
.detail-relations { border-top: 1px solid rgba(57,255,20,0.25); border-bottom: 1px solid rgba(57,255,20,0.25); padding: 8px 0; margin-bottom: 14px; font-size: 0.78em; flex-shrink: 0; }
|
||||
.relations-count { color: rgba(57,255,20,0.8); display: block; margin-bottom: 6px; }
|
||||
.relations-nav { display: flex; align-items: center; gap: 6px; }
|
||||
.relations-nav button { background: none; border: 1px solid #39ff14; color: #39ff14; cursor: pointer; padding: 3px 8px; border-radius: 3px; font-family: 'Fira Code', monospace; font-size: 0.85em; white-space: nowrap; transition: background 0.2s; }
|
||||
.relations-nav button:hover:not(:disabled) { background: rgba(57,255,20,0.12); }
|
||||
.relations-nav button:disabled { opacity: 0.3; cursor: default; }
|
||||
#relationLabel { flex: 1; text-align: center; color: rgba(255,255,255,0.55); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.85em; }
|
||||
|
||||
.detail-body { flex: 1; overflow-y: auto; white-space: pre-wrap; line-height: 1.55; font-size: 0.78em; color: #e0e0e0; font-family: 'Fira Code', monospace; min-height: 0; }
|
||||
.detail-img { width: 100%; max-height: 180px; object-fit: contain; margin-bottom: 12px; border: 1px solid rgba(57,255,20,0.3); border-radius: 3px; }
|
||||
#detailPanel .placeholder { color: rgba(255,255,255,0.3); font-style: italic; padding: 40px 20px; text-align: center; font-size: 0.85em; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main.split-screen.show-detail #graphPanel { display: none; }
|
||||
main.split-screen.show-detail #detailPanel { width: 100%; max-width: 100%; }
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
+<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Demografía y Sociedad</title>
|
||||
<link rel="stylesheet" href="popl-up.css">
|
||||
<link rel="stylesheet" href="sub-nav.css">
|
||||
|
||||
<!-- Fuentes -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Librerías necesarias -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://esm.sh/three@0.179.0", "three/": "https://esm.sh/three@0.179.0/" } }
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Navegación -->
|
||||
<nav class="top-nav">
|
||||
<div class="section-buttons">
|
||||
|
||||
<div class="section-item">
|
||||
<a href="glob-war.html" class="section-btn glob-war-btn">GLOB-WAR</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="int-sec.html" class="section-btn int-sec-btn">INT-SEC</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="climate.html" class="section-btn climate-btn">CLIMATE</a>
|
||||
</div>
|
||||
<div class="section-item">
|
||||
<a href="eco-corp.html" class="section-btn eco-corp-btn">ECO-CORP</a>
|
||||
</div>
|
||||
|
||||
<div class="section-item current">
|
||||
<a href="popl-up.html" class="section-btn popl-up-btn">POPL-UP <span class="dropdown-arrow">▾</span></a>
|
||||
<div class="section-dropdown">
|
||||
<a href="#" onclick="setSubtema('sobrepoblación');return false;">Sobrepoblación</a>
|
||||
<a href="#" onclick="setSubtema('enfermedades');return false;">COVID / Enfermedades</a>
|
||||
<a href="#" onclick="setSubtema('migraciones');return false;">Migraciones</a>
|
||||
<a href="#" onclick="setSubtema('urbanización');return false;">Urbanización</a>
|
||||
<a href="#" onclick="setSubtema('distribucion_edad');return false;">Despoblación Rural</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function setSubtema(val) {
|
||||
var sel = document.getElementById('param2');
|
||||
if (sel) { sel.value = val; }
|
||||
var form = document.getElementById('paramForm');
|
||||
if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
/* ── Dropdown touch (móvil) ── */
|
||||
var curr = document.querySelector('.section-item.current');
|
||||
if (curr) {
|
||||
curr.querySelector('.section-btn').addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
curr.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('touchend', function(e) {
|
||||
if (!curr.contains(e.target)) curr.classList.remove('open');
|
||||
});
|
||||
}
|
||||
/* ── Sidebar: botón toggle visible en móvil ── */
|
||||
var toggle = document.getElementById('sidebarToggle');
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.style.display = 'block';
|
||||
toggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Contenedor principal -->
|
||||
<main class="split-screen">
|
||||
<div id="graphPanel" style="position:relative;">
|
||||
<button id="volverGrafoBtn">← Volver al grafo completo</button>
|
||||
<div id="poplUpContainer" style="position: absolute; width: 100%; height: 100%; z-index: 0;"></div>
|
||||
<!-- Fondo animado -->
|
||||
<div class="background">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
<img src="/images/flujos3.jpg">
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
const fondo = document.querySelector('.background');
|
||||
fondo.classList.add('fade-out');
|
||||
fondo.style.pointerEvents = 'none';
|
||||
}, 1000);
|
||||
</script>
|
||||
</div>
|
||||
<div id="detailPanel">
|
||||
<button id="closeDetail">✕ cerrar</button>
|
||||
<div id="detailContent">
|
||||
<div class="detail-meta">
|
||||
<span class="detail-author"></span>
|
||||
<span class="detail-date"></span>
|
||||
</div>
|
||||
<h2 class="detail-title"></h2>
|
||||
<div class="detail-relations">
|
||||
<span class="relations-count"></span>
|
||||
<div class="relations-nav">
|
||||
<button id="prevRelation" disabled>← ant</button>
|
||||
<span id="relationLabel"></span>
|
||||
<button id="nextRelation" disabled>sig →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Barra lateral de filtros -->
|
||||
<div id="sidebar">
|
||||
<h2>Parámetros</h2>
|
||||
<form id="paramForm">
|
||||
<label for="fecha_inicio">Fecha de inicio:</label>
|
||||
<input type="date" id="fecha_inicio" name="fecha_inicio">
|
||||
|
||||
<label for="fecha_fin">Fecha de fin:</label>
|
||||
<input type="date" id="fecha_fin" name="fecha_fin">
|
||||
|
||||
<label for="nodos">Nodos:</label>
|
||||
<input type="number" id="nodos" name="nodos" value="100" min="5" max="500">
|
||||
|
||||
<label for="complejidad">% similitud: <span id="complejidadVal" class="sim-val">8</span></label>
|
||||
<input type="range" id="complejidad" name="complejidad" min="1" max="25" value="8"
|
||||
oninput="document.getElementById('complejidadVal').textContent = this.value">
|
||||
|
||||
<label for="param1">Palabra clave:</label>
|
||||
<input type="text" id="param1" name="param1" placeholder="ej: migración, pandemia...">
|
||||
|
||||
<label for="param2">Subtematica:</label>
|
||||
<select id="param2" name="param2">
|
||||
<option value="">— Todas —</option>
|
||||
<option value="enfermedades">enfermedades</option>
|
||||
<option value="urbanización">urbanización</option>
|
||||
<option value="migraciones">migraciones</option>
|
||||
<option value="sobrepoblación">sobrepoblación</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Aplicar">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Botón para colapsar la barra -->
|
||||
<button id="sidebarToggle">Toggle Sidebar</button>
|
||||
|
||||
<!-- Script principal -->
|
||||
<script src="semana-toggle.js"></script>
|
||||
<script type="module" src="output_popl_up_pruebas.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
:root {
|
||||
--cube-size: 70px; /* Tamaño reducido de los cubos */
|
||||
--line-color: #333;
|
||||
--hover-color: #2ecc71;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #000;
|
||||
border: 1px solid #ddd;
|
||||
margin: 20px auto;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.cube-container {
|
||||
position: absolute;
|
||||
width: var(--cube-size);
|
||||
height: var(--cube-size);
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 1s;
|
||||
}
|
||||
|
||||
.cube-container:hover {
|
||||
transform: rotateX(360deg) rotateY(360deg);
|
||||
}
|
||||
|
||||
.cube {
|
||||
position: absolute;
|
||||
width: var(--cube-size);
|
||||
height: var(--cube-size);
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.face {
|
||||
position: absolute;
|
||||
width: var(--cube-size);
|
||||
height: var(--cube-size);
|
||||
background-color: var(--hover-color);
|
||||
border: 1px solid #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Posiciones de las caras del cubo */
|
||||
.front { transform: translateZ(calc(var(--cube-size) / 2)); }
|
||||
.back { transform: rotateY(180deg) translateZ(calc(var(--cube-size) / 2)); }
|
||||
.left { transform: rotateY(-90deg) translateZ(calc(var(--cube-size) / 2)); }
|
||||
.right { transform: rotateY(90deg) translateZ(calc(var(--cube-size) / 2)); }
|
||||
.top { transform: rotateX(90deg) translateZ(calc(var(--cube-size) / 2)); }
|
||||
.bottom { transform: rotateX(-90deg) translateZ(calc(var(--cube-size) / 2)); }
|
||||
|
||||
.lines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.line-path {
|
||||
stroke: var(--line-color);
|
||||
stroke-width: 2;
|
||||
transition: stroke 0.3s;
|
||||
}
|
||||
|
||||
.line-path:hover {
|
||||
stroke: #e74c3c;
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Noticias de Guerras y Conflictos Políticos - Disposición Aleatoria</title>
|
||||
<link rel="stylesheet" type="text/css" href="script_eco-corp.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Noticias de Guerras y Conflictos Políticos - Últimos 10 años</h1>
|
||||
<div class="graph-container" id="graph-container">
|
||||
<!-- Los cubos serán añadidos dinámicamente con JavaScript -->
|
||||
<svg class="lines" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" id="svg-lines">
|
||||
<!-- Las flechas también serán generadas dinámicamente -->
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const numCubes = 5; // Número de cubos
|
||||
const graphContainer = document.getElementById('graph-container');
|
||||
const svgLines = document.getElementById('svg-lines');
|
||||
let cubePositions = [];
|
||||
|
||||
// Función para generar una posición aleatoria dentro del contenedor
|
||||
function getRandomPosition(maxWidth, maxHeight) {
|
||||
return {
|
||||
x: Math.floor(Math.random() * (maxWidth - 100)),
|
||||
y: Math.floor(Math.random() * (maxHeight - 100))
|
||||
};
|
||||
}
|
||||
|
||||
// Función para generar cubos aleatoriamente
|
||||
function generateCubes(num) {
|
||||
for (let i = 0; i < num; i++) {
|
||||
const position = getRandomPosition(window.innerWidth, window.innerHeight);
|
||||
const cubeContainer = document.createElement('div');
|
||||
cubeContainer.classList.add('cube-container');
|
||||
cubeContainer.style.top = position.y + 'px';
|
||||
cubeContainer.style.left = position.x + 'px';
|
||||
|
||||
const cube = document.createElement('div');
|
||||
cube.className = 'cube';
|
||||
[['front', `País ${i+1}`], ['back', `Detalle ${i+1}`],
|
||||
['left', `Año ${2010 + i}`], ['right', `Muertos ${(i+1)*1000}`],
|
||||
['top', 'ONU'], ['bottom', 'Fuente']].forEach(([cls, txt]) => {
|
||||
const face = document.createElement('div');
|
||||
face.className = `face ${cls}`;
|
||||
face.textContent = txt;
|
||||
cube.appendChild(face);
|
||||
});
|
||||
cubeContainer.appendChild(cube);
|
||||
|
||||
cubePositions.push({x: position.x + 35, y: position.y + 35}); // Ajustar al centro del cubo
|
||||
graphContainer.appendChild(cubeContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// Función para generar las flechas que conectan los cubos
|
||||
function generateLines() {
|
||||
for (let i = 0; i < cubePositions.length - 1; i++) {
|
||||
const x1 = cubePositions[i].x;
|
||||
const y1 = cubePositions[i].y;
|
||||
const x2 = cubePositions[i + 1].x;
|
||||
const y2 = cubePositions[i + 1].y;
|
||||
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', x1);
|
||||
line.setAttribute('y1', y1);
|
||||
line.setAttribute('x2', x2);
|
||||
line.setAttribute('y2', y2);
|
||||
line.setAttribute('stroke', '#333');
|
||||
line.setAttribute('stroke-width', '2');
|
||||
|
||||
svgLines.appendChild(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Generar cubos y flechas al cargar la página
|
||||
generateCubes(numCubes);
|
||||
generateLines();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||