Compare commits

..

3 commits

Author SHA1 Message Date
hacklab
d6af9046ba feat: demo visualización imágenes Wikipedia + gitignore wiki/
- demo_wiki_images.html: grafo 3D con las 15 imágenes de Wikipedia
  sobre cambio climático como nodos sprite, panel de detalle al click
- images/wiki/.gitignore: excluye imágenes binarias del repo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 20:48:18 +02:00
hacklab
655a4de5e9 feat: scraper de imágenes Wikipedia + debug
- wikipedia_image_scraper.py: descarga imágenes de Wikipedia por tema
  usando Wikimedia API, con filtros de tamaño/extensión y metadatos
  (autor, licencia, dimensiones, artículo origen)
- debug_wiki.py: script de diagnóstico para verificar API responses
- .gitignore: excluye output/ y __pycache__

Fix: normalizar prefijo "Archivo:" → "File:" para Wikimedia Commons API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 19:41:11 +02:00
hacklab
5c73521685 feat: pipeline de imágenes + demos de visualización 3D
- BACK_BACK/IMAGENES/: nueva pipeline para análisis de imágenes con VLM
  - image_analyzer.py: imagen → keywords via ollama
  - image_comparator.py: TF-IDF similitud keywords vs corpus
  - mongo_helper.py: CRUD colecciones imagenes y comparaciones
  - pipeline_pruebas.py: script end-to-end con CLI
  - requirements_imagenes.txt

- BACK_BACK/FLUJOS_APP_PRUEBAS.js: servidor Express ligero (port 3001)
  sin MongoDB para testear los demos de visualización

- VISUALIZACION/public/demos/: tres demos de 3d-force-graph
  - demo_text_nodes.html: nodos como texto (three-spritetext)
  - demo_img_nodes.html: nodos como imágenes (THREE.Sprite)
  - demo_mixed_nodes.html: cards HTML en 3D (CSS2DRenderer)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:28:34 +01:00
238 changed files with 51 additions and 190962 deletions

View file

@ -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
View file

@ -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
View file

@ -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/

View file

@ -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);
});

View file

@ -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)

View file

@ -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'))

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -1 +0,0 @@
pip install obsei[all]

View file

@ -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.

View file

@ -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
// }
// ];

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[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>

View file

@ -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%; }
}

View file

@ -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>&#8592; ant</button>
<span id="relationLabel"></span>
<button id="nextRelation" disabled>sig &#8594;</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>

View file

@ -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>

View file

@ -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; }
}

View file

@ -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%; }
}

View file

@ -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>&#8592; ant</button>
<span id="relationLabel"></span>
<button id="nextRelation" disabled>sig &#8594;</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>

View file

@ -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%; }
}

View file

@ -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>&#8592; ant</button>
<span id="relationLabel"></span>
<button id="nextRelation" disabled>sig &#8594;</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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View file

@ -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>

View file

@ -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%; }
}

View file

@ -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>&#8592; ant</button>
<span id="relationLabel"></span>
<button id="nextRelation" disabled>sig &#8594;</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>

View file

@ -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;
}

View file

@ -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">&ldquo;Los gigantes vistos en perspectiva parecen marionetas.&rdquo;</p>
</div>
</footer>
</body>
</html>

View file

@ -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 */
}
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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 };

View file

@ -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

View file

@ -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;
}
}
}
}

View file

@ -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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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".');
}
});

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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);
}
});

View file

@ -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();
});

View file

@ -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%; }
}

View file

@ -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>&#8592; ant</button>
<span id="relationLabel"></span>
<button id="nextRelation" disabled>sig &#8594;</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>

View file

@ -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;
}

View file

@ -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>

Some files were not shown because too many files have changed in this diff Show more