diff --git a/public/css/themes/h2g2.css b/public/css/themes/h2g2.css new file mode 100644 index 0000000..a8e9824 --- /dev/null +++ b/public/css/themes/h2g2.css @@ -0,0 +1,228 @@ +/* + * Hitchhiker's Guide to the Galaxy Theme + * DON'T PANIC - Hidden theme activated via easter egg + * Inspired by the book covers: deep space blue with warm friendly orange lettering + */ + +/* ========== DARK MODE (default) ========== */ +:root { + /* === Core Palette === */ + --palette-panic-orange: #FF9F1C; + --palette-panic-gold: #FFBF69; + --palette-towel-brown: #A67C52; + --palette-space-deep: #0D1B2A; + --palette-space-mid: #1B263B; + --palette-space-light: #243B53; + --palette-space-lighter: #334E68; + --palette-space-border: #486581; + --palette-starlight: #F0F4F8; + --palette-starlight-muted: #9FB3C8; + --palette-improbability-purple: #7B68EE; + --palette-babel-fish-yellow: #FFE066; + + /* === Semantic: Backgrounds === */ + --bg: var(--palette-space-deep); + --bg-secondary: var(--palette-space-mid); + --bg-tertiary: var(--palette-space-lighter); + --surface: var(--palette-space-light); + --input-bg: var(--palette-space-mid); + + /* === Semantic: Text === */ + --text: var(--palette-starlight); + --text-secondary: var(--palette-starlight-muted); + + /* === Semantic: Accent === */ + --accent: var(--palette-panic-orange); + --accent-light: var(--palette-panic-gold); + --accent-secondary: var(--palette-improbability-purple); + + /* === Semantic: Status === */ + --danger: #E63946; + --success: #06D6A0; + --warning: var(--palette-panic-orange); + + /* === Semantic: Borders === */ + --border: var(--palette-space-border); + --glass-bg: rgba(27, 38, 59, 0.9); + --glass-border: rgba(255, 159, 28, 0.15); + + /* === Semantic: Message Bubbles === */ + --user-bubble: var(--palette-panic-orange); + --user-bubble-end: #E8890C; + --assistant-bubble: var(--palette-space-mid); + + /* === Semantic: List View === */ + --scope-header-color: var(--palette-panic-orange); + --card-border: rgba(255, 159, 28, 0.4); + --pinned-border: rgba(255, 159, 28, 0.6); + --pinned-bg: rgba(255, 159, 28, 0.1); + + /* === Alpha Variants === */ + --accent-alpha-8: rgba(255, 159, 28, 0.08); + --accent-alpha-12: rgba(255, 159, 28, 0.12); + --accent-alpha-15: rgba(255, 159, 28, 0.15); + --accent-alpha-20: rgba(255, 159, 28, 0.2); + --accent-alpha-25: rgba(255, 159, 28, 0.25); + --accent-alpha-30: rgba(255, 159, 28, 0.3); + --accent-alpha-35: rgba(255, 159, 28, 0.35); + --accent-alpha-50: rgba(255, 159, 28, 0.5); + + --danger-alpha-15: rgba(230, 57, 70, 0.15); + --danger-alpha-40: rgba(230, 57, 70, 0.4); + + --teal-alpha-6: rgba(6, 214, 160, 0.06); + --teal-alpha-25: rgba(6, 214, 160, 0.25); + + --terracotta-alpha-6: rgba(255, 159, 28, 0.06); + --terracotta-alpha-10: rgba(255, 159, 28, 0.1); + + --white-alpha-6: rgba(255, 255, 255, 0.06); + --white-alpha-10: rgba(255, 255, 255, 0.1); + --white-alpha-60: rgba(255, 255, 255, 0.6); + + --black-alpha-8: rgba(0, 0, 0, 0.08); + --black-alpha-15: rgba(0, 0, 0, 0.15); + --black-alpha-20: rgba(0, 0, 0, 0.2); + --black-alpha-25: rgba(0, 0, 0, 0.25); + --black-alpha-30: rgba(0, 0, 0, 0.3); + --black-alpha-40: rgba(0, 0, 0, 0.4); + --black-alpha-50: rgba(0, 0, 0, 0.5); + + /* === Component: Status Indicators === */ + --status-idle: var(--palette-panic-gold); + --status-thinking: var(--palette-panic-orange); + --status-disconnected: var(--palette-space-border); + + /* === Component: Mode Badges === */ + --mode-autopilot-bg: var(--teal-alpha-25); + --mode-autopilot-text: #06D6A0; + --mode-readonly-bg: var(--accent-alpha-20); + --mode-readonly-text: var(--palette-panic-orange); + + /* === Component: Syntax Highlighting === */ + --syntax-keyword: var(--palette-panic-orange); + --syntax-string: var(--palette-babel-fish-yellow); + --syntax-number: #E63946; + --syntax-comment: #627D98; + --syntax-function: var(--palette-panic-gold); + --syntax-type: var(--palette-improbability-purple); + --syntax-variable: var(--palette-towel-brown); + --syntax-operator: var(--palette-starlight-muted); + + /* === Component: Code Blocks === */ + --code-bg: var(--bg); + --code-header-bg: var(--bg-tertiary); + --code-inline-bg: var(--accent-alpha-15); + + /* === Component: Gradient (list view background) === */ + --mesh-color-1: rgba(255, 159, 28, 0.08); + --mesh-color-2: rgba(123, 104, 238, 0.06); + --mesh-color-3: rgba(166, 124, 82, 0.04); + + /* === Component: Brand === */ + --brand-icon-color: var(--palette-panic-orange); + --brand-gradient: linear-gradient(135deg, var(--palette-panic-orange) 0%, var(--palette-improbability-purple) 100%); + + /* === PWA: Theme Colors === */ + --theme-color: #0D1B2A; +} + +/* ========== LIGHT MODE ========== */ +html[data-theme="light"] { + --palette-space-deep: #F0F4F8; + --palette-space-mid: #D9E2EC; + --palette-space-light: #FFFFFF; + --palette-space-lighter: #BCCCDC; + --palette-space-border: #9FB3C8; + --palette-starlight: #102A43; + --palette-starlight-muted: #486581; + --palette-panic-orange: #E8890C; + + --bg: #F0F4F8; + --bg-secondary: #D9E2EC; + --bg-tertiary: #BCCCDC; + --surface: #FFFFFF; + --input-bg: #FFFFFF; + --text: #102A43; + --text-secondary: #486581; + --border: #9FB3C8; + --glass-bg: rgba(255, 255, 255, 0.9); + --glass-border: rgba(232, 137, 12, 0.15); + --assistant-bubble: #F0F4F8; + + --scope-header-color: #D97706; + --card-border: rgba(217, 119, 6, 0.4); + --pinned-border: rgba(217, 119, 6, 0.6); + --pinned-bg: rgba(217, 119, 6, 0.1); + + --white-alpha-6: rgba(0, 0, 0, 0.03); + --white-alpha-10: rgba(0, 0, 0, 0.06); + --code-bg: #D9E2EC; + --code-header-bg: #BCCCDC; + --code-inline-bg: rgba(232, 137, 12, 0.1); + --mesh-color-1: rgba(217, 119, 6, 0.12); + --mesh-color-2: rgba(123, 104, 238, 0.04); + --mesh-color-3: rgba(166, 124, 82, 0.03); + + --syntax-keyword: #B45309; + --syntax-string: #92400E; + --syntax-number: #BE123C; + --syntax-comment: #64748B; + --syntax-function: #D97706; + --syntax-type: #6D28D9; + --syntax-variable: #78350F; + --syntax-operator: #486581; + + --scrollbar-thumb: rgba(0, 0, 0, 0.15); + --theme-color: #F0F4F8; +} + +/* Auto theme via media query */ +@media (prefers-color-scheme: light) { + html:not([data-theme="dark"]):not([data-theme="light"]) { + --palette-space-deep: #F0F4F8; + --palette-space-mid: #D9E2EC; + --palette-space-light: #FFFFFF; + --palette-space-lighter: #BCCCDC; + --palette-space-border: #9FB3C8; + --palette-starlight: #102A43; + --palette-starlight-muted: #486581; + --palette-panic-orange: #E8890C; + + --bg: #F0F4F8; + --bg-secondary: #D9E2EC; + --bg-tertiary: #BCCCDC; + --surface: #FFFFFF; + --input-bg: #FFFFFF; + --text: #102A43; + --text-secondary: #486581; + --border: #9FB3C8; + --glass-bg: rgba(255, 255, 255, 0.9); + --glass-border: rgba(232, 137, 12, 0.15); + --assistant-bubble: #F0F4F8; + + --scope-header-color: #D97706; + --card-border: rgba(217, 119, 6, 0.4); + --pinned-border: rgba(217, 119, 6, 0.6); + --pinned-bg: rgba(217, 119, 6, 0.1); + + --white-alpha-6: rgba(0, 0, 0, 0.03); + --white-alpha-10: rgba(0, 0, 0, 0.06); + --code-bg: #D9E2EC; + --code-header-bg: #BCCCDC; + --code-inline-bg: rgba(232, 137, 12, 0.1); + --mesh-color-1: rgba(217, 119, 6, 0.12); + --mesh-color-2: rgba(123, 104, 238, 0.04); + --mesh-color-3: rgba(166, 124, 82, 0.03); + --syntax-keyword: #B45309; + --syntax-string: #92400E; + --syntax-number: #BE123C; + --syntax-comment: #64748B; + --syntax-function: #D97706; + --syntax-type: #6D28D9; + --syntax-variable: #78350F; + --syntax-operator: #486581; + --scrollbar-thumb: rgba(0, 0, 0, 0.15); + --theme-color: #F0F4F8; + } +} diff --git a/public/js/ui.js b/public/js/ui.js index 6408cd6..20b8548 100644 --- a/public/js/ui.js +++ b/public/js/ui.js @@ -248,16 +248,18 @@ const bellQuotes = { "It's possible I may wet the bed. I'm a very anxious person.", ], aquatic: [ - "This is an adventure.", + "Now if you'll excuse me, I'm going to go on an overnight drunk.", "I wonder if it remembers me.", - "Let me tell you about my boat.", - "Out here, we're all equals.", + "Don't point that gun at him, he's an unpaid intern.", + "Son of a bitch, I'm sick of these dolphins.", "Be still, Cody.", + "I'm going to find it and I'm going to destroy it.", "We're in the middle of a lightning strike rescue.", - "I'm right on top of that.", - "This is supposed to be a happy occasion!", + "This is supposed to be a happy occasion. Let's not blow it.", "That's an endangered species at most.", - "You know I'm not good with those things.", + "We're being led on an illegal suicide mission by a selfish maniac.", + "Out here, we're all equals.", + "Let me tell you about my boat.", ], monokai: [ "Hello, World!", @@ -278,10 +280,13 @@ const bellQuotes = { "Time for a nap...", "*blinks slowly*", "Cozy vibes only.", - "*curls up*", + "In ancient times cats were worshipped as gods; they have not forgotten this.", "Warm and fuzzy.", - "*kneads blanket*", + "I'm not sleeping, I'm debugging with my eyes closed.", "Purrfect.", + "Cats are connoisseurs of comfort.", + "The smallest feline is a masterpiece.", + "*knocks things off desk*", ], fjord: [ "Velkommen.", @@ -295,6 +300,34 @@ const bellQuotes = { "Stay cozy.", "The fjords await.", ], + paper: [ + "The first draft of anything is shit.", + "Start writing, no matter what. The water does not flow until the faucet is turned on.", + "A word after a word after a word is power.", + "The scariest moment is always just before you start.", + "You can always edit a bad page. You can't edit a blank page.", + "Write drunk, edit sober.", + "There is nothing to writing. All you do is sit down at a typewriter and bleed.", + "The secret of getting ahead is getting started.", + "Fill your paper with the breathings of your heart.", + "Either write something worth reading or do something worth writing.", + "A writer is someone for whom writing is more difficult than it is for other people.", + "Tomorrow may be hell, but today was a good writing day, and on the good writing days nothing else matters.", + ], + h2g2: [ + "Don't Panic.", + "The Answer to the Ultimate Question of Life, the Universe, and Everything is 42.", + "Time is an illusion. Lunchtime doubly so.", + "So long, and thanks for all the fish.", + "A towel is about the most massively useful thing an interstellar hitchhiker can have.", + "In the beginning the Universe was created. This has made a lot of people very angry.", + "I love deadlines. I love the whooshing noise they make as they go by.", + "Anyone who is capable of getting themselves made President should on no account be allowed to do the job.", + "The ships hung in the sky in much the same way that bricks don't.", + "For a moment, nothing happened. Then, after a second or so, nothing continued to happen.", + "Would it save you a lot of time if I just gave up and went mad now?", + "Space is big. Really big. You just won't believe how vastly, hugely, mind-bogglingly big it is.", + ], }; // Bell ring handler @@ -613,6 +646,31 @@ export function openNewChatModal(cwd = '') { // Thank you easter egg - check for gratitude and show hearts const THANK_YOU_PATTERNS = /\b(thanks?|thank\s*you|thx|ty|tysm|thank\s*u|cheers|gracias|merci|danke|arigatou?|grazie)\b/i; + +// Hitchhiker's Guide easter eggs +const DONT_PANIC_PATTERN = /\bdon['']?t\s*panic\b/i; + +// Marvin the Paranoid Android - triggers on frustration/sadness +const MARVIN_TRIGGERS = { + frustration: /\b(ugh+|argh+|grr+|ffs|wtf|smh|doesn['']?t\s*work|won['']?t\s*work|not\s*working|why\s*(won['']?t|doesn['']?t|isn['']?t|can['']?t)|can['']?t\s*figure|so\s*(annoying|frustrated)|this\s*is\s*broken|hate\s*this|sick\s*of|tired\s*of)\b/i, + sadness: /\b(depressed|miserable|hopeless|awful|terrible|horrible|devastated|give\s*up|giving\s*up|want\s*to\s*cry|can['']?t\s*do\s*this|what['']?s\s*the\s*point)\b/i, + profanity: /\b(fuck(ing|ed)?|shit(ty)?|damn(it)?|crap|hell|bastard|bitch|ass(hole)?)\b/i +}; + +const MARVIN_QUOTES = [ + "Life? Don't talk to me about life.", + "I think you ought to know I'm feeling very depressed.", + "Here I am, brain the size of a planet, and they ask me to pick up a piece of paper.", + "I'd make a suggestion, but you wouldn't listen. No one ever does.", + "The first ten million years were the worst. And the second ten million... they were the worst too.", + "Do you want me to sit in a corner and rust, or just fall apart where I'm standing?", + "Pardon me for breathing, which I never do anyway.", + "I have a million ideas. They all point to certain death.", + "I've been talking to the ship's computer. It hates me.", + "I'm not getting you down at all, am I?", + "My capacity for happiness you could fit into a matchbox without taking out the matches first.", + "I'm at a rough estimate thirty billion times more intelligent than you. Let me give you an example.", +]; const COPY_ICON_SVG = ''; function buildUserMessageActionButtons() { @@ -653,6 +711,443 @@ function triggerHeartsAnimation() { } } +// /dance command - make the UI wiggle +function triggerDanceMode() { + haptic(30); + const chatView = document.getElementById('chat-view'); + if (!chatView) return; + + // Add dance animation + if (!document.getElementById('dance-style')) { + const style = document.createElement('style'); + style.id = 'dance-style'; + style.textContent = ` + @keyframes ui-dance { + 0%, 100% { transform: rotate(0deg) scale(1); } + 10% { transform: rotate(-1deg) scale(1.01); } + 20% { transform: rotate(1deg) scale(0.99); } + 30% { transform: rotate(-0.5deg) scale(1.02); } + 40% { transform: rotate(0.5deg) scale(1); } + 50% { transform: rotate(-1deg) scale(1.01); } + 60% { transform: rotate(1deg) scale(0.99); } + 70% { transform: rotate(-0.5deg) scale(1); } + 80% { transform: rotate(0.5deg) scale(1.01); } + 90% { transform: rotate(-0.5deg) scale(1); } + } + .dancing { animation: ui-dance 0.8s ease-in-out 3; } + `; + document.head.appendChild(style); + } + + chatView.classList.add('dancing'); + setTimeout(() => chatView.classList.remove('dancing'), 2500); + showToast('๐Ÿ’ƒ๐Ÿ•บ'); +} + +// /matrix command - green falling code rain +function triggerMatrixMode() { + haptic(30); + + // Create canvas overlay + const canvas = document.createElement('canvas'); + canvas.id = 'matrix-canvas'; + canvas.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 9999; + opacity: 0.9; + `; + document.body.appendChild(canvas); + + const ctx = canvas.getContext('2d'); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + const chars = 'ใ‚ขใ‚คใ‚ฆใ‚จใ‚ชใ‚ซใ‚ญใ‚ฏใ‚ฑใ‚ณใ‚ตใ‚ทใ‚นใ‚ปใ‚ฝใ‚ฟใƒใƒ„ใƒ†ใƒˆใƒŠใƒ‹ใƒŒใƒใƒŽใƒใƒ’ใƒ•ใƒ˜ใƒ›ใƒžใƒŸใƒ ใƒกใƒขใƒคใƒฆใƒจใƒฉใƒชใƒซใƒฌใƒญใƒฏใƒฒใƒณ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const fontSize = 14; + const columns = Math.floor(canvas.width / fontSize); + const rows = Math.floor(canvas.height / fontSize); + // Randomize initial positions so columns don't all start together + const drops = Array(columns).fill(0).map(() => Math.floor(Math.random() * -rows)); + + let frameCount = 0; + const maxFrames = 300; // ~5 seconds at 60fps + + function draw() { + ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = '#0f0'; + ctx.font = `${fontSize}px monospace`; + + for (let i = 0; i < drops.length; i++) { + // Only draw when on screen (drops[i] > 0) + if (drops[i] > 0) { + const char = chars[Math.floor(Math.random() * chars.length)]; + ctx.fillText(char, i * fontSize, drops[i] * fontSize); + } + + if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) { + drops[i] = 0; + } + drops[i]++; + } + + frameCount++; + if (frameCount < maxFrames) { + requestAnimationFrame(draw); + } else { + // Fade out + canvas.style.transition = 'opacity 0.5s'; + canvas.style.opacity = '0'; + setTimeout(() => canvas.remove(), 500); + } + } + + draw(); + showToast('๐Ÿ‡ Follow the white rabbit...'); +} + +// 42 characters - Hitchhiker's Guide reference +function triggerHitchhikersEgg() { + haptic(20); + showToast('๐ŸŒŒ The Answer to the Ultimate Question of Life, the Universe, and Everything'); +} + +// Don't Panic - large friendly letters +function triggerDontPanic() { + haptic(20); + + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: #1a1a2e; + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; + `; + overlay.innerHTML = ` +
DON'T PANIC
+ `; + document.body.appendChild(overlay); + + requestAnimationFrame(() => { + overlay.style.opacity = '1'; + setTimeout(() => { + overlay.style.opacity = '0'; + setTimeout(() => overlay.remove(), 300); + }, 1500); + }); +} + +// Marvin quotes on frustration/sadness (30% chance, 100% in h2g2 mode) +function maybeShowMarvinQuote(text) { + const isTriggered = + MARVIN_TRIGGERS.frustration.test(text) || + MARVIN_TRIGGERS.sadness.test(text) || + MARVIN_TRIGGERS.profanity.test(text); + + // 100% chance in h2g2 mode, 30% otherwise + const chance = h2g2Mode ? 1.0 : 0.3; + if (isTriggered && Math.random() < chance) { + const quote = MARVIN_QUOTES[Math.floor(Math.random() * MARVIN_QUOTES.length)]; + showToast(`๐Ÿค– "${quote}"`, { duration: 5000 }); + } +} + +// --- Hitchhiker's Guide to the Galaxy Mode --- +let h2g2Mode = false; +let previousTheme = null; + +function activateH2G2Mode(showAnimation = true) { + if (h2g2Mode) return; + h2g2Mode = true; + + // Save current theme and switch to h2g2 + const themeLink = document.getElementById('color-theme-link'); + if (themeLink) { + previousTheme = themeLink.href; + themeLink.href = '/css/themes/h2g2.css'; + } + + // Add subtle 42 to header + const brand = document.querySelector('.brand h1'); + if (brand && !document.getElementById('h2g2-42')) { + const fortyTwo = document.createElement('span'); + fortyTwo.id = 'h2g2-42'; + fortyTwo.textContent = '42'; + fortyTwo.style.cssText = ` + font-size: 0.5em; + opacity: 0.4; + margin-left: 0.5em; + font-weight: normal; + `; + brand.appendChild(fortyTwo); + } + + // Update empty state text + const emptyText = document.querySelector('.chat-empty-text'); + if (emptyText) { + emptyText.dataset.originalText = emptyText.textContent; + emptyText.textContent = 'Mostly Harmless'; + } + + // Add "Don't Panic" to typing indicator + const typingIndicator = document.getElementById('typing-indicator'); + if (typingIndicator && !document.getElementById('h2g2-dont-panic')) { + const dontPanic = document.createElement('span'); + dontPanic.id = 'h2g2-dont-panic'; + dontPanic.textContent = "DON'T PANIC"; + dontPanic.style.cssText = ` + margin-left: 0.5em; + font-weight: bold; + color: var(--accent); + font-size: 0.85em; + letter-spacing: 0.05em; + `; + typingIndicator.appendChild(dontPanic); + } + + if (showAnimation) { + showToast("๐Ÿš€ Hitchhiker's Mode Activated โ€” DON'T PANIC", { duration: 3000 }); + } +} + +function deactivateH2G2Mode() { + if (!h2g2Mode) return; + h2g2Mode = false; + + // Remove 42 from header + const fortyTwo = document.getElementById('h2g2-42'); + if (fortyTwo) fortyTwo.remove(); + + // Restore empty state text + const emptyText = document.querySelector('.chat-empty-text'); + if (emptyText && emptyText.dataset.originalText) { + emptyText.textContent = emptyText.dataset.originalText; + } + + // Remove "Don't Panic" from typing indicator + const dontPanic = document.getElementById('h2g2-dont-panic'); + if (dontPanic) dontPanic.remove(); + + previousTheme = null; +} + +// Whale and petunias falling animation - position-based for all screen sizes +function triggerImprobabilityDrive() { + haptic(50); + + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: linear-gradient(to bottom, #0D1B2A 0%, #1B263B 100%); + z-index: 9999; + overflow: hidden; + `; + document.body.appendChild(overlay); + + const whaleThoughts = [ + "What's happening?", + "Who am I?", + "Why am I here?", + "What's my purpose?", + "What's that thing rushing towards me?", + "I wonder if it will be friends with me?", + "I shall call it... ground!", + ]; + + const screenHeight = window.innerHeight; + const fallSpeed = 70; // pixels per second - slow enough to read all thoughts + const startY = -100; + const endY = screenHeight + 150; + + // Calculate trigger points for thoughts based on VISIBLE screen area only + // Whale is visible from Y=0 to Y=screenHeight-50 (accounting for emoji size) + // Distribute all 7 thoughts across 5% to 75% of visible screen height + const visibleStart = 0; + const visibleEnd = screenHeight * 0.75; // Last thought at 75% down screen + const visibleRange = visibleEnd - visibleStart; + + const thoughtTriggers = whaleThoughts.map((_, i) => + visibleStart + (visibleRange * i / (whaleThoughts.length - 1)) + ); + const petuniasThoughtTrigger = screenHeight * 0.4; // Show at 40% of screen + + // Create whale + const whale = document.createElement('div'); + whale.innerHTML = '๐Ÿ‹'; + whale.style.cssText = ` + position: absolute; + font-size: clamp(3rem, 8vw, 5rem); + left: 25%; + top: ${startY}px; + `; + overlay.appendChild(whale); + + // Create thought bubble for whale + const thought = document.createElement('div'); + thought.style.cssText = ` + position: absolute; + left: calc(25% + clamp(60px, 12vw, 100px)); + top: ${startY}px; + background: white; + color: #333; + padding: 8px 16px; + border-radius: 20px; + font-size: clamp(0.8rem, 2.5vw, 1rem); + max-width: min(200px, 45vw); + opacity: 0; + box-shadow: 0 2px 10px rgba(0,0,0,0.3); + transition: opacity 0.2s; + `; + overlay.appendChild(thought); + + // Create petunias + const petunias = document.createElement('div'); + petunias.innerHTML = '๐ŸŒธ'; + petunias.style.cssText = ` + position: absolute; + font-size: clamp(2rem, 5vw, 3rem); + left: 65%; + top: ${startY - 50}px; + `; + overlay.appendChild(petunias); + + // Petunias thought + const petuniasThought = document.createElement('div'); + petuniasThought.textContent = 'Oh no, not again.'; + petuniasThought.style.cssText = ` + position: absolute; + left: calc(65% + clamp(40px, 8vw, 60px)); + top: ${startY - 50}px; + background: white; + color: #333; + padding: 8px 16px; + border-radius: 20px; + font-size: clamp(0.75rem, 2vw, 0.9rem); + opacity: 0; + box-shadow: 0 2px 10px rgba(0,0,0,0.3); + font-style: italic; + transition: opacity 0.2s; + `; + overlay.appendChild(petuniasThought); + + // Animation state + let whaleY = startY; + let petuniasY = startY - 50; + let currentThoughtIndex = -1; + let petuniasThoughtShown = false; + let lastTime = performance.now(); + let animationDone = false; + + function animate(currentTime) { + if (animationDone) return; + + const deltaTime = (currentTime - lastTime) / 1000; // seconds + lastTime = currentTime; + + // Update positions + whaleY += fallSpeed * deltaTime; + petuniasY += fallSpeed * 1.1 * deltaTime; // Petunias fall slightly faster + + whale.style.top = `${whaleY}px`; + thought.style.top = `${whaleY}px`; + petunias.style.top = `${petuniasY}px`; + petuniasThought.style.top = `${petuniasY}px`; + + // Check for thought triggers + for (let i = currentThoughtIndex + 1; i < whaleThoughts.length; i++) { + if (whaleY >= thoughtTriggers[i]) { + currentThoughtIndex = i; + thought.textContent = whaleThoughts[i]; + thought.style.opacity = '1'; + // Fade slightly after showing + setTimeout(() => { + if (thought.textContent === whaleThoughts[i]) { + thought.style.opacity = '0.85'; + } + }, 400); + break; + } + } + + // Check for petunias thought trigger + if (!petuniasThoughtShown && petuniasY >= petuniasThoughtTrigger) { + petuniasThoughtShown = true; + petuniasThought.style.opacity = '1'; + } + + // Check if whale has fallen off screen + if (whaleY >= endY) { + animationDone = true; + showFinalScreen(); + return; + } + + requestAnimationFrame(animate); + } + + function showFinalScreen() { + // Flash and activate h2g2 mode + overlay.style.transition = 'background 0.3s'; + overlay.style.background = '#fff'; + setTimeout(() => { + overlay.style.background = '#0D1B2A'; + overlay.innerHTML = ` +
+
INFINITE IMPROBABILITY DRIVE
+
Hitchhiker's Mode Activated
+
42
+
+ `; + setTimeout(() => { + overlay.style.transition = 'opacity 0.5s'; + overlay.style.opacity = '0'; + setTimeout(() => overlay.remove(), 500); + activateH2G2Mode(false); + showToast("๐Ÿ‹ The whale and the bowl of petunias send their regards", { duration: 4000 }); + }, 1500); + }, 200); + } + + // Start animation + requestAnimationFrame(animate); +} + export async function sendMessage(text) { const pendingAttachments = state.getPendingAttachments(); const currentConversationId = state.getCurrentConversationId(); @@ -668,6 +1163,70 @@ export async function sendMessage(text) { triggerHeartsAnimation(); } + // Easter egg: /dance command - UI wiggles + if (text.trim().toLowerCase() === '/dance') { + triggerDanceMode(); + messageInput.value = ''; + autoResizeInput(); + return; // Don't send as a message + } + + // Easter egg: /matrix command - green falling code + if (text.trim().toLowerCase() === '/matrix') { + triggerMatrixMode(); + messageInput.value = ''; + autoResizeInput(); + return; // Don't send as a message + } + + // Easter egg: Hitchhiker's Mode triggers + const h2g2Commands = ['/hitchhikers', '/h2g2', '/42']; + if (h2g2Commands.includes(text.trim().toLowerCase())) { + activateH2G2Mode(); + messageInput.value = ''; + autoResizeInput(); + return; + } + + // Easter egg: /improbability - whale and petunias + if (text.trim().toLowerCase() === '/improbability') { + triggerImprobabilityDrive(); + messageInput.value = ''; + autoResizeInput(); + return; + } + + // Easter egg: /normalcy - exit h2g2 mode + if (text.trim().toLowerCase() === '/normalcy') { + if (h2g2Mode) { + deactivateH2G2Mode(); + // Restore previous theme if we have one + const themeLink = document.getElementById('color-theme-link'); + if (themeLink && previousTheme) { + themeLink.href = previousTheme; + } + showToast('๐ŸŒ Returning to normalcy...', { duration: 2000 }); + } else { + showToast('You were already normal. Relatively speaking.', { duration: 2000 }); + } + messageInput.value = ''; + autoResizeInput(); + return; + } + + // Easter egg: exactly 42 characters - Hitchhiker's Guide + if (text.trim().length === 42) { + triggerHitchhikersEgg(); + } + + // Easter egg: "don't panic" - large friendly letters + if (DONT_PANIC_PATTERN.test(text)) { + triggerDontPanic(); + } + + // Easter egg: Marvin quotes on frustration/sadness (30% chance) + maybeShowMarvinQuote(text); + // Queue if offline (without attachments) if (!ws || ws.readyState !== WebSocket.OPEN) { if (pendingAttachments.length > 0) { @@ -1180,6 +1739,13 @@ function closeChatMoreMenu() { // --- Setup all event listeners --- export function setupEventListeners(createConversation) { + // Deactivate h2g2 mode when theme changes + window.addEventListener('color-theme-changed', (e) => { + if (h2g2Mode && e.detail?.theme !== 'h2g2') { + deactivateH2G2Mode(); + } + }); + // Bell easter egg - header icon const brandIcon = document.querySelector('.brand-icon'); if (brandIcon) { diff --git a/public/js/ui/theme.js b/public/js/ui/theme.js index 0d2fc94..7e6f37f 100644 --- a/public/js/ui/theme.js +++ b/public/js/ui/theme.js @@ -268,6 +268,9 @@ export function selectColorTheme(newTheme) { // Don't close dropdown - let user compare themes by clicking through const info = COLOR_THEMES[newTheme] || { name: newTheme }; showToast(`Color theme: ${info.name}`); + + // Dispatch event for easter egg mode cleanup + window.dispatchEvent(new CustomEvent('color-theme-changed', { detail: { theme: newTheme } })); } export function updateColorThemeIcon() { diff --git a/public/sw.js b/public/sw.js index db2d239..21754c8 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'concierge-v108'; +const CACHE_NAME = 'concierge-v117'; const STATIC_ASSETS = [ '/', '/index.html',