| <!DOCTYPE html> |
| <html lang="es"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>PLATE · Formatos Institucionales</title> |
| <style> |
| @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap"); |
| :root{--black:#111111;--dark:#1a1a1a;--card:#1f1f1f;--border:#2e2e2e;--white:#f0f0f0;--muted:#888;--amber:#d4932a;--blue:#2b9fd8;--green:#6abf4b;} |
| *{box-sizing:border-box;margin:0;padding:0;} |
| body{background:var(--black);color:var(--white);font-family:"IBM Plex Sans",sans-serif;min-height:100vh;display:flex;flex-direction:column;} |
| header{display:flex;align-items:center;justify-content:space-between;padding:20px 40px;border-bottom:1px solid var(--border);background:var(--dark);} |
| .logo-wrap{display:flex;align-items:center;gap:14px;} |
| .logo-text{font-family:"IBM Plex Mono",monospace;font-size:18px;font-weight:600;letter-spacing:0.1em;} |
| .logo-sep{width:1px;height:28px;background:var(--border);} |
| .logo-sub{font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--muted);letter-spacing:0.15em;text-transform:uppercase;} |
| .btn-back{font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--muted);text-decoration:none;border:1px solid var(--border);border-radius:6px;padding:6px 14px;transition:all .15s;} |
| .btn-back:hover{border-color:var(--amber);color:var(--white);} |
| main{flex:1;max-width:720px;margin:0 auto;width:100%;padding:48px 24px;} |
| .page-tag{font-size:11px;font-family:"IBM Plex Mono",monospace;color:var(--amber);letter-spacing:0.2em;text-transform:uppercase;margin-bottom:10px;} |
| h1{font-size:26px;font-weight:600;margin-bottom:8px;} |
| .subtitle{font-size:14px;color:var(--muted);margin-bottom:36px;line-height:1.6;} |
|
|
| .search-wrap{position:relative;margin-bottom:12px;} |
| .search-input{width:100%;background:var(--dark);border:1px solid var(--border);border-radius:12px;color:var(--white);font-family:"IBM Plex Sans",sans-serif;font-size:15px;padding:16px 56px 16px 20px;outline:none;resize:none;line-height:1.5;transition:border-color .15s;} |
| .search-input:focus{border-color:var(--amber);} |
| .search-input::placeholder{color:#555;} |
| .btn-search{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:var(--amber);border:none;border-radius:8px;width:36px;height:36px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s;} |
| .btn-search:hover:not(:disabled){background:#c47d1e;} |
| .btn-search:disabled{opacity:0.4;cursor:default;} |
| .btn-search svg{width:16px;height:16px;color:#fff;} |
| .hint{font-size:12px;color:#555;margin-bottom:32px;} |
| .hint span{color:#666;margin-right:12px;cursor:pointer;transition:color .15s;} |
| .hint span:hover{color:var(--amber);} |
|
|
| .results{display:flex;flex-direction:column;gap:12px;} |
| .result-card{background:var(--dark);border:1px solid var(--border);border-radius:12px;padding:20px 24px;transition:border-color .15s;} |
| .result-card:hover{border-color:#444;} |
| .rc-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px;} |
| .rc-code{font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--amber);font-weight:600;letter-spacing:0.05em;white-space:nowrap;margin-top:2px;} |
| .rc-name{font-size:15px;font-weight:600;color:var(--white);} |
| .rc-fase{font-size:11px;font-family:"IBM Plex Mono",monospace;color:var(--muted);margin-bottom:6px;} |
| .rc-desc{font-size:13px;color:var(--muted);line-height:1.6;margin-bottom:8px;} |
| .rc-reason{font-size:12px;color:#6abf4b;line-height:1.5;} |
| .rc-reason::before{content:"→ ";} |
| .rc-ruta{display:flex;align-items:center;gap:6px;margin-top:10px;padding-top:10px;border-top:1px solid #2a2a2a;font-size:11px;color:#555;font-family:"IBM Plex Mono",monospace;} |
| .rc-ruta svg{width:12px;height:12px;flex-shrink:0;color:#666;} |
| .rc-ruta span{color:#666;} |
| .nota-card{background:rgba(212,147,42,0.08);border:1px solid rgba(212,147,42,0.25);border-radius:10px;padding:14px 18px;font-size:13px;color:#d4a04a;margin-top:4px;} |
|
|
| .rc-archivos{margin-top:10px;padding-top:10px;border-top:1px solid #2a2a2a;display:flex;flex-direction:column;gap:6px;} |
| .rc-download{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--green);text-decoration:none;width:fit-content;} |
| .rc-download:hover{text-decoration:underline;} |
| .rc-upload-link{font-size:12px;color:#666;background:none;border:none;cursor:pointer;font-family:inherit;text-decoration:underline;padding:0;width:fit-content;} |
| .rc-upload-link:hover{color:var(--amber);} |
|
|
| .upload-toggle{font-size:12px;color:#666;cursor:pointer;text-decoration:underline;margin-bottom:20px;display:inline-block;} |
| .upload-toggle:hover{color:var(--amber);} |
| .upload-panel{display:none;background:var(--dark);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin-bottom:24px;flex-direction:column;gap:10px;} |
| .upload-panel.on{display:flex;} |
| .upload-panel label{font-size:11px;color:var(--muted);font-family:"IBM Plex Mono",monospace;letter-spacing:0.08em;} |
| .upload-panel input[type=text]{background:var(--card);border:1px solid var(--border);border-radius:8px;color:var(--white);font-size:13px;padding:8px 12px;width:100%;} |
| .upload-panel input[type=file]{font-size:12px;color:var(--muted);} |
| .upload-panel button{background:var(--amber);border:none;color:#fff;border-radius:8px;padding:9px 16px;font-size:12px;font-weight:600;cursor:pointer;width:fit-content;font-family:"IBM Plex Mono",monospace;} |
| .upload-panel button:hover:not(:disabled){background:#c47d1e;} |
| .upload-panel button:disabled{opacity:.4;cursor:default;} |
| .upload-status{font-size:12px;color:var(--muted);} |
| .upload-status.ok{color:var(--green);} |
| .upload-status.err{color:#e05252;} |
|
|
| .empty{text-align:center;padding:48px 24px;color:var(--muted);} |
| .empty svg{width:40px;height:40px;opacity:.3;margin-bottom:12px;} |
|
|
| .loading{display:flex;gap:4px;justify-content:center;padding:40px;} |
| .loading span{width:8px;height:8px;border-radius:50%;background:var(--amber);animation:ld 1s ease-in-out infinite;} |
| .loading span:nth-child(2){animation-delay:.15s;} |
| .loading span:nth-child(3){animation-delay:.3s;} |
| @keyframes ld{0%,80%,100%{transform:scale(0.6);opacity:0.4;}40%{transform:scale(1);opacity:1;}} |
|
|
| footer{border-top:1px solid var(--border);padding:14px 40px;display:flex;align-items:center;justify-content:space-between;background:var(--dark);} |
| .ftext{font-size:11px;color:var(--muted);font-family:"IBM Plex Mono",monospace;} |
| .dot{width:6px;height:6px;border-radius:50%;background:var(--amber);display:inline-block;margin-right:6px;animation:pulse 2s ease-in-out infinite;} |
| @keyframes pulse{0%,100%{opacity:1;}50%{opacity:0.3;}} |
| @media(max-width:540px){header{padding:16px 20px;}.logo-sub{display:none;}main{padding:32px 16px;}h1{font-size:20px;}} |
| </style> |
| </head> |
| <body> |
| <header> |
| <div class="logo-wrap"> |
| <span class="logo-text">PLATE</span> |
| <div class="logo-sep"></div> |
| <span class="logo-sub">Formatos Institucionales</span> |
| </div> |
| <a class="btn-back" href="/">← Toolkit</a> |
| </header> |
|
|
| <main> |
| <p class="page-tag">Buscador de Formatos</p> |
| <h1>¿Qué necesitas hacer?</h1> |
| <p class="subtitle">Describe en tus palabras la tarea o situación y te digo qué formato institucional de PLATE usar.</p> |
|
|
| <div class="search-wrap"> |
| <textarea class="search-input" id="queryInput" rows="2" placeholder="Ej: quiero invitar a un contratista a concursar para obra civil…"></textarea> |
| <button class="btn-search" id="searchBtn"> |
| <svg viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> |
| </button> |
| </div> |
| <p class="hint">Prueba: <span onclick="setQ('hacer el acta de entrega del proyecto al cliente')">Acta de entrega</span><span onclick="setQ('enviar documentos formalmente a un contratista')">Transmittal</span><span onclick="setQ('hacer una minuta de recorrido de obra')">Minuta de obra</span><span onclick="setQ('controlar los pagos y saldos de contratos')">Control contractual</span></p> |
|
|
| <span class="upload-toggle" id="uploadToggle">+ Subir una plantilla</span> |
| <div class="upload-panel" id="uploadPanel"> |
| <div> |
| <label>CÓDIGO DEL FORMATO</label> |
| <input type="text" id="uploadCodigo" placeholder="Ej: 3.5.9"> |
| </div> |
| <div> |
| <label>ARCHIVO</label><br> |
| <input type="file" id="uploadArchivo"> |
| </div> |
| <button id="uploadBtn" onclick="subirPlantilla()">Subir</button> |
| <div class="upload-status" id="uploadStatus"></div> |
| </div> |
|
|
| <div class="results" id="results"></div> |
| </main> |
|
|
| <footer> |
| <span class="ftext"><span class="dot"></span>PLATE · Formatos Institucionales</span> |
| <span class="ftext">Confidencial — Solo para uso interno</span> |
| </footer> |
|
|
| <script> |
| const BASE = '99. RECURSOS → Formatos Plate → FORMATOS'; |
| const RUTAS = { |
| '1.1': '1.0 INICIO → 1.1 KICK-OFF', |
| '2.1': '2.0 PRE-DESARROLLO → 2.1 FORMATOS PARA REUNIONES DE TRABAJO', |
| '2.2': '2.0 PRE-DESARROLLO → 2.2 FORMATOS PARA REUNIONES DE DEFINICION DEL PROYECTO CON EL CLIENTE', |
| '2.3': '2.0 PRE-DESARROLLO → 2.3 FORMATOS PARA REUNIONES DE DEFINICIÓN DEL PROYECTO (PLATE)', |
| '2.4': '2.0 PRE-DESARROLLO → 2.4 FORMATOS PARA CONTROL ADMINISTRATIVO CLIENTE', |
| '2.5': '2.0 PRE-DESARROLLO → 2.5 FORMATOS PARA CONTROL ADMINISTRATIVO PLATE', |
| '3.1': '3.0 DESARROLLO → 3.1 FORMATOS PARA REUNIONES DE TRABAJO', |
| '3.2': '3.0 DESARROLLO → 3.2 FORMATOS PARA REUNIONES DE DEFINICION DEL PROYECTO CLIENTE', |
| '3.3': '3.0 DESARROLLO → 3.3 FORMATOS PARA REUNIONES DE DEFINICION DEL PROYECTO PLATE', |
| '3.4': '3.0 DESARROLLO → 3.4 FORMATOS CONTROL DE OBRA CLIENTE', |
| '3.5': '3.0 DESARROLLO → 3.5 FORMATOS CONTROL DE OBRA PLATE', |
| '3.6': '3.0 DESARROLLO → 3.6 FORMATOS CONTROL ADMINISTRATIVO CLIENTE', |
| '3.7': '3.0 DESARROLLO → 3.7 FORMATOS CONTROL ADMINISTRATIVO PLATE', |
| '4.1': '4.0 CIERRE ADMINISTRATIVO → 4.1 FORMATOS PARA REUNIONES DE TRABAJO', |
| '4.2': '4.0 CIERRE ADMINISTRATIVO → 4.2 FORMATOS CIERRE DE OBRA CLIENTE', |
| '4.3': '4.0 CIERRE ADMINISTRATIVO → 4.3 FORMATOS CIERRE DE OBRA PLATE', |
| '4.4': '4.0 CIERRE ADMINISTRATIVO → 4.4 FORMATOS CIERRE ADMINISTRATIVO CLIENTE', |
| }; |
| |
| function getRuta(codigo) { |
| const prefix = codigo.split('.').slice(0,2).join('.'); |
| const r = RUTAS[prefix]; |
| if (!r) return null; |
| return `Buildpeer → Documentos → ${BASE} → ${r}`; |
| } |
| |
| const input = document.getElementById('queryInput'); |
| const btn = document.getElementById('searchBtn'); |
| const res = document.getElementById('results'); |
| |
| function setQ(text) { input.value = text; input.focus(); } |
| |
| const uploadToggle = document.getElementById('uploadToggle'); |
| const uploadPanel = document.getElementById('uploadPanel'); |
| uploadToggle.addEventListener('click', () => uploadPanel.classList.toggle('on')); |
| |
| function abrirSubida(codigo) { |
| uploadPanel.classList.add('on'); |
| document.getElementById('uploadCodigo').value = codigo; |
| document.getElementById('uploadCodigo').focus(); |
| uploadPanel.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| } |
| |
| async function subirPlantilla() { |
| const codigo = document.getElementById('uploadCodigo').value.trim(); |
| const archivoInput = document.getElementById('uploadArchivo'); |
| const archivo = archivoInput.files[0]; |
| const statusEl = document.getElementById('uploadStatus'); |
| if (!codigo || !archivo) { |
| statusEl.textContent = 'Falta el código o el archivo.'; |
| statusEl.className = 'upload-status err'; |
| return; |
| } |
| const btn = document.getElementById('uploadBtn'); |
| btn.disabled = true; |
| statusEl.textContent = 'Subiendo...'; |
| statusEl.className = 'upload-status'; |
| try { |
| const fd = new FormData(); |
| fd.append('codigo', codigo); |
| fd.append('archivo', archivo); |
| const r = await fetch('/formatos/subir', { method: 'POST', body: fd }); |
| const data = await r.json(); |
| if (data.ok) { |
| statusEl.textContent = `Listo — "${data.archivo}" ya está disponible para el código ${data.codigo}.`; |
| statusEl.className = 'upload-status ok'; |
| archivoInput.value = ''; |
| } else { |
| statusEl.textContent = 'Error: ' + (data.error || 'desconocido'); |
| statusEl.className = 'upload-status err'; |
| } |
| } catch (e) { |
| statusEl.textContent = 'No se pudo conectar con el servidor.'; |
| statusEl.className = 'upload-status err'; |
| } |
| btn.disabled = false; |
| } |
| |
| async function cargarArchivos(codigo, contenedorId) { |
| try { |
| const r = await fetch(`/formatos/archivos/${encodeURIComponent(codigo)}`); |
| const data = await r.json(); |
| const el = document.getElementById(contenedorId); |
| if (!el) return; |
| if (data.ok && data.archivos && data.archivos.length) { |
| el.innerHTML = data.archivos.map(nombre => { |
| const nombreCorto = nombre.split('/').pop(); |
| return `<a class="rc-download" href="/formatos/descargar/${encodeURIComponent(codigo)}/${encodeURIComponent(nombreCorto)}" download>⬇ Descargar: ${nombreCorto}</a>`; |
| }).join(''); |
| } else { |
| el.innerHTML = `<button class="rc-upload-link" onclick="abrirSubida('${codigo}')">Aún no hay plantilla subida — subir una</button>`; |
| } |
| } catch (e) {} |
| } |
| |
| input.addEventListener('keydown', e => { |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); buscar(); } |
| }); |
| input.addEventListener('input', () => { |
| input.style.height = 'auto'; |
| input.style.height = Math.min(input.scrollHeight, 120) + 'px'; |
| }); |
| btn.addEventListener('click', buscar); |
| |
| async function buscar() { |
| const q = input.value.trim(); |
| if (!q) return; |
| btn.disabled = true; |
| res.innerHTML = '<div class="loading"><span></span><span></span><span></span></div>'; |
| |
| try { |
| const fd = new FormData(); |
| fd.append('query', q); |
| const r = await fetch('/buscar-formato', { method: 'POST', body: fd }); |
| const data = await r.json(); |
| |
| if (!data.ok) { res.innerHTML = `<div class="nota-card">Error: ${data.error}</div>`; return; } |
| |
| const items = data.resultados || []; |
| if (!items.length) { |
| res.innerHTML = `<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg><p>No encontré un formato específico para eso.<br>${data.nota || 'Intenta describir de otra forma.'}</p></div>`; |
| return; |
| } |
| |
| const faseLabel = { '1': '1.0 INICIO', '2': '2.0 PRE-DESARROLLO', '3': '3.0 DESARROLLO', '4': '4.0 CIERRE ADMINISTRATIVO' }; |
| let html = items.map((it, i) => { |
| const ruta = getRuta(it.codigo); |
| const archivosId = `archivos-${i}`; |
| return ` |
| <div class="result-card"> |
| <div class="rc-top"> |
| <div> |
| <div class="rc-name">${it.nombre}</div> |
| <div class="rc-fase">${it.fase || ''}</div> |
| </div> |
| <span class="rc-code">[${it.codigo}]</span> |
| </div> |
| <div class="rc-desc">${it.para_que}</div> |
| ${it.por_que ? `<div class="rc-reason">${it.por_que}</div>` : ''} |
| ${ruta ? `<div class="rc-ruta"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg><span>${ruta}</span></div>` : ''} |
| <div class="rc-archivos" id="${archivosId}"></div> |
| </div>`; |
| }).join(''); |
| |
| if (data.nota) html += `<div class="nota-card">${data.nota}</div>`; |
| res.innerHTML = html; |
| items.forEach((it, i) => cargarArchivos(it.codigo, `archivos-${i}`)); |
| } catch(e) { |
| res.innerHTML = `<div class="nota-card">No se pudo conectar. Intenta de nuevo.</div>`; |
| } |
| btn.disabled = false; |
| } |
| </script> |
| <script> |
| window.PAGE_CONTEXT = "El usuario está en el Buscador de Formatos Institucionales de PLATE. Aquí puede describir en lenguaje natural lo que necesita hacer (ej: 'quiero invitar a un contratista a concursar') y el sistema le dice qué formato oficial de PLATE usar, con su código de referencia. Los formatos están organizados en 4 fases: Inicio, Pre-desarrollo, Desarrollo y Cierre Administrativo. Si ya hay una plantilla subida para ese código, aparece un enlace de descarga directa en la tarjeta de resultado; si no, aparece un enlace para subir una. También hay un botón '+ Subir una plantilla' arriba de los resultados para subir un archivo asociado a un código de formato en cualquier momento (queda guardado permanentemente, no solo en esa sesión)."; |
| </script> |
| <script src="/static/chat-bubble.js"></script> |
| </body> |
| </html> |
|
|