// document.documentElement.classList.toggle('light-mode', isLight); // document.documentElement.classList.toggle('dark-mode', !isLight); if(lightIcon) lightIcon.classList.toggle('hidden', !isLight); if(darkIcon) darkIcon.classList.toggle('hidden', isLight); if (term) { term.options.theme = getXtermTheme(theme); } }; const getXtermTheme = (theme) => { const isLight = theme === 'light'; // Definición de temas para xterm.js usando la nueva paleta const viqusDarkTheme = { background: 'transparent', foreground: '#A8D4DB', // --text-secondary cursor: '#F0B44F', // --accent cursorAccent: '#0A1A21', // --bg selectionBackground: 'rgba(64, 192, 192, 0.3)', // --accent-secondary con alpha selectionForeground: '#E8F4F8', // --text-primary black: '#142A35', // --card-bg brightBlack: '#6B9AA8', // --text-muted white: '#E8F4F8', // --text-primary brightWhite: '#ffffff', red: '#e57373', brightRed: '#ef5350', green: '#81c784', brightGreen: '#66bb6a', yellow: '#F0B44F', // --accent brightYellow: '#ffcc66', blue: '#64b5f6', brightBlue: '#42a5f5', magenta: '#ba68c8', brightMagenta: '#ab47bc', cyan: '#40C0C0', // --accent-secondary brightCyan: '#60d8d8' }; const viqusLightTheme = { background: 'transparent', foreground: '#2A4853', // --text-secondary cursor: '#D4861F', // --accent cursorAccent: '#F7FAFB', // --bg selectionBackground: 'rgba(212, 134, 31, 0.3)', // --accent con alpha selectionForeground: '#0A2329', // --text-primary black: '#5B7A86', // --text-muted brightBlack: '#2A4853', white: '#0A2329', brightWhite: '#000000', red: '#d32f2f', brightRed: '#c62828', green: '#388e3c', brightGreen: '#2e7d32', yellow: '#D4861F', // --accent brightYellow: '#c78c21', blue: '#1976d2', brightBlue: '#1565c0', magenta: '#7b1fa2', brightMagenta: '#6a1b9a', cyan: '#1A7A7A', // --accent-secondary brightCyan: '#00838f' }; return isLight ? viqusLightTheme : viqusDarkTheme; } if(themeToggle) { themeToggle.addEventListener('click', () => { const currentTheme = document.documentElement.classList.contains('dark-mode') ? 'dark' : 'light'; const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; localStorage.setItem('theme', newTheme); // Actualizar clase en el momento para transiciones suaves document.documentElement.classList.remove(currentTheme + '-mode'); document.documentElement.classList.add(newTheme + '-mode'); applyTheme(newTheme); }); } const initialTheme = document.documentElement.classList.contains('dark-mode') ? 'dark' : 'light'; // --- INICIALIZACIÓN DE LA TERMINAL XTERM.JS --- term = new Terminal({ cursorBlink: true, fontFamily: "'JetBrains Mono', monospace", fontSize: 15, theme: getXtermTheme(initialTheme), allowTransparency: true // ¡Importante para el efecto glass! }); const fitAddon = new FitAddon.FitAddon(); term.loadAddon(fitAddon); const terminalDiv = document.getElementById('terminal'); term.open(terminalDiv); // Ajustar al contenedor function fitTerminal() { try { fitAddon.fit(); } catch (e) { console.error("Error al ajustar la terminal:", e); } } fitTerminal(); window.addEventListener('resize', fitTerminal); // Aplicar el tema inicial a los iconos applyTheme(initialTheme); term.focus(); // --- LÓGICA DE LA CONSOLA (Ligeras mejoras visuales) --- const prompt = `\r\n\x1b[1;36m┌──(\x1b[1;33mViqus\x1b[1;36m)-[\x1b[0;37m~/labs\x1b[1;36m]\x1b[0m\n\x1b[1;36m└─$\x1b[0m `; let currentLine = ''; let commandHistory = []; let historyIndex = -1; // --- RSS CLIENT (basado en el feed del lector anterior) --- const rssClient = { feedUrl: localStorage.getItem('viqus_cli_feed') || 'rss.xml', articles: [], lastResults: [], // buffer de la última lista mostrada (para open/show) async loadFeed({ refresh=false } = {}) { const url = this.feedUrl + (refresh ? ('?v=' + Date.now()) : ''); try { const resp = await fetch(url); if (!resp.ok) throw new Error('HTTP ' + resp.status); const xmlText = await resp.text(); const parser = new DOMParser(); const xml = parser.parseFromString(xmlText, 'application/xml'); if (xml.querySelector('parsererror')) throw new Error('XML mal formado'); const items = Array.from(xml.querySelectorAll('item')); this.articles = items.map((item, idx) => { const title = item.querySelector('title')?.textContent?.trim() || 'Sin título'; const link = item.querySelector('link')?.textContent?.trim() || '#'; const description = item.querySelector('description')?.textContent?.trim() || ''; const pubDate = item.querySelector('pubDate')?.textContent?.trim() || ''; const categories = Array.from(item.querySelectorAll('category')).map(c => c.textContent.trim()); const iso = pubDate ? (new Date(pubDate)).toISOString() : null; return { idx: idx + 1, title, link, description, pubDate, iso, categories }; }); // Por defecto, ordenar por fecha si existe this.articles.sort((a,b) => { if (a.iso && b.iso) return b.iso.localeCompare(a.iso); return 0; }); return this.articles.length; } catch (err) { throw new Error('Failed to load feed: ' + err.message); } }, setFeed(url) { if (!url) throw new Error('URL no válida'); this.feedUrl = url; localStorage.setItem('viqus_cli_feed', url); }, info() { return { url: this.feedUrl, total: this.articles.length }; }, filterLatest(n=5) { const count = Math.max(1, Math.min(n, this.articles.length || 0)); this.lastResults = this.articles.slice(0, count); return this.lastResults; }, search(term) { const q = term.toLowerCase(); this.lastResults = this.articles.filter(a => a.title.toLowerCase().includes(q) || a.description.toLowerCase().includes(q) || a.categories.some(c => c.toLowerCase().includes(q)) ); return this.lastResults; }, byDate(dateStr) { // YYYY-MM-DD en zona local this.lastResults = this.articles.filter(a => (a.iso || '').startsWith(dateStr)); return this.lastResults; }, between(fromStr, toStr) { // inclusive: comparar prefijos YYYY-MM-DD const from = fromStr; const to = toStr; this.lastResults = this.articles.filter(a => { const d = (a.iso || '').slice(0,10); return d >= from && d <= to; }); return this.lastResults; }, byTag(tag) { const q = tag.toLowerCase(); this.lastResults = this.articles.filter(a => a.categories.some(c => c.toLowerCase() === q)); return this.lastResults; }, getByIndex(i) { // i es índice 1-based sobre lastResults const idx = parseInt(i, 10); if (isNaN(idx) || idx < 1 || idx > this.lastResults.length) return null; return this.lastResults[idx - 1]; } }; // --- FORMATEADORES --- const ansi = { reset: '\x1b[0m', dim: '\x1b[38;5;244m', strong: '\x1b[1;37m', accent: '\x1b[1;33m', cyan: '\x1b[36m', yellow: '\x1b[33m', red: '\x1b[31m' }; function truncate(str, max=200) { const s = (str || '').replace(/\s+/g,' ').trim(); return s.length > max ? s.slice(0, max - 1) + '…' : s; } function fmtDate(iso) { if (!iso) return 'unknown date'; try { const d = new Date(iso); return d.toLocaleString(); } catch { return iso; } } function openLink(href) { try { window.open(href, '_blank'); } catch { /* noop */ } } const render = { list(items, {title='Resultados', showIndex=true}={}) { let out = `\r\n${ansi.strong}${title}:${ansi.reset}\r\n`; if (!items || items.length === 0) return out + ' (no results)'; items.forEach((a, i) => { const idx = showIndex ? `${ansi.dim}[${i+1}]${ansi.reset} ` : ''; const tags = a.categories && a.categories.length ? ` ${ansi.dim}#${a.categories.join(' #')}${ansi.reset}` : ''; out += ` ${idx}${a.title}${tags}\r\n ${ansi.dim}${fmtDate(a.iso)} · ${a.link}${ansi.reset}\r\n`; }); return out; }, article(a) { if (!a) return `\r\n${ansi.red}Error:${ansi.reset} Index out of range. Use ${ansi.accent}latest${ansi.reset} o ${ansi.accent}search${ansi.reset} first to populate the context.`; let out = '\r\n'; out += ` ${ansi.dim}┌─ ${ansi.strong}Article${ansi.reset} ${ansi.dim}────────────────────────────────────────────┐${ansi.reset}\r\n`; out += ` ${ansi.dim}│${ansi.reset} ${ansi.strong}📰 ${a.title}${ansi.reset}\r\n`; out += ` ${ansi.dim}│${ansi.reset} ${ansi.dim}${fmtDate(a.iso)}${ansi.reset}\r\n`; if (a.categories?.length) out += ` ${ansi.dim}│${ansi.reset} ${ansi.cyan}Tags:${ansi.reset} ${a.categories.join(', ')}\r\n`; out += ` ${ansi.dim}│${ansi.reset}\r\n`; out += ` ${ansi.dim}│${ansi.reset} ${truncate(a.description, 600)}\r\n`; out += ` ${ansi.dim}│${ansi.reset}\r\n`; out += ` ${ansi.dim}│${ansi.reset} ${ansi.cyan}Link:${ansi.reset} ${a.link}\r\n`; out += ` ${ansi.dim}└──────────────────────────────────────────────────────────────────┘${ansi.reset}`; return out; }, help() { return `\r\n ${ansi.strong}Viqus CLI Commands (RSS)${ansi.reset}\r\n\r\n` + ` ${ansi.accent}help${ansi.reset} Displays this help.\r\n` + ` ${ansi.accent}clear${ansi.reset} Clears the screen.\r\n` + `\r\n ${ansi.strong}Feed:${ansi.reset}\r\n` + ` ${ansi.accent}feed set ${ansi.reset} Sets the feed URL (persists in localStorage).\r\n` + ` ${ansi.accent}feed info${ansi.reset} Muestra la URL actual y el número de articles cargados.\r\n` + ` ${ansi.accent}feed refresh${ansi.reset} Reloads the feed (without cache).\r\n` + `\r\n ${ansi.strong}Noticias:${ansi.reset}\r\n` + ` ${ansi.accent}latest [N]${ansi.reset} Lists the last N news items (default: 5).\r\n` + ` ${ansi.accent}search ${ansi.reset} Search by title, description or category.\r\n` + ` ${ansi.accent}on ${ansi.reset} Filter by exact date.\r\n` + ` ${ansi.accent}between ${ansi.reset} Date range.\r\n` + ` ${ansi.accent}tag ${ansi.reset} Filter by RSS tag/category.\r\n` + ` ${ansi.accent}show <#>${ansi.reset} Shows details of result # (from the last list).\r\n` + ` ${ansi.accent}open <#>${ansi.reset} Opens result # in a new tab (from the last list).\r\n`; } }; // --- CARGA INICIAL DEL FEED --- (async () => { try { const total = await rssClient.loadFeed(); term.writeln(`\r\n${ansi.dim}Feed loaded:${ansi.reset} ${rssClient.feedUrl} ${ansi.dim}(${total} articles)${ansi.reset}`); } catch (e) { term.writeln(`\r\n${ansi.red}Notice:${ansi.reset} ${e.message}. You can configure the feed with ${ansi.accent}feed set ${ansi.reset} and then ${ansi.accent}feed refresh${ansi.reset}.`); } })(); const formatters = { bar(score, color = '\x1b[38;5;214m') { /* Usando el color naranja/amarillo como default */ const filled = '█'; const empty = '░'; const filledCount = Math.round(score); const emptyCount = 10 - filledCount; return `${color}${filled.repeat(filledCount)}\x1b[38;5;240m${empty.repeat(emptyCount)}\x1b[0m`; }, article(id, data) { if (!data) return `\r\n\x1b[31mError:\x1b[0m No se encontró el análisis con ID '${id}'.`; let output = '\r\n'; output += ` \x1b[38;5;242m┌─ \x1b[1;37mViqus Analysis #${id} \x1b[38;5;242m────────────────────────────┐\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[1;37m📰 ${data.title}\x1b[0m\r\n`; output += ` \x1b[38;5;242m├──────────────────────────────────────────────────┤\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[1;33mVerdict:\x1b[0m \x1b[1;37m${data.veredicto_viqus}\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[33mHype: ${data.puntuacion_hype}/10 \x1b[0m${this.bar(data.puntuacion_hype)} \r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[3;38;5;244m"${data.justificacion_hype}"\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[1;36mImpacto: ${data.puntuacion_impacto}/10 \x1b[0m${this.bar(data.puntuacion_impacto, '\x1b[36m')} \r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[3;38;5;244m"${data.justificacion_impacto}"\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[1;37m Key Points:\x1b[0m\r\n`; data.puntos_clave.forEach(p => { output += ` \x1b[38;5;242m│\x1b[0m \x1b[36m•\x1b[0m ${p}\r\n`; }); output += ` \x1b[38;5;242m└──────────────────────────────────────────────────┘\x1b[0m`; return output; }, entity(name, data) { if (!data) return `\r\n\x1b[31mError:\x1b[0m No se encontró la entidad '${name}'.`; let output = '\r\n'; output += ` \x1b[38;5;242m┌─ \x1b[1;37mEntity Dossier: ${name.charAt(0).toUpperCase() + name.slice(1)} \x1b[38;5;242m───────────────┐\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[1;37mStrategic Summary:\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[3m${data.resumen}\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m \r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[1;37mAverage Viqus Verdict:\x1b[0m\r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[33mHype: ${data.veredicto_promedio.hype}/10 \x1b[0m${this.bar(data.veredicto_promedio.hype)}\r\n`; output += ` \x1b[38;5;242m│\x1b[0m \x1b[1;36mImpacto: ${data.veredicto_promedio.impacto}/10 \x1b[0m${this.bar(data.veredicto_promedio.impacto, '\x1b[36m')}\r\n`; output += ` \x1b[38;5;242m└──────────────────────────────────────────────────┘\x1b[0m`; return output; }, help() { return render.help() /* replaced help content */ + `\r\n \x1b[1;37mViqus Console Commands:\x1b[0m\r\n\r\n \x1b[1;33mhelp\x1b[0m Displays this help.\r\n \x1b[1;33mlatest \x1b[38;5;244m[N]\x1b[0m Muestra los N últimos análisis (def: 5).\r\n \x1b[1;33mget \x1b[38;5;244m\x1b[0m Gets the full analysis of a news item.\r\n \x1b[1;33msearch \x1b[38;5;244m\x1b[0m Searches news by title or entity.\r\n \x1b[1;33mentity \x1b[38;5;244m\x1b[0m Displays a dossier about an entity.\r\n \x1b[1;33mclear\x1b[0m Limpia la pantalla de la consola.\r\n`;} }; async function runCommand(command) { const [cmd, ...args] = command.trim().split(' '); term.writeln(''); switch (cmd) { case 'help': term.writeln(render.help()); break; case 'clear': term.clear(); break; // --- Feed management --- case 'feed': const sub = (args[0] || '').toLowerCase(); if (sub === 'set') { if (!args[1]) { term.writeln(` ${ansi.red}Error:${ansi.reset} Debes indicar una URL. Uso: feed set `); break; } try { rssClient.setFeed(args[1]); term.writeln(` Feed updated: ${rssClient.feedUrl}`); } catch (e) { term.writeln(` ${ansi.red}Error:${ansi.reset} ${e.message}`); } } else if (sub === 'info') { const info = rssClient.info(); term.writeln(` URL: ${info.url} Articles loaded: ${info.total}`); } else if (sub === 'refresh') { try { const total = await rssClient.loadFeed({ refresh: true }); term.writeln(` Refreshed. Articles: ${total}`); } catch (e) { term.writeln(` ${ansi.red}Error:${ansi.reset} ${e.message}`); } } else { term.writeln(` ${ansi.red}Error:${ansi.reset} Unknown subcommand. Use: feed set|info|refresh`); } break; // --- News listing & filters --- case 'latest': { const n = args[0] ? parseInt(args[0], 10) : 5; const items = rssClient.filterLatest(isNaN(n) ? 5 : n); term.writeln(render.list(items, { title: 'Latest news' })); break; } case 'search': { if (args.length === 0) { term.writeln(` ${ansi.red}Error:${ansi.reset} You must provide a term.`); break; } const q = args.join(' '); const items = rssClient.search(q); term.writeln(render.list(items, { title: `Results for "${q}"` })); break; } case 'on': { if (!args[0]) { term.writeln(` ${ansi.red}Error:${ansi.reset} You must provide a date YYYY-MM-DD.`); break; } const items = rssClient.byDate(args[0]); term.writeln(render.list(items, { title: `News from ${args[0]}` })); break; } case 'between': { if (args.length < 2) { term.writeln(` ${ansi.red}Error:${ansi.reset} Usage: between `); break; } const items = rssClient.between(args[0], args[1]); term.writeln(render.list(items, { title: `News between ${args[0]} y ${args[1]}` })); break; } case 'tag': { if (!args[0]) { term.writeln(` ${ansi.red}Error:${ansi.reset} You must provide a tag.`); break; } const items = rssClient.byTag(args[0]); term.writeln(render.list(items, { title: `News with tag "${args[0]}"` })); break; } // --- Article actions --- case 'show': { if (!args[0]) { term.writeln(` ${ansi.red}Error:${ansi.reset} You must provide an index (from the last list).`); break; } const a = rssClient.getByIndex(args[0]); term.writeln(render.article(a)); break; } case 'open': { if (!args[0]) { term.writeln(` ${ansi.red}Error:${ansi.reset} You must provide an index (from the last list).`); break; } const a = rssClient.getByIndex(args[0]); if (!a) { term.writeln(` ${ansi.red}Error:${ansi.reset} Invalid index.`); break; } openLink(a.link); term.writeln(` Opening: ${a.link}`); break; } case '': break; default: term.writeln(`\r\n\x1b[31mError:\x1b[0m Unrecognized command '\x1b[1;31m${command}\x1b[0m'. Escribe '\x1b[1;33mhelp\x1b[0m' para ver la lista de comandos.`); break; } term.write(prompt); } term.writeln('\x1b[1;36m╭──────────────────────────────────────────────────────────╮\x1b[0m'); term.writeln('\x1b[1;36m│\x1b[0m \x1b[1;37mWelcome to Viqus CLI\x1b[0m \x1b[1;36m│\x1b[0m'); term.writeln('\x1b[1;36m│\x1b[0m \x1b[36mType \x1b[1;33m\'help\'\x1b[0;36m to get started.\x1b[0m \x1b[1;36m│\x1b[0m'); term.writeln('\x1b[1;36m╰──────────────────────────────────────────────────────────╯\x1b[0m'); term.write(prompt); term.onKey(({ key, domEvent }) => { const printable = !domEvent.altKey && !domEvent.ctrlKey && !domEvent.metaKey; if (domEvent.keyCode === 13) { runCommand(currentLine); if (currentLine) { commandHistory.unshift(currentLine); if(commandHistory.length > 50) commandHistory.pop(); } currentLine = ''; historyIndex = -1; } else if (domEvent.keyCode === 8) { if (currentLine.length > 0) { term.write('\b \b'); currentLine = currentLine.slice(0, -1); } } else if (domEvent.keyCode === 38) { // Up if (historyIndex < commandHistory.length - 1) { historyIndex++; term.write('\x1b[2K\r' + prompt.replace(/\r?\n|\r/g, '')); currentLine = commandHistory[historyIndex]; term.write(currentLine); } } else if (domEvent.keyCode === 40) { // Down if (historyIndex > 0) { historyIndex--; term.write('\x1b[2K\r' + prompt.replace(/\r?\n|\r/g, '')); currentLine = commandHistory[historyIndex]; term.write(currentLine); } else { historyIndex = -1; term.write('\x1b[2K\r' + prompt.replace(/\r?\n|\r/g, '')); currentLine = ''; } } else if (printable) { currentLine += key; term.write(key); } }); });