diff --git a/i18n/en.json b/i18n/en.json index 8874353..ab9fc1c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -520,6 +520,249 @@ "statusBody": "The registration window has closed. We hope to see you at the next NaN hackathon!" } }, + "gauntlet": { + "metaTitle": "Gauntlet Challenge · NaN", + "metaDesc": "August at NaN: one prompt fires up a system that builds, critiques and corrects itself. Build until August 28, then the community votes.", + "heroEyebrow": "// gauntlet challenge", + "heroTitle1": "Gauntlet", + "heroTitle2": "Challenge.", + "heroSub": "One prompt. A system that builds, critiques and corrects itself. Build until August 28. Then the community votes.", + "status": { + "pre": "Starts in {n} days", + "preOne": "Starts tomorrow", + "preZero": "Starts today", + "live": "{n} days left to submit", + "liveOne": "Tomorrow is the last day to submit", + "liveZero": "Last day to submit", + "voting": "Voting is open", + "closed": "This edition is closed" + }, + "ctaSecondary": "Join the community", + "ctaVote": "Voting is on Discord", + "ctaClosed": "See community projects", + "form": { + "url": "https://docs.google.com/forms/d/e/1FAIpQLSdsu4TfUcWHv_DsDYge1hGgZN4naQy-ju_9ettH0j98X4Dx-Q/viewform", + "cta": "Sign up for the challenge", + "note": "Opens a Google Form in a new tab · 4 fields, half a minute", + "newTab": "opens in a new tab", + "soonTitle": "signup opens at the kickoff", + "soon": "The form goes live with the workshop on August 7. We announce the link on NaN's Discord." + }, + "showcase": { + "label": "what's already shipping", + "title": "This is what's coming out of building this way.", + "lead": "None of this is ours. These are projects other people posted over the last few weeks: every card goes to the original post, which is where the video lives.", + "credits": "credits the Gauntlet Loop", + "foot": "All posted between July 25 and August 8, 2026.", + "items": { + "claudefare": { + "title": "Modern Claudefare", + "desc": "A Call of Duty remake with four maps, solo and multiplayer, and 84,100 lines of code. Its author says they put it together in a few days applying the principles of the Gauntlet Loop." + }, + "original": { + "title": "The original", + "desc": "The shooter that started all of this. Not one external asset: every line is custom." + }, + "museum": { + "title": "The Neural Museum", + "desc": "A museum you walk through where every framed piece is an AI paper." + }, + "anatomy": { + "title": "Anatomy Atelier", + "desc": "Human anatomy in 3D, organ by organ, inside the browser." + }, + "ironveil": { + "title": "Ironveil Range", + "desc": "A playable ballistic missile defense simulator." + } + } + }, + "join": { + "label": "the challenge", + "title": "The challenge is:", + "sub": "Build something good with the Gauntlet Loop prompt, using at least one NaN model. The theme is open: the shooter was the experiment, not the assignment.", + "reqTitle": "requirements", + "reqs": [ + "Use at least one NaN model" + ], + "deliverLabel": "what you hand in at the end", + "signupTitle": "signup is always open", + "signupBody": "There's no early cut-off: the form stays open until closing day. Nobody is left out for showing up late. The calendar is there to spread the work, not to shut a door." + }, + "context": { + "label": "where this comes from", + "title": "Someone asked an agent for a Call of Duty.", + "lead": "Not once. They built a loop: sub-agents each owning one piece, and a critic comparing the result against the real game that refused to sign off until you couldn't tell them apart. That is a Gauntlet Loop, and it's what the workshop opening August takes apart. The challenge is that everyone builds their own.", + "cards": [ + { + "q": "What do I build?", + "a": "Whatever you want. The theme is open: the shooter was the experiment, not the assignment. What gets shown is your loop and what came out of it." + }, + { + "q": "Who runs it?", + "a": "NaN, the springboard for builders shipping with open AI. Taking part means being a member.", + "link": "How to join" + }, + { + "q": "What do I get?", + "a": "A finished project, in front of people. No jury: the community votes on what was submitted and the most voted present it live." + } + ] + }, + "prompt": { + "label": "the prompt", + "title": "This is the whole prompt. Copy it and make it yours.", + "lead": "Here it is in full, annotated with what each piece is doing. Copy it and swap in your own thing: what carries over isn't the shooter, it's the shape.", + "frame": "Gauntlet Prompt", + "copy": "Copy the prompt", + "copied": "Copied", + "copyFail": "Select it and copy it by hand", + "foot": "Copied verbatim, in English: it is the original. Yours does not have to be a shooter. The theme is open.", + "notes": { + "ref": "Reference", + "fanout": "Fan out sub-agents, each owning one element until it is perfect", + "loop": "Every sub-agent inside a loop", + "critic": "One critic sub-agent per builder sub-agent", + "harsh": "Be a harsh critic and do not stop until it is indistinguishable from the original", + "ultra": "Super saiyan 4, ultra instinct, sharingan, find the one piece" + } + }, + "idea": { + "label": "the key idea", + "title": "A prompt doesn't do the work.", + "sub": "It fires up an autonomous system that builds, observes, critiques and corrects.", + "steps": [ + { + "n": "01", + "name": "Goal", + "body": "One human instruction stating what has to exist." + }, + { + "n": "02", + "name": "Builders", + "body": "Agents that write it. Each one owns a piece and ships it." + }, + { + "n": "03", + "name": "Critics", + "body": "Agents that review the work against proof, not impressions." + }, + { + "n": "04", + "name": "Evidence", + "body": "Screenshots, tests, metrics. Artifacts you can inspect." + }, + { + "n": "05", + "name": "Iteration", + "body": "Beats the reference: it lands. Doesn't: back to the builder." + } + ], + "quote": "The edge isn't thinking harder. It's closing the loop with feedback you can observe.", + "note": "\"One prompt\" means one initial human instruction, not one call and not one agent." + }, + "calendar": { + "label": "calendar", + "title": "August is a season, not a day.", + "month": "August 2026", + "weekdays": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ], + "tbd": "Presentation day · to be confirmed", + "short": { + "workshop": "Kickoff", + "build": "Build", + "close": "Closes", + "voting": "Vote" + } + }, + "submit": { + "label": "the submission", + "title": "What you submit has to prove what you built, and how.", + "sub": "One submission per project, editable until it closes.", + "requiredLabel": "required", + "required": [ + "Name + description", + "Model used", + "16:9 cover image", + "Loop architecture", + "Repository", + "Human intervention", + "Initial prompt", + "Result + learnings" + ], + "requiredNote": "The last two are the ones that prove what you built and how.", + "recommendedLabel": "recommended", + "recommended": [ + { + "name": "Video", + "why": "Shows the result" + }, + { + "name": "Demo", + "why": "Lets people try it" + }, + { + "name": "Baseline vs final", + "why": "Makes the improvement visible" + }, + { + "name": "Tokens and time", + "why": "Explain the cost" + }, + { + "name": "Logs and traces", + "why": "Make it auditable" + }, + { + "name": "Diagram", + "why": "Clarifies the architecture" + } + ], + "rule": "the image is required · the video is recommended · a public demo is optional", + "notes": [ + "Repository: it can stay private during August, but it has to be opened at closing time, or shared with the organisers so it can be verified.", + "Video: we don't host it. A YouTube, Loom or similar URL works." + ], + "whereTitle": "where to submit", + "whereBody": "The submission form will appear here later." + }, + "after": { + "label": "after the close", + "title": "The community picks who presents.", + "sub": "No jury. What you ship gets shown, the community votes, and the most-voted projects present live.", + "steps": [ + { + "when": "Aug 28", + "name": "Entry closes", + "body": "Signup and submissions stop." + }, + { + "when": "A few days", + "name": "Community voting", + "body": "Everything submitted gets shown, and the community votes." + }, + { + "when": "To be confirmed", + "name": "Presentations", + "body": "The most-voted projects present live." + } + ], + "tbdLabel": "date to be confirmed", + "tbdBody": "We're looking for a day that works for everyone, so it gets announced on Discord rather than pinned down now.", + "note": "Voting doesn't live on this page: it happens on NaN's Discord." + }, + "close": { + "title": "One prompt. A whole system.", + "text": "August at NaN is a season: build it, let it be critiqued, correct it and ship something you can prove. Signup is open until the 28th." + } + }, "community": { "metaTitle": "Community — NaN", "metaDesc": "NaN is the springboard for builders shipping with open AI. Build in public, together.", diff --git a/i18n/es.json b/i18n/es.json index ebace28..85dfadc 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -520,6 +520,249 @@ "statusBody": "La ventana de inscripción se ha cerrado. ¡Esperamos verte en el próximo hackathon de NaN!" } }, + "gauntlet": { + "metaTitle": "Gauntlet Challenge · NaN", + "metaDesc": "Agosto en NaN: un prompt activa un sistema que construye, critica y se corrige solo. Construye hasta el 28 de agosto y después vota la comunidad.", + "heroEyebrow": "// gauntlet challenge", + "heroTitle1": "Gauntlet", + "heroTitle2": "Challenge.", + "heroSub": "Un prompt. Un sistema que construye, critica y se corrige solo. Construye hasta el 28 de agosto. Después vota la comunidad.", + "status": { + "pre": "Empieza en {n} días", + "preOne": "Empieza mañana", + "preZero": "Empieza hoy", + "live": "Quedan {n} días para el cierre", + "liveOne": "Mañana es el último día para entregar", + "liveZero": "Último día para entregar", + "voting": "Votaciones abiertas", + "closed": "Edición cerrada" + }, + "ctaSecondary": "Únete a la comunidad", + "ctaVote": "La votación es en Discord", + "ctaClosed": "Ver los proyectos de la comunidad", + "form": { + "url": "https://docs.google.com/forms/d/e/1FAIpQLSdsu4TfUcWHv_DsDYge1hGgZN4naQy-ju_9ettH0j98X4Dx-Q/viewform", + "cta": "Apúntate al challenge", + "note": "Abre un Google Form en otra pestaña · 4 campos, medio minuto", + "newTab": "se abre en otra pestaña", + "soonTitle": "la inscripción abre en el kickoff", + "soon": "El formulario se publica con el workshop del 7 de agosto. El enlace se anuncia en el Discord de NaN." + }, + "showcase": { + "label": "lo que ya está saliendo", + "title": "Esto es lo que está saliendo de construir así.", + "lead": "Nada de esto es nuestro. Son proyectos que ha publicado otra gente en las últimas semanas: cada tarjeta lleva a la publicación original, que es donde está el vídeo.", + "credits": "cita el Gauntlet Loop", + "foot": "Todos publicados entre el 25 de julio y el 8 de agosto de 2026.", + "items": { + "claudefare": { + "title": "Modern Claudefare", + "desc": "Un remake de Call of Duty con cuatro mapas, modo solo y multijugador, y 84.100 líneas de código. Su autor cuenta que lo levantó en unos días aplicando los principios del Gauntlet Loop." + }, + "original": { + "title": "El original", + "desc": "El shooter que empezó todo. Ni un solo asset externo: todo el código es propio." + }, + "museum": { + "title": "The Neural Museum", + "desc": "Un museo que se recorre y en el que cada cuadro es un paper de IA." + }, + "anatomy": { + "title": "Anatomy Atelier", + "desc": "Anatomía humana en 3D, órgano a órgano, dentro del navegador." + }, + "ironveil": { + "title": "Ironveil Range", + "desc": "Un simulador de defensa antimisiles, jugable." + } + } + }, + "join": { + "label": "el reto", + "title": "El reto consiste en:", + "sub": "Montar algo que esté guapo con el prompt del Gauntlet Loop, usando al menos un modelo de NaN. El tema es libre: el shooter era el experimento, no el encargo.", + "reqTitle": "requisitos", + "reqs": [ + "Usar al menos un modelo de NaN" + ], + "deliverLabel": "lo que se entrega al final", + "signupTitle": "la inscripción está siempre abierta", + "signupBody": "No hay ventana que se cierre antes: el formulario sigue abierto hasta el día del cierre. Nadie se queda fuera por llegar tarde. El calendario está para repartir el trabajo, no para levantar una puerta." + }, + "context": { + "label": "de dónde sale esto", + "title": "Alguien le pidió a un agente un Call of Duty.", + "lead": "No se lo pidió una vez. Montó un bucle: subagentes construyendo una pieza cada uno, y un crítico comparando el resultado con el juego real que no daba el visto bueno hasta que no se distinguía. Eso es un Gauntlet Loop, y es lo que desmonta el workshop que abre agosto. El reto es que cada uno monte el suyo.", + "cards": [ + { + "q": "¿Qué construyo?", + "a": "Lo que quieras. El tema es libre: el shooter era el experimento, no el encargo. Lo que se enseña es tu bucle y lo que salió de él." + }, + { + "q": "¿Quién lo organiza?", + "a": "NaN, el trampolín para builders que construyen con IA abierta. Participar pide ser miembro.", + "link": "Cómo se entra" + }, + { + "q": "¿Qué me llevo?", + "a": "Un proyecto terminado y delante de gente. Sin jurado: la comunidad vota lo entregado y los más votados lo presentan en directo." + } + ] + }, + "prompt": { + "label": "el prompt", + "title": "Este es el prompt entero. Cópialo y adáptalo.", + "lead": "Aquí está entero, con las anotaciones de lo que hace cada trozo. Cópialo y cámbialo por lo tuyo: lo que se aprovecha no es el shooter, es la forma.", + "frame": "Gauntlet Prompt", + "copy": "Copiar el prompt", + "copied": "Copiado", + "copyFail": "Selecciónalo y cópialo a mano", + "foot": "Se copia tal cual, en inglés: es el original. El tuyo no tiene por qué ser un shooter. El tema es libre.", + "notes": { + "ref": "Referencia", + "fanout": "Despliega subagentes que se encarguen individualmente de cada elemento para que quede perfecto", + "loop": "Cada subagente dentro de un loop", + "critic": "Un subagente crítico por cada agente constructor", + "harsh": "Sé muy crítico y no pares hasta que no se distinga la versión original", + "ultra": "Super saiyan 4, ultrainstinto, sharingan, encuentra el one piece" + } + }, + "idea": { + "label": "la idea clave", + "title": "Un prompt no hace el trabajo.", + "sub": "Activa un sistema autónomo que construye, observa, critica y corrige.", + "steps": [ + { + "n": "01", + "name": "Objetivo", + "body": "Una instrucción humana que dice qué tiene que existir." + }, + { + "n": "02", + "name": "Builders", + "body": "Agentes que lo construyen. Cada uno es dueño de una pieza y la entrega." + }, + { + "n": "03", + "name": "Críticos", + "body": "Agentes que revisan el trabajo con pruebas, no con impresiones." + }, + { + "n": "04", + "name": "Evidencia", + "body": "Capturas, tests, métricas. Artefactos que se pueden inspeccionar." + }, + { + "n": "05", + "name": "Iteración", + "body": "Supera la referencia: entra. No la supera: vuelve al builder." + } + ], + "quote": "La ventaja no está en pensar más, sino en cerrar el bucle con feedback observable.", + "note": "«Un prompt» es una instrucción humana inicial, no una sola llamada ni un único agente." + }, + "calendar": { + "label": "calendario", + "title": "Agosto es una temporada, no un día.", + "month": "agosto 2026", + "weekdays": [ + "Lun", + "Mar", + "Mié", + "Jue", + "Vie", + "Sáb", + "Dom" + ], + "tbd": "Día de presentaciones · por confirmar", + "short": { + "workshop": "Kickoff", + "build": "Construir", + "close": "Cierre", + "voting": "Votar" + } + }, + "submit": { + "label": "la entrega", + "title": "Lo que entregas tiene que probar qué construiste y cómo.", + "sub": "Una entrega por proyecto, editable hasta el cierre.", + "requiredLabel": "obligatorio", + "required": [ + "Nombre + descripción", + "Modelo utilizado", + "Portada 16:9", + "Arquitectura del loop", + "Repositorio", + "Intervención humana", + "Prompt inicial", + "Resultado + aprendizajes" + ], + "requiredNote": "Los dos últimos son los que prueban qué se construyó y cómo.", + "recommendedLabel": "recomendado", + "recommended": [ + { + "name": "Vídeo", + "why": "Demuestra el resultado" + }, + { + "name": "Demo", + "why": "Permite probarlo" + }, + { + "name": "Baseline vs. final", + "why": "Hace visible la mejora" + }, + { + "name": "Tokens y tiempo", + "why": "Explican el coste" + }, + { + "name": "Logs y trazas", + "why": "Permiten auditar" + }, + { + "name": "Diagrama", + "why": "Aclara la arquitectura" + } + ], + "rule": "la imagen es obligatoria · el vídeo es recomendado · la demo pública es opcional", + "notes": [ + "Repositorio: puede seguir privado durante agosto, pero tiene que abrirse al cierre, o compartirse con la organización para poder verificarlo.", + "Vídeo: no lo alojamos. Vale una URL de YouTube, Loom o similares." + ], + "whereTitle": "por dónde se entrega", + "whereBody": "El formulario de entrega aparecerá aquí más adelante." + }, + "after": { + "label": "después del cierre", + "title": "La comunidad elige quién presenta.", + "sub": "Sin jurado. Lo entregado se enseña, la comunidad vota y los proyectos más votados se presentan en directo.", + "steps": [ + { + "when": "28 ago", + "name": "Se cierra la entrada", + "body": "Se paran las inscripciones y las entregas." + }, + { + "when": "Unos días", + "name": "Votaciones de la comunidad", + "body": "Se enseña todo lo entregado y la comunidad vota." + }, + { + "when": "Por concretar", + "name": "Presentaciones", + "body": "Presentan los proyectos más votados." + } + ], + "tbdLabel": "fecha por confirmar", + "tbdBody": "Buscamos un día que le venga bien a todo el mundo, así que se anuncia en Discord en vez de fijarlo ahora.", + "note": "La votación no vive en esta página: pasa en el Discord de NaN." + }, + "close": { + "title": "Un prompt. Un sistema entero.", + "text": "Agosto en NaN es una temporada: construye, deja que te critiquen, corrige y entrega algo que se pueda probar. La inscripción está abierta hasta el 28." + } + }, "community": { "metaTitle": "Comunidad — NaN", "metaDesc": "NaN es el trampolín para builders que construyen con IA abierta. Build in public, en comunidad.", diff --git a/public/img/gauntlet/anatomy-atelier.webp b/public/img/gauntlet/anatomy-atelier.webp new file mode 100644 index 0000000..b7fa4b8 Binary files /dev/null and b/public/img/gauntlet/anatomy-atelier.webp differ diff --git a/public/img/gauntlet/ironveil-range.webp b/public/img/gauntlet/ironveil-range.webp new file mode 100644 index 0000000..4dea138 Binary files /dev/null and b/public/img/gauntlet/ironveil-range.webp differ diff --git a/public/img/gauntlet/modern-claudefare.webp b/public/img/gauntlet/modern-claudefare.webp new file mode 100644 index 0000000..934c179 Binary files /dev/null and b/public/img/gauntlet/modern-claudefare.webp differ diff --git a/public/img/gauntlet/neural-museum.webp b/public/img/gauntlet/neural-museum.webp new file mode 100644 index 0000000..6fbafba Binary files /dev/null and b/public/img/gauntlet/neural-museum.webp differ diff --git a/public/img/gauntlet/shooter-original.webp b/public/img/gauntlet/shooter-original.webp new file mode 100644 index 0000000..c671cc1 Binary files /dev/null and b/public/img/gauntlet/shooter-original.webp differ diff --git a/src/components/nan/StructuredData.astro b/src/components/nan/StructuredData.astro index 9640ccc..9bfaf69 100644 --- a/src/components/nan/StructuredData.astro +++ b/src/components/nan/StructuredData.astro @@ -12,7 +12,7 @@ import eventos from '../../data/eventos.json'; interface Props { lang: Locale; - page: 'home' | 'events' | 'other'; + page: 'home' | 'events' | 'gauntlet' | 'other'; canonical: string; origin: string; } @@ -65,6 +65,25 @@ if (page === 'events') { }); } +if (page === 'gauntlet') { + // Solo lo que la propia página afirma: nombre, fechas de construcción y que + // es un evento en línea de NaN. Ni el día de presentaciones (no tiene fecha) + // ni premios (sin definir) entran aquí. + graph.push({ + '@type': 'Event', + '@id': `${canonical}#event`, + name: 'Gauntlet Challenge', + description: t.gauntlet.metaDesc, + startDate: eventos.gauntlet.start, + endDate: eventos.gauntlet.close, + eventStatus: 'https://schema.org/EventScheduled', + eventAttendanceMode: 'https://schema.org/OnlineEventAttendanceMode', + location: { '@type': 'VirtualLocation', url: canonical }, + url: canonical, + organizer: { '@id': `${origin}/#organization` }, + }); +} + const jsonld = { '@context': 'https://schema.org', '@graph': graph }; --- diff --git a/src/data/eventos.json b/src/data/eventos.json index 65bd3e2..8cecafa 100644 --- a/src/data/eventos.json +++ b/src/data/eventos.json @@ -2,8 +2,14 @@ "agenda": [ { "date": "2026-07-31", "type": "workshop", "title": { "en": "Agent evaluation and tracing", "es": "Evaluación y trazabilidad de agentes" }, "by": "@Jasonjimnz" }, { "date": "2026-08-07", "type": "workshop", "title": { "en": "#5 NaN Workshop: Gauntlet Loop", "es": "#5 Workshop de NaN: Gauntlet Loop" }, "by": "@sahul_125" }, - { "date": "2026-08-08", "until": "2026-08-31", "type": "event", "title": "Gauntlet Challenge" } + { "date": "2026-08-08", "until": "2026-08-28", "type": "event", "title": "Gauntlet Challenge", "href": "/gauntlet" } ], + "gauntlet": { + "workshop": "2026-08-07", + "start": "2026-08-08", + "close": "2026-08-28", + "votingEnd": "2026-08-31" + }, "workshops": [ { "title": { "en": "Workshop: AI Agent Observability", "es": "Workshop: Observabilidad en Agentes de IA" }, "desc": { "en": "How to instrument AI agents so you can trace every decision, measure what is going on in production and debug when something breaks.", "es": "Cómo instrumentar agentes de IA para poder trazar cada decisión, medir qué está pasando en producción y depurar cuando algo se rompe." }, "date": "31 jul 2026", "duration": "1:33:28", "thumb": "/img/events/observabilidad-agentes.webp", "tags": ["agents", "observability", "IA"] }, { "title": "Workshop: Opencode", "date": "24 jul 2026", "duration": "1:03:11", "thumb": "/img/events/opencode.webp", "tags": ["opencode", "coding agent", "dev tools"] }, diff --git a/src/data/gauntletPrompt.ts b/src/data/gauntletPrompt.ts new file mode 100644 index 0000000..ec241b0 --- /dev/null +++ b/src/data/gauntletPrompt.ts @@ -0,0 +1,46 @@ +/** + * El prompt original del Gauntlet, el del experimento que dio pie al challenge. + * + * Vive aquí y NO en los diccionarios de i18n por dos razones: no se traduce + * —lo que se copia es el original, en inglés, tal cual se escribió— y tenerlo + * en un solo sitio evita que la versión que se lee y la que se copia acaben + * diciendo cosas distintas. La página pinta los trozos marcados y el botón + * copia `promptText()`: las dos salidas nacen de esta misma lista. + */ + +/** Los subrayados de color. Cada uno tiene su nota al margen, menos `stack`. */ +export type PromptInk = 'ref' | 'fanout' | 'loop' | 'critic' | 'harsh' | 'stack' | 'ultra'; + +export type PromptPiece = { t: string; ink?: PromptInk }; + +export const GAUNTLET_PROMPT: PromptPiece[][] = [ + [ + { t: 'I want you to build a first-person shooter at the level of the most recent ' }, + { t: 'Call of Duty games', ink: 'ref' }, + { + t: '. It should be utterly perfect, visually beautiful, with every single thing done at AAA quality—from textures to physics to anything you could think of.', + }, + ], + [ + { t: 'Fan out sub-agents and have sub-agents tackle each one individually', ink: 'fanout' }, + { t: ' so that the game is utterly perfect. You should ' }, + { t: '/loop', ink: 'loop' }, + { t: ' on each item and have a ' }, + { t: 'separate sub-agent check it visually to ensure it', ink: 'critic' }, + { t: ' looks triple A. That separate sub-agent should be a really ' }, + { t: 'harsh critic', ink: 'harsh' }, + { t: ", and if it doesn't look triple A, it should keep going." }, + ], + [ + { + t: "Don't stop until each sub-agent is utterly wowed with the quality when compared with the actual Call of Duty game. It should literally compare them side by side blind and say which one looks better. Do this in ", + }, + { t: 'ThreeJS', ink: 'stack' }, + { t: '. ' }, + { t: "/loop until it's utterly perfect. Fan out sub-agents and ultracode.", ink: 'ultra' }, + ], +]; + +/** El prompt en plano, que es lo que acaba en el portapapeles. */ +export const promptText = (paragraphs: PromptPiece[][] = GAUNTLET_PROMPT) => + paragraphs.map((p) => p.map((piece) => piece.t).join('')).join('\n\n'); diff --git a/src/data/gauntletShowcase.ts b/src/data/gauntletShowcase.ts new file mode 100644 index 0000000..1c51938 --- /dev/null +++ b/src/data/gauntletShowcase.ts @@ -0,0 +1,75 @@ +/** + * Los ejemplos de la sección 01 del Gauntlet Challenge. + * + * Son OBRA DE TERCEROS. Ninguno es de NaN, así que cada tarjeta enlaza a la + * publicación original y enseña el handle de quien la firma: la miniatura es + * el fotograma de portada del vídeo, servido desde /img/gauntlet para que + * mirar la página no dispare una sola petición a X. + * + * Aquí solo vive lo que no se traduce (enlace, imagen, autor, fecha). El + * título y la descripción están en i18n, emparejados por `id`, porque son lo + * único que cambia de idioma a idioma. + * + * `credits` marca la única publicación que nombra el Gauntlet Loop de forma + * explícita. Las demás no lo hacen, y por eso la sección se titula por lo que + * está saliendo y no por la técnica: decir que las cinco salen del bucle sería + * afirmar algo que sus autores no dicen. + */ +export interface ShowcaseShot { + /** Empareja con la entrada del mismo `id` en `gauntlet.showcase.items`. */ + id: string; + /** Handle de X sin arroba, tal y como lo escribe su dueño. */ + author: string; + /** La publicación original. Es el único destino de la tarjeta. */ + href: string; + /** Fotograma de portada, 640x360, copiado a /public en el repo. */ + img: string; + /** Fecha de publicación, ya formateada: es la misma en los dos idiomas. */ + date: string; + /** Cierto solo si el autor nombra el Gauntlet Loop en su publicación. */ + credits?: true; +} + +/** + * El primero es el destacado y ocupa el ancho entero: es el único que cita el + * bucle por su nombre, y además es un remake de Call of Duty, que es justo lo + * que cuenta la sección siguiente. + */ +export const GAUNTLET_SHOWCASE: readonly ShowcaseShot[] = [ + { + id: 'claudefare', + author: '0xRishi', + href: 'https://x.com/0xRishi/status/2084322235788226653', + img: '/img/gauntlet/modern-claudefare.webp', + date: '3 ago 2026', + credits: true, + }, + { + id: 'original', + author: 'mattshumer_', + href: 'https://x.com/mattshumer_/status/2081054356405731740', + img: '/img/gauntlet/shooter-original.webp', + date: '25 jul 2026', + }, + { + id: 'museum', + author: 'chetaslua', + href: 'https://x.com/chetaslua/status/2085982035072713012', + img: '/img/gauntlet/neural-museum.webp', + date: '8 ago 2026', + }, + { + id: 'anatomy', + author: 'thebuggeddev', + href: 'https://x.com/thebuggeddev/status/2083884856531177942', + img: '/img/gauntlet/anatomy-atelier.webp', + date: '2 ago 2026', + }, + { + id: 'ironveil', + author: 'ChrisGPT', + href: 'https://x.com/ChrisGPT/status/2085889688397570315', + img: '/img/gauntlet/ironveil-range.webp', + date: '8 ago 2026', + }, +]; diff --git a/src/lib/agenda.ts b/src/lib/agenda.ts index 8e37709..7b5e340 100644 --- a/src/lib/agenda.ts +++ b/src/lib/agenda.ts @@ -21,6 +21,12 @@ export type AgendaItem = { type: string; title: Localized; by?: string; + /** + * Ruta interna con la landing del evento, sin prefijo de idioma: la fila la + * pinta con `withLang`. Opcional a propósito — la mayoría de entradas no + * tienen página propia y siguen siendo texto. + */ + href?: string; }; export const loc = (v: Localized, lang: Locale): string => (typeof v === 'string' ? v : v[lang]); diff --git a/src/lib/gauntlet.ts b/src/lib/gauntlet.ts new file mode 100644 index 0000000..52de040 --- /dev/null +++ b/src/lib/gauntlet.ts @@ -0,0 +1,140 @@ +/** + * En qué momento del Gauntlet Challenge estamos. + * + * Vive aquí y no en la página por lo mismo que `agenda.ts`: es aritmética de + * fechas, se equivoca en silencio (un día de más y el CTA de inscripción + * desaparece mientras el formulario sigue abierto) y con el reloj como + * parámetro se prueba sin depender de cuándo se ejecute el test. + * + * Las fechas NO se cablean aquí: vienen de `data/eventos.json`, porque el fin + * de la votación todavía no está cerrado ("unos días" desde el 28) y tiene que + * poder cambiarse sin tocar código. + * + * Importante: la fase solo cambia texto y enlaces. No habilita ni deshabilita + * nada, porque no hay nada que habilitar — la inscripción es un Google Form + * externo. Es deliberado: el reloj del Worker es UTC y cerca de medianoche en + * Canarias o LATAM va un día adelantado, así que un cálculo de fase nunca puede + * ser lo que deje a alguien fuera. + */ + +import { parseISO, fmtISO } from './agenda'; + +export type GauntletDates = { + /** Workshop de kickoff. */ + workshop: string; + /** Primer día para construir. */ + start: string; + /** Último día para inscribirse y entregar, inclusivo. */ + close: string; + /** Último día de votación, inclusivo. Provisional hasta que se cierre. */ + votingEnd: string; +}; + +export type GauntletPhase = 'pre' | 'live' | 'voting' | 'closed'; + +export type GauntletState = { + phase: GauntletPhase; + /** + * Días que faltan para el hito de la fase: para el arranque en `pre`, para + * el cierre en `live`. En `voting` y `closed` no hay cuenta atrás (la + * votación no tiene final anunciado), así que es 0. + */ + days: number; +}; + +const DAY = 86_400_000; + +/** Días completos entre dos ISO, positivo si `to` es posterior. */ +export const daysBetween = (from: string, to: string): number => + Math.round((parseISO(to).getTime() - parseISO(from).getTime()) / DAY); + +export function gauntletState(dates: GauntletDates, today: Date): GauntletState { + const iso = fmtISO(today); + + if (iso < dates.start) return { phase: 'pre', days: daysBetween(iso, dates.start) }; + // El día del cierre sigue siendo `live`: se entrega hasta el final del día. + if (iso <= dates.close) return { phase: 'live', days: daysBetween(iso, dates.close) }; + if (iso <= dates.votingEnd) return { phase: 'voting', days: 0 }; + return { phase: 'closed', days: 0 }; +} + +/** Qué pasa cada día del mes en la rejilla del calendario. */ +export type DayKind = 'idle' | 'workshop' | 'build' | 'close' | 'voting'; + +export type CalendarCell = { day: number; iso: string; kind: DayKind }; + +export type CalendarMonth = { + /** Huecos vacíos antes del día 1, con la semana empezando en lunes. */ + lead: number; + cells: CalendarCell[]; +}; + +/** ISO de un día concreto sin pasar por Date, que ya trae bastantes zonas. */ +const isoOf = (year: number, month: number, day: number) => + `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + +/** + * La rejilla del mes en el que se construye, marcada con lo que pasa cada día. + * + * El mes sale de `dates.start`, no de un literal: si la edición se mueve a + * septiembre, el calendario se mueve con ella. Los días fuera de las ventanas + * (antes del workshop, después de la votación) quedan en `idle` — se pintan, + * pero apagados, para que el mes se lea entero y no solo el trozo activo. + */ +export function calendarMonth(dates: GauntletDates): CalendarMonth { + const [year, month] = dates.start.split('-').map(Number); + const days = new Date(year, month, 0).getDate(); + // getDay() cuenta desde el domingo; aquí la semana empieza en lunes. + const lead = (new Date(year, month - 1, 1).getDay() + 6) % 7; + + const cells = Array.from({ length: days }, (_, i) => { + const day = i + 1; + const iso = isoOf(year, month, day); + const kind: DayKind = + iso === dates.workshop + ? 'workshop' + : iso === dates.close + ? 'close' + : iso >= dates.start && iso < dates.close + ? 'build' + : iso > dates.close && iso <= dates.votingEnd + ? 'voting' + : 'idle'; + return { day, iso, kind }; + }); + + return { lead, cells }; +} + +/** + * El texto de estado del hero. Hoy y mañana tienen frase propia porque + * "empieza en 0 días" no lo dice nadie. + */ +export function statusText( + state: GauntletState, + strings: { + pre: string; + preOne: string; + preZero: string; + live: string; + liveOne: string; + liveZero: string; + voting: string; + closed: string; + }, +): string { + switch (state.phase) { + case 'pre': + if (state.days === 0) return strings.preZero; + if (state.days === 1) return strings.preOne; + return strings.pre.replace('{n}', String(state.days)); + case 'live': + if (state.days === 0) return strings.liveZero; + if (state.days === 1) return strings.liveOne; + return strings.live.replace('{n}', String(state.days)); + case 'voting': + return strings.voting; + default: + return strings.closed; + } +} diff --git a/src/pages/_events.astro b/src/pages/_events.astro index 54487ff..7c152f3 100644 --- a/src/pages/_events.astro +++ b/src/pages/_events.astro @@ -1,7 +1,7 @@ --- import NanPage from '../layouts/NanPage.astro'; import eventos from '../data/eventos.json'; -import { getLang, useT } from '../lib/i18n'; +import { getLang, useT, withLang } from '../lib/i18n'; import { calendarDays, loc as resolve, @@ -79,7 +79,13 @@ const hack = eventos.hackathons[0];
{tt.heroEyebrow}
+{tt.heroSub}
+ +{tt.form.note}
} +{tt.showcase.lead}
+ + + +{tt.showcase.foot}
+{tt.join.sub}
+ +{tt.join.reqTitle}
+{tt.join.deliverLabel}
+{tt.submit.title} {tt.submit.sub}
+ + {/* Los dos últimos van destacados: en la fuente son los que prueban qué se + construyó y cómo, y por eso cierran la rejilla. */} +{tt.submit.requiredNote}
+ +{tt.submit.recommendedLabel}
+{tt.submit.rule}
+ +{tt.submit.whereTitle}
+{tt.submit.whereBody}
+{tt.join.signupTitle}
+{tt.join.signupBody}
+ + {formUrl ? ( + <> + + {tt.form.cta} → + +{tt.form.note}
+ > + ) : ( +{tt.form.soonTitle}: {tt.form.soon}
+ )} +{tt.context.lead}
+ +{c.a}
+ {/* Solo la tarjeta de la comunidad tiene a dónde ir. Las otras dos + se responden aquí mismo y no mandan a nadie a ningún sitio. */} + {'link' in c && {c.link} →} +{tt.prompt.lead}
+ + {/* El prompt anotado a mano, como la captura de la que salió: el texto en + la columna del medio y las lecturas de cada trozo subrayado en los + márgenes. Cada párrafo es una fila propia para que su nota caiga a su + altura sin posicionar nada en absoluto; en móvil las notas se apilan + debajo del párrafo que anotan, que es donde tienen sentido. */} +{tt.prompt.frame}
+ +{P[0].map((s) => (s.ink ? {s.t} : s.t))}
+{tt.prompt.notes.ref}
+{tt.prompt.notes.loop}
+{tt.prompt.notes.critic}
+{P[1].map((s) => (s.ink ? {s.t} : s.t))}
+{tt.prompt.notes.fanout}
+{tt.prompt.notes.harsh}
+{P[2].map((s) => (s.ink ? {s.t} : s.t))}
+{tt.prompt.notes.ultra}
+{tt.prompt.foot}
+{tt.idea.sub}
+ +{s.body}
+{tt.idea.quote}
+{tt.idea.note}
+{tt.calendar.month}
+ +{tt.calendar.tbd}
+{tt.after.sub}
+ +{s.body}
+{tt.after.tbdLabel}
+{tt.after.tbdBody}
+{tt.after.note}
+ {tt.ctaVote} → +